[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    [\"@babel/preset-env\", { \"targets\": { \"chrome\": \"110\" } }],\n    [\"@babel/preset-react\", { \"runtime\": \"automatic\" }]\n  ]\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: alyssaxuu\n"
  },
  {
    "path": ".gitignore",
    "content": "logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nnode_modules/\n.DS_Store\n.vscode/\n.env\n.env.local\n.env.development\n.env.production\n.env.*.local\nbuild/\ndocs/\ntools/"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\nhi@alyssax.com.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\nCopyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\nEveryone is permitted to copy and distribute verbatim copies\nof this license document, but changing it is not allowed.\n\n                            Preamble\n\nThe GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works. By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users. We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors. You can apply it to\nyour programs, too.\n\nWhen we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nTo protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights. Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\nFor example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received. You must make sure that they, too, receive\nor can get the source code. And you must show them these terms so they\nknow their rights.\n\nDevelopers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\nFor the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software. For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\nSome devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so. This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software. The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable. Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts. If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\nFinally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary. To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n0. Definitions.\n\n\"This License\" refers to version 3 of the GNU General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense. Each licensee is addressed as \"you\". \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy. The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy. Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies. Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License. If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it. \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form. A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities. However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work. For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met. This License explicitly affirms your unlimited\npermission to run the unmodified Program. The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work. This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force. You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright. Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below. Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit. Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n6. Conveying Non-Source Forms.\n\nYou may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling. In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage. For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product. A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source. The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information. But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed. Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law. If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit. (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.) You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10. If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term. If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License. Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License. If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program. Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance. However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work. These actions infringe copyright if you do\nnot accept this License. Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License. You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations. If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License. For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based. The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version. For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement). To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients. \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License. You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all. For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n13. Use with the GNU Affero General Public License.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work. The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n14. Revised Versions of this License.\n\nThe Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time. Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation. If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions. However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License. Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\nYou should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\nThe GNU General Public License does not permit incorporating your program\ninto proprietary programs. If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library. If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License. But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "# Screenity\n\n> _✨ Screenity's open source work is sponsored by_\n> ### Recall.ai - API for desktop recording [<img src=\"https://github.com/user-attachments/assets/eee643f1-1b64-4d14-bb04-498c321fd937\" align=\"right\">](https://www.recall.ai/product/desktop-recording-sdk?utm_source=github&utm_medium=sponsorship&utm_campaign=alyssaxuu-screenity)\n>\n> If you’re looking for a hosted desktop recording API, consider checking out [Recall.ai](https://www.recall.ai/product/desktop-recording-sdk?utm_source=github&utm_medium=sponsorship&utm_campaign=alyssaxuu-screenity), an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.\n\n[![jiewjjc232](https://github.com/alyssaxuu/screenity/assets/7581348/ed55e52e-4adf-442b-b774-6856abacdffb)](https://screenity.io)\n\nThe free and privacy-friendly screen recorder with no limits 🎥\n\n[Get it now - it's free!](https://chrome.google.com/webstore/detail/screenity-screen-recorder/kbbdabhdfibnancpjfhlkhafgdilcnji)\n\nScreenity is a powerful privacy-friendly screen recorder and annotation tool to make better videos for work, education, and more. You can create stunning product demos, tutorials, presentations, or share feedback with your team - all for free.\n\n<a href=\"https://www.producthunt.com/posts/screenity?utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-screenity\" target=\"_blank\"><img src=\"https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=275308&theme=light&period=daily\" alt=\"Screenity - The most powerful screen recorder for Chrome | Product Hunt\" style=\"width: 250px; height: 54px;\" width=\"250\" height=\"54\" /></a>\n<a href=\"https://news.ycombinator.com/item?id=25150804\" target=\"_blank\"><img height=53 src=\"https://hackerbadge.now.sh/api?id=25150804&type=orange\" alt=\"Featured on HackerNews\"></a>\n\n> Want to support the project (and the solo developer behind it)?  \n>\n> Check out [**Screenity Pro**](https://screenity.io/pro): a privacy-friendly, EU-hosted platform with link sharing, multi-scene editing, zoom keyframes, captions, and more ❤️  \n\nMade by [Alyssa X](https://alyssax.com)\n\n## Table of contents\n- [Screenity](#screenity)\n\t- [Table of contents](#table-of-contents)\n\t- [Features](#features)\n\t- [Self-hosting Screenity](#self-hosting-screenity)\n\t- [Creating a development version](#creating-a-development-version)\n\t\t- [Enabling Save to Google Drive](#enabling-save-to-google-drive)\n\t- [Acknowledgements](#acknowledgements)\n\n## Features\n\n🎥 Make unlimited recordings of your tab, a specific area, desktop, any application, or camera<br>\n🎙️ Record your microphone or internal audio, and use features like push to talk<br>\n✏️ Annotate by drawing anywhere on the screen, adding text, arrows, shapes, and more<br>\n✨ Use AI-powered camera backgrounds or blur to enhance your recordings<br>\n🔎 Zoom in smoothly in your recordings to focus on specific areas<br>\n🪄 Blur out any sensitive content of any page to keep it private<br>\n✂️ Remove or add audio, cut, trim, or crop your recordings with a comprehensive editor<br>\n👀 Highlight your clicks and cursor, and go in spotlight mode<br>\n⏱️ Set up alarms to automatically stop your recording<br>\n💾 Export as mp4, gif, and webm, or save the video directly to Google Drive to share a link<br>\n⚙️ Set a countdown, hide parts of the UI, or move it anywhere<br>\n🔒 Only you can see your videos, we don’t collect any of your data. You can even go offline!<br>\n💙 No limits, make as many videos as you want, for as long as you want<br> …and much more - all for free & no sign in needed!\n\n## Self-hosting Screenity\n> 🛠️ Note: When self-hosted, the extension runs entirely in local-only mode.  \n> No API calls, sign-in flows, or platform features are enabled, nothing is sent anywhere.  \n> Some internal code paths connect to [Screenity Pro](https://screenity.io/pro), but these are only active in the official Chrome Store version.\n\nYou can run Screenity locally without having to install it from the Chrome Store. Here's how:\n\n1. Download the latest Build.zip from the [releases page](https://github.com/alyssaxuu/screenity/releases)\n2. Load the extension by pasting `chrome://extensions/` in the address bar, and [enabling developer mode](https://developer.chrome.com/docs/extensions/mv2/faq/#:~:text=You%20can%20start%20by%20turning,a%20packaged%20extension%2C%20and%20more.).\n3. Drag the folder that contains the code (make sure it's a folder and not a ZIP file, so unzip first), or click on the \"Load unpacked\" button and locate the folder.\n4. That's it, you should now be able to use Screenity locally. [Follow these instructions](#enabling-save-to-google-drive) to set up the Google Drive integration.\n\n<small>Self-hosting is totally fine for personal, educational, or internal use.\nIf you’re thinking of building a commercial product from this, feel free to [reach out](mailto:alyssa@screenity.io), I’m open to chatting 💜</small>\n\n## Creating a development version\n\n> ❗️ Note that the license has changed to [GPLv3](https://github.com/alyssaxuu/screenity/blob/master/LICENSE) for the current MV3 version (Screenity version 3.0.0 and higher). Make sure to read the license and the [Terms of Service](https://screenity.io/en/terms/) regarding intellectual property.\n\n1. Check if your [Node.js](https://nodejs.org/) version is >= **14**.\n2. Clone this repository.\n3. Run `npm install` to install dependencies.\n4. Run `npm start` to start the local development server.\n5. Open `chrome://extensions/` in your browser and [enable developer mode](https://developer.chrome.com/docs/extensions/mv2/faq/#:~:text=You%20can%20start%20by%20turning,a%20packaged%20extension%2C%20and%20more.).\n6. Click **Load unpacked** and select the `build` folder.\n7. The extension should now be available locally.  \n   To rebuild after code changes, run `npm run build`.\n\n### Enabling Save to Google Drive\n\nTo enable the Google Drive Upload (authorization consent screen) you must change the client_id in the manifest.json file with your linked extension key.\n\nYou can create it accessing [Google Cloud Console](https://console.cloud.google.com/apis/credentials) and selecting Create Credential > OAuth Client ID > Chrome App. To create a persistent extension key, you can follow the steps detailed [here](https://developer.chrome.com/docs/extensions/reference/manifest/key).\n\n## Acknowledgements\n\n- Thanks to [HelpKit](https://www.helpkit.so/) for sponsoring this project by hosting the [Screenity Help Center](https://help.screenity.io/).\n- Thanks to [Mei Xuan](https://www.behance.net/meixuanloo) for helping with the Chinese translation of the extension.\n\nIf you need any help, or want to become a Screenity expert, you can browse articles and guides in the [help center](https://help.screenity.io). You can also submit any feedback or ideas in this [form](https://tally.so/r/3ElpXq), or contact through [this page](https://help.screenity.io/contact)\n"
  },
  {
    "path": "custom.config.js",
    "content": "module.exports = {\n  notHMR: [\"background\", \"contentScript\", \"devtools\", \"sandbox\"],\n  enableBackgroundAutoReload: true,\n  enableContentScriptsAutoReload: true,\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"screenity\",\n  \"version\": \"4.3.0\",\n  \"description\": \"The free and privacy-friendly screen recorder with no sign in needed.\",\n  \"scripts\": {\n    \"build\": \"cross-env NODE_ENV=production node utils/build.js\",\n    \"start\": \"cross-env NODE_ENV=development node utils/server.js\",\n    \"dev\": \"npm run hot-reload\",\n    \"watch\": \"cross-env NODE_ENV=development webpack --watch\",\n    \"hot-reload\": \"cross-env NODE_ENV=development webpack serve --hot\",\n    \"prettier\": \"prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'\",\n    \"postinstall\": \"patch-package && node scripts/patch-radix.js\",\n    \"package\": \"npm run build && cd build && zip -r ../extension.zip . && cd ..\",\n    \"clean\": \"rm -rf build && mkdir build\",\n    \"lint\": \"eslint src/**/*.js --fix\"\n  },\n  \"dependencies\": {\n    \"@babel/plugin-transform-react-jsx\": \"^7.27.1\",\n    \"@mediapipe/tasks-vision\": \"0.10.32\",\n    \"@radix-ui/react-alert-dialog\": \"^1.0.4\",\n    \"@radix-ui/react-collapsible\": \"^1.0.2\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.0.6\",\n    \"@radix-ui/react-popover\": \"^1.0.5\",\n    \"@radix-ui/react-select\": \"^1.2.1\",\n    \"@radix-ui/react-slider\": \"^1.1.2\",\n    \"@radix-ui/react-switch\": \"^1.0.2\",\n    \"@radix-ui/react-tabs\": \"^1.0.3\",\n    \"@radix-ui/react-toast\": \"^1.2.13\",\n    \"@radix-ui/react-toggle\": \"^1.1.8\",\n    \"@radix-ui/react-toggle-group\": \"^1.1.9\",\n    \"@radix-ui/react-toolbar\": \"^1.1.9\",\n    \"@radix-ui/react-tooltip\": \"^1.2.6\",\n    \"@sentry/browser\": \"^7.94.1\",\n    \"@uiw/color-convert\": \"^1.2.2\",\n    \"@uiw/react-color-wheel\": \"^1.2.2\",\n    \"axios\": \"^1.6.2\",\n    \"concurrently\": \"^8.2.0\",\n    \"dotenv\": \"^17.0.1\",\n    \"driver.js\": \"^1.3.6\",\n    \"fabric\": \"^5.3.0\",\n    \"fix-webm-duration\": \"^1.0.5\",\n    \"gif.js\": \"^0.2.0\",\n    \"jszip\": \"^3.10.1\",\n    \"localforage\": \"^1.10.0\",\n    \"mediabunny\": \"^1.24.4\",\n    \"patch-package\": \"^8.0.0\",\n    \"plyr\": \"3.7.8\",\n    \"plyr-react\": \"^5.3.0\",\n    \"querystring\": \"^0.2.1\",\n    \"raw-loader\": \"^4.0.2\",\n    \"react\": \"^18.2.0\",\n    \"react-advanced-cropper\": \"^0.19.4\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-hotkeys-hook\": \"^4.4.1\",\n    \"react-rnd\": \"^10.4.1\",\n    \"react-shadow\": \"^20.0.0\",\n    \"react-svg\": \"^16.1.18\",\n    \"ssestream\": \"^1.1.0\",\n    \"tar\": \"7.5.9\",\n    \"wavesurfer.js\": \"^7.4.2\",\n    \"webm-duration-fix\": \"^1.0.4\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.20.2\",\n    \"@babel/plugin-transform-runtime\": \"^7.27.1\",\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"@babel/preset-react\": \"^7.27.1\",\n    \"@types/react\": \"^18.3.21\",\n    \"@types/react-dom\": \"^18.3.7\",\n    \"babel-loader\": \"^9.1.3\",\n    \"copy-webpack-plugin\": \"^7.0.0\",\n    \"cross-env\": \"^7.0.3\",\n    \"css-loader\": \"^6.7.2\",\n    \"fs-extra\": \"^10.1.0\",\n    \"html-loader\": \"^3.1.0\",\n    \"html-webpack-plugin\": \"^5.5.0\",\n    \"prettier\": \"^2.7.1\",\n    \"sass\": \"^1.97.3\",\n    \"sass-loader\": \"^16.0.7\",\n    \"source-map-loader\": \"^3.0.1\",\n    \"style-loader\": \"^3.3.1\",\n    \"terser-webpack-plugin\": \"^5.3.6\",\n    \"ts-loader\": \"^9.4.1\",\n    \"typescript\": \"^4.9.3\",\n    \"webpack-cli\": \"^5.1.4\",\n    \"webpack-dev-server\": \"^4.11.1\"\n  },\n  \"overrides\": {\n    \"tar\": \"7.5.9\",\n    \"minimatch\": \"10.2.2\"\n  }\n}\n"
  },
  {
    "path": "patches/fabric+5.3.0.patch",
    "content": "diff --git a/node_modules/fabric/dist/fabric.js b/node_modules/fabric/dist/fabric.js\nindex faee7fc..02e5ae3 100644\n--- a/node_modules/fabric/dist/fabric.js\n+++ b/node_modules/fabric/dist/fabric.js\n@@ -12591,7 +12591,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab\n       this.cacheCanvasEl = this._createCanvasElement();\n       this.cacheCanvasEl.setAttribute('width', this.width);\n       this.cacheCanvasEl.setAttribute('height', this.height);\n-      this.contextCache = this.cacheCanvasEl.getContext('2d');\n+      this.contextCache = this.cacheCanvasEl.getContext('2d', {willReadFrequently: true});\n     },\n \n     /**\n@@ -15103,7 +15103,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati\n     _createCacheCanvas: function() {\n       this._cacheProperties = {};\n       this._cacheCanvas = fabric.util.createCanvasElement();\n-      this._cacheContext = this._cacheCanvas.getContext('2d');\n+      this._cacheContext = this._cacheCanvas.getContext('2d', {willReadFrequently: true});\n       this._updateCacheCanvas();\n       // if canvas gets created, is empty, so dirty.\n       this.dirty = true;\n"
  },
  {
    "path": "patches/fabric+5.5.2.patch",
    "content": "diff --git a/node_modules/fabric/dist/fabric.js b/node_modules/fabric/dist/fabric.js\nindex a7ad0f5..d8e7670 100644\n--- a/node_modules/fabric/dist/fabric.js\n+++ b/node_modules/fabric/dist/fabric.js\n@@ -12596,7 +12596,7 @@ fabric.PatternBrush = fabric.util.createClass(fabric.PencilBrush, /** @lends fab\n       this.cacheCanvasEl = this._createCanvasElement();\n       this.cacheCanvasEl.setAttribute('width', this.width);\n       this.cacheCanvasEl.setAttribute('height', this.height);\n-      this.contextCache = this.cacheCanvasEl.getContext('2d');\n+      this.contextCache = this.cacheCanvasEl.getContext('2d', {willReadFrequently: true});\n     },\n \n     /**\n@@ -15108,7 +15108,7 @@ fabric.util.object.extend(fabric.StaticCanvas.prototype, /** @lends fabric.Stati\n     _createCacheCanvas: function() {\n       this._cacheProperties = {};\n       this._cacheCanvas = fabric.util.createCanvasElement();\n-      this._cacheContext = this._cacheCanvas.getContext('2d');\n+      this._cacheContext = this._cacheCanvas.getContext('2d', {willReadFrequently: true});\n       this._updateCacheCanvas();\n       // if canvas gets created, is empty, so dirty.\n       this.dirty = true;\n"
  },
  {
    "path": "patches/plyr+3.7.8.patch",
    "content": "diff --git a/node_modules/plyr/dist/plyr.js b/node_modules/plyr/dist/plyr.js\nindex 0a79517..e69634f 100644\n--- a/node_modules/plyr/dist/plyr.js\n+++ b/node_modules/plyr/dist/plyr.js\n@@ -3539,21 +3539,7 @@ typeof navigator === \"object\" && (function (global, factory) {\n       }\n     },\n     // URLs\n-    urls: {\n-      download: null,\n-      vimeo: {\n-        sdk: 'https://player.vimeo.com/api/player.js',\n-        iframe: 'https://player.vimeo.com/video/{0}?{1}',\n-        api: 'https://vimeo.com/api/oembed.json?url={0}'\n-      },\n-      youtube: {\n-        sdk: 'https://www.youtube.com/iframe_api',\n-        api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\n-      },\n-      googleIMA: {\n-        sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\n-      }\n-    },\n+    urls: null,\n     // Custom control listeners\n     listeners: {\n       seek: null,\ndiff --git a/node_modules/plyr/dist/plyr.min.js b/node_modules/plyr/dist/plyr.min.js\nindex 707308e..25fc326 100644\n--- a/node_modules/plyr/dist/plyr.min.js\n+++ b/node_modules/plyr/dist/plyr.min.js\n@@ -1,2 +1,2 @@\n-\"object\"==typeof navigator&&function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(\"Plyr\",t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).Plyr=t()}(this,(function(){\"use strict\";function e(e,t,i){return(t=function(e){var t=function(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}(e,\"string\");return\"symbol\"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function t(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function i(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function s(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function n(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?s(Object(n),!0).forEach((function(t){i(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):s(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var a={addCSS:!0,thumbWidth:15,watch:!0};var l=function(e){return null!=e?e.constructor:null},r=function(e,t){return!!(e&&t&&e instanceof t)},o=function(e){return null==e},c=function(e){return l(e)===Object},u=function(e){return l(e)===String},h=function(e){return Array.isArray(e)},d=function(e){return r(e,NodeList)},m={nullOrUndefined:o,object:c,number:function(e){return l(e)===Number&&!Number.isNaN(e)},string:u,boolean:function(e){return l(e)===Boolean},function:function(e){return l(e)===Function},array:h,nodeList:d,element:function(e){return r(e,Element)},event:function(e){return r(e,Event)},empty:function(e){return o(e)||(u(e)||h(e)||d(e))&&!e.length||c(e)&&!Object.keys(e).length}};function p(e,t){if(1>t){var i=function(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var g=function(){function e(t,i){(function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")})(this,e),m.element(t)?this.element=t:m.string(t)&&(this.element=document.querySelector(t)),m.element(this.element)&&m.empty(this.element.rangeTouch)&&(this.config=n({},a,{},i),this.init())}return function(e,i,s){i&&t(e.prototype,i),s&&t(e,s)}(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!m.event(t))return null;var i,s=t.target,n=t.changedTouches[0],a=parseFloat(s.getAttribute(\"min\"))||0,l=parseFloat(s.getAttribute(\"max\"))||100,r=parseFloat(s.getAttribute(\"step\"))||1,o=s.getBoundingClientRect(),c=100/o.width*(this.config.thumbWidth/2)/100;return 0>(i=100/o.width*(n.clientX-o.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),a+p(i/100*(l-a),r)}},{key:\"set\",value:function(t){e.enabled&&m.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),function(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(m.empty(t)||m.string(t)?s=Array.from(document.querySelectorAll(m.string(t)?t:'input[type=\"range\"]')):m.element(t)?s=[t]:m.nodeList(t)?s=Array.from(t):m.array(t)&&(s=t.filter(m.element)),m.empty(s))return null;var l=n({},a,{},i);if(m.string(t)&&l.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){m.element(i)&&function(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}(i,t)&&new e(i,l)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const f=e=>null!=e?e.constructor:null,y=(e,t)=>Boolean(e&&t&&e instanceof t),b=e=>null==e,v=e=>f(e)===Object,w=e=>f(e)===String,T=e=>\"function\"==typeof e,k=e=>Array.isArray(e),C=e=>y(e,NodeList),A=e=>b(e)||(w(e)||k(e)||C(e))&&!e.length||v(e)&&!Object.keys(e).length;var S={nullOrUndefined:b,object:v,number:e=>f(e)===Number&&!Number.isNaN(e),string:w,boolean:e=>f(e)===Boolean,function:T,array:k,weakMap:e=>y(e,WeakMap),nodeList:C,element:e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,textNode:e=>f(e)===Text,event:e=>y(e,Event),keyboardEvent:e=>y(e,KeyboardEvent),cue:e=>y(e,window.TextTrackCue)||y(e,window.VTTCue),track:e=>y(e,TextTrack)||!b(e)&&w(e.kind),promise:e=>y(e,Promise)&&T(e.then),url:e=>{if(y(e,window.URL))return!0;if(!w(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!A(new URL(t).hostname)}catch(e){return!1}},empty:A};const E=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!S.string(i)&&t[i]})();function P(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}var M={isIE:Boolean(window.document.documentMode),isEdge:/Edge/g.test(navigator.userAgent),isWebKit:\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone:/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS:\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos:/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1};function N(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function x(e={},...t){if(!t.length)return e;const i=t.shift();return S.object(i)?(Object.keys(i).forEach((t=>{S.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),x(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),x(e,...t)):e}function L(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,a=e.nextSibling;s.appendChild(e),a?n.insertBefore(s,a):n.appendChild(s)}))}function I(e,t){S.element(e)&&!S.empty(t)&&Object.entries(t).filter((([,e])=>!S.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function $(e,t,i){const s=document.createElement(e);return S.object(t)&&I(s,t),S.string(i)&&(s.innerText=i),s}function _(e,t,i,s){S.element(t)&&t.appendChild($(e,i,s))}function O(e){S.nodeList(e)||S.array(e)?Array.from(e).forEach(O):S.element(e)&&S.element(e.parentNode)&&e.parentNode.removeChild(e)}function j(e){if(!S.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function q(e,t){return S.element(t)&&S.element(t.parentNode)&&S.element(e)?(t.parentNode.replaceChild(e,t),e):null}function D(e,t){if(!S.string(e)||S.empty(e))return{};const i={},s=x({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),a=t.replace(/[[\\]]/g,\"\").split(\"=\"),[l]=a,r=a.length>1?a[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":S.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[l]=r}})),x(s,i)}function H(e,t){if(!S.element(e))return;let i=t;S.boolean(i)||(i=!e.hidden),e.hidden=i}function R(e,t,i){if(S.nodeList(e))return Array.from(e).map((e=>R(e,t,i)));if(S.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function F(e,t){return S.element(e)&&e.classList.contains(t)}function V(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function U(e){return this.elements.container.querySelectorAll(e)}function B(e){return this.elements.container.querySelector(e)}function W(e=null,t=!1){S.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const z={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},K={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=K[e]||\"html5\"!==t;return{api:i,ui:i&&K.rangeInput}},pip:!(M.isIPhone||!S.function($(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||$(\"video\").disablePictureInPicture)),airplay:S.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(S.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(z).includes(i)&&(i+=`; codecs=\"${z[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==E,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},Y=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function Q(e,t,i,s=!1,n=!0,a=!1){if(!e||!(\"addEventListener\"in e)||S.empty(t)||!S.function(i))return;const l=t.split(\" \");let r=a;Y&&(r={passive:n,capture:a}),l.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:r}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,r)}))}function X(e,t=\"\",i,s=!0,n=!1){Q.call(this,e,t,i,!0,s,n)}function J(e,t=\"\",i,s=!0,n=!1){Q.call(this,e,t,i,!1,s,n)}function G(e,t=\"\",i,s=!0,n=!1){const a=(...l)=>{J(e,t,a,s,n),i.apply(this,l)};Q.call(this,e,t,a,!0,s,n)}function Z(e,t=\"\",i=!1,s={}){if(!S.element(e)||S.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function ee(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function te(){return new Promise((e=>this.ready?setTimeout(e,0):X.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function ie(e){S.promise(e)&&e.then(null,(()=>{}))}function se(e){return S.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function ne(e,t){return S.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function ae(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const le=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function re(e){if(!(S.array(e)||S.string(e)&&e.includes(\":\")))return!1;return(S.array(e)?e:e.split(\":\")).map(Number).every(S.number)}function oe(e){if(!S.array(e)||!e.every(S.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function ce(e){const t=e=>re(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!S.empty(this.embed)&&S.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return oe(i)}function ue(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=ce.call(this,e);if(!S.array(i))return{};const[s,n]=oe(i),a=100/s*n;if(ae(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${a}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-a)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:a,ratio:i}}function he(e,t,i=.05){const s=e/t,n=ne(Object.keys(le),s);return Math.abs(n-s)<=i?le[n]:[e,t]}const de={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!S.empty(t)||K.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:de.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,S.empty(this.config.ratio)||ue.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=de.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&S.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=de.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:a,readyState:l,playbackRate:r}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==a||l)&&(e.once(\"loadedmetadata\",(()=>{e.speed=r,e.currentTime=s,n||ie(e.play())})),e.media.load())}Z.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(O(de.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function me(e,...t){return S.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}const pe=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),ge=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function fe(e=\"\"){let t=e.toString();return t=function(e=\"\"){let t=e.toString();return t=pe(t,\"-\",\" \"),t=pe(t,\"_\",\" \"),t=ge(t),pe(t,\" \",\"\")}(t),t.charAt(0).toLowerCase()+t.slice(1)}function ye(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const be={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},ve={get(e=\"\",t={}){if(S.empty(e)||S.empty(t))return\"\";let i=N(t.i18n,e);if(S.empty(i))return Object.keys(be).includes(e)?be[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=pe(i,e,t)})),i}};class we{constructor(t){e(this,\"get\",(e=>{if(!we.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(S.empty(t))return null;const i=JSON.parse(t);return S.string(e)&&e.length?i[e]:i})),e(this,\"set\",(e=>{if(!we.supported||!this.enabled)return;if(!S.object(e))return;let t=this.get();S.empty(t)&&(t={}),x(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=t.config.storage.enabled,this.key=t.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function Te(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function ke(e,t){if(!S.string(e))return;const i=\"cache\",s=S.string(t);let n=!1;const a=()=>null!==document.getElementById(t),l=(e,t)=>{e.innerHTML=t,s&&a()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!a()){const a=we.supported,r=document.createElement(\"div\");if(r.setAttribute(\"hidden\",\"\"),s&&r.setAttribute(\"id\",t),a){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);l(r,t.content)}}Te(e).then((e=>{if(!S.empty(e)){if(a)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}l(r,e)}})).catch((()=>{}))}}const Ce=e=>Math.trunc(e/60/60%60,10),Ae=e=>Math.trunc(e/60%60,10),Se=e=>Math.trunc(e%60,10);function Ee(e=0,t=!1,i=!1){if(!S.number(e))return Ee(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=Ce(e);const a=Ae(e),l=Se(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(a)}:${s(l)}`}const Pe={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||M.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=B.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:U.call(this,this.config.selectors.buttons.play),pause:B.call(this,this.config.selectors.buttons.pause),restart:B.call(this,this.config.selectors.buttons.restart),rewind:B.call(this,this.config.selectors.buttons.rewind),fastForward:B.call(this,this.config.selectors.buttons.fastForward),mute:B.call(this,this.config.selectors.buttons.mute),pip:B.call(this,this.config.selectors.buttons.pip),airplay:B.call(this,this.config.selectors.buttons.airplay),settings:B.call(this,this.config.selectors.buttons.settings),captions:B.call(this,this.config.selectors.buttons.captions),fullscreen:B.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=B.call(this,this.config.selectors.progress),this.elements.inputs={seek:B.call(this,this.config.selectors.inputs.seek),volume:B.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:B.call(this,this.config.selectors.display.buffer),currentTime:B.call(this,this.config.selectors.display.currentTime),duration:B.call(this,this.config.selectors.display.duration)},S.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=Pe.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,a=document.createElementNS(i,\"svg\");I(a,x(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const l=document.createElementNS(i,\"use\"),r=`${n}-${e}`;return\"href\"in l&&l.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",r),l.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",r),a.appendChild(l),a},createLabel(e,t={}){const i=ve.get(e,this.config);return $(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(S.empty(e))return null;const t=$(\"span\",{class:this.config.classNames.menu.value});return t.appendChild($(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=x({},t);let s=fe(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||x(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:S.empty(n.label)&&(n.label=s),S.empty(n.icon)&&(n.icon=e)}const a=$(n.element);return n.toggle?(a.appendChild(Pe.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),a.appendChild(Pe.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),a.appendChild(Pe.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),a.appendChild(Pe.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(a.appendChild(Pe.createIcon.call(this,n.icon)),a.appendChild(Pe.createLabel.call(this,n.label))),x(i,D(this.config.selectors.buttons[s],i)),I(a,i),\"play\"===s?(S.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(a)):this.elements.buttons[s]=a,a},createRange(e,t){const i=$(\"input\",x(D(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":ve.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,Pe.updateRangeFill.call(this,i),g.setup(i),i},createProgress(e,t){const i=$(\"progress\",x(D(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild($(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?ve.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=D(this.config.selectors.display[e],t),s=$(\"div\",x(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":ve.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){X.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=V(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))Pe.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,S.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,S.element(t)||(t=e.parentNode.lastElementChild)),W.call(this,t,!0))}}),!1),X.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&Pe.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:a=!1}){const l=D(this.config.selectors.inputs[i]),r=$(\"button\",x(l,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${l.class?l.class:\"\"}`.trim(),\"aria-checked\":a,value:e})),o=$(\"span\");o.innerHTML=s,S.element(n)&&o.appendChild(n),r.appendChild(o),Object.defineProperty(r,\"checked\",{enumerable:!0,get:()=>\"true\"===r.getAttribute(\"aria-checked\"),set(e){e&&Array.from(r.parentNode.children).filter((e=>V(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),r.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(r,\"click keyup\",(t=>{if(!S.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),r.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}Pe.showMenuPanel.call(this,\"home\",S.keyboardEvent(t))}}),i,!1),Pe.bindMenuItemShortcuts.call(this,r,i),t.appendChild(r)},formatTime(e=0,t=!1){if(!S.number(e))return e;return Ee(e,Ce(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){S.element(e)&&S.number(t)&&(e.innerText=Pe.formatTime(t,i))},updateVolume(){this.supported.ui&&(S.element(this.elements.inputs.volume)&&Pe.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),S.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){S.element(e)&&(e.value=t,Pe.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!S.event(e))return;let t=0;const i=(e,t)=>{const i=S.number(t)?t:0,s=S.element(e)?e:this.elements.display.buffer;if(S.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];S.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":s=this.currentTime,n=this.duration,t=0===s||0===n||Number.isNaN(s)||Number.isNaN(n)?0:(s/n*100).toFixed(2),\"timeupdate\"===e.type&&Pe.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}var s,n},updateRangeFill(e){const t=S.event(e)?e.target:e;if(S.element(t)&&\"range\"===t.getAttribute(\"type\")){if(V(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=Pe.formatTime(this.currentTime),i=Pe.formatTime(this.duration),s=ve.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(V(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(M.isWebKit||M.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!S.element(this.elements.inputs.seek)||!S.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,a=e=>R(s,n,e);if(this.touch)return void a(!1);let l=0;const r=this.elements.progress.getBoundingClientRect();if(S.event(e))l=100/r.width*(e.pageX-r.left);else{if(!F(s,n))return;l=parseFloat(s.style.left,10)}l<0?l=0:l>100&&(l=100);const o=this.duration/100*l;s.innerText=Pe.formatTime(o);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(o)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${l}%`,S.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&a(\"mouseenter\"===e.type)},timeUpdate(e){const t=!S.element(this.elements.display.duration)&&this.config.invertTime;Pe.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||Pe.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return H(this.elements.display.currentTime,!0),void H(this.elements.progress,!0);S.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=S.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&Pe.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&Pe.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&Pe.setMarkers.call(this),Pe.updateSeekTooltip.call(this)},toggleMenuButton(e,t){H(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,a=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=S.empty(i)?this[e]:i,S.empty(n)&&(n=this.config[e].default),!S.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(S.element(a)||(a=s&&s.querySelector('[role=\"menu\"]')),!S.element(a))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=Pe.getLabel.call(this,e,n);const l=a&&a.querySelector(`[value=\"${n}\"]`);S.element(l)&&(l.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?ve.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(S.number(t)){const e=ve.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return ge(t);case\"captions\":return xe.getLabel.call(this);default:return null}},setQualityMenu(e){if(!S.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');S.array(e)&&(this.options.quality=se(e).filter((e=>this.config.quality.options.includes(e))));const s=!S.empty(this.options.quality)&&this.options.quality.length>1;if(Pe.toggleMenuButton.call(this,t,s),j(i),Pe.checkMenu.call(this),!s)return;const n=e=>{const t=ve.get(`qualityBadge.${e}`,this.config);return t.length?Pe.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{Pe.createMenuItem.call(this,{value:e,list:i,type:t,title:Pe.getLabel.call(this,\"quality\",e),badge:n(e)})})),Pe.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!S.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=xe.getTracks.call(this),s=Boolean(i.length);if(Pe.toggleMenuButton.call(this,e,s),j(t),Pe.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:xe.getLabel.call(this,e),badge:e.language&&Pe.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:ve.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(Pe.createMenuItem.bind(this)),Pe.updateSetting.call(this,e,t)},setSpeedMenu(){if(!S.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!S.empty(this.options.speed)&&this.options.speed.length>1;Pe.toggleMenuButton.call(this,e,i),j(t),Pe.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{Pe.createMenuItem.call(this,{value:i,list:t,type:e,title:Pe.getLabel.call(this,\"speed\",i)})})),Pe.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!S.empty(e)&&Object.values(e).some((e=>!e.hidden));H(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;S.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');W.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!S.element(t)||!S.element(i))return;const{hidden:s}=t;let n=s;if(S.boolean(e))n=e;else if(S.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(S.event(e)){const s=S.function(e.composedPath)?e.composedPath()[0]:e.target,a=t.contains(s);if(a||!a&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),H(t,!n),R(this.elements.container,this.config.classNames.menu.open,n),n&&S.keyboardEvent(e)?Pe.focusFirstMenuItem.call(this,null,!0):n||s||W.call(this,i,S.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return O(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!S.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(K.transitions&&!K.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=Pe.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",J.call(this,s,E,t))};X.call(this,s,E,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}H(n,!0),H(i,!1),Pe.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;S.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:a,setQualityMenu:l,setSpeedMenu:r,showMenuPanel:o}=Pe;this.elements.controls=null,S.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=$(\"div\",D(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return se(S.array(this.config.controls)?this.config.controls:[]).forEach((l=>{if(\"restart\"===l&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===l&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===l&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===l&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===l){const t=$(\"div\",{class:`${u.class} plyr__progress__container`}),i=$(\"div\",D(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=$(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===l&&c.appendChild(a.call(this,\"currentTime\",u)),\"duration\"===l&&c.appendChild(a.call(this,\"duration\",u)),\"mute\"===l||\"volume\"===l){let{volume:t}=this.elements;if(S.element(t)&&c.contains(t)||(t=$(\"div\",x({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===l&&t.appendChild(i.call(this,\"mute\")),\"volume\"===l&&!M.isIos&&!M.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",x(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===l&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===l&&!S.empty(this.config.settings)){const s=$(\"div\",x({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=$(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),a=$(\"div\"),l=$(\"div\",{id:`plyr-settings-${e.id}-home`}),r=$(\"div\",{role:\"menu\"});l.appendChild(r),a.appendChild(l),this.elements.settings.panels.home=l,this.config.settings.forEach((i=>{const s=$(\"button\",x(D(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),X.call(this,s,\"click\",(()=>{o.call(this,i,!1)}));const n=$(\"span\",null,ve.get(i,this.config)),l=$(\"span\",{class:this.config.classNames.menu.value});l.innerHTML=e[i],n.appendChild(l),s.appendChild(n),r.appendChild(s);const c=$(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=$(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild($(\"span\",{\"aria-hidden\":!0},ve.get(i,this.config))),u.appendChild($(\"span\",{class:this.config.classNames.hidden},ve.get(\"menuBack\",this.config))),X.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),o.call(this,\"home\",!0))}),!1),X.call(this,u,\"click\",(()=>{o.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild($(\"div\",{role:\"menu\"})),a.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(a),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===l&&K.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===l&&K.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===l){const e=x({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!S.url(t)&&this.isEmbed&&x(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===l&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&l.call(this,de.getQualityOptions.call(this)),r.call(this),c},inject(){if(this.config.loadSprite){const e=Pe.getIconUrl.call(this);e.cors&&ke(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;S.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),S.element(this.config.controls)||S.string(this.config.controls)?e=this.config.controls:(e=Pe.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:xe.getLabel.call(this)}),i=!1);let s;i&&S.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=pe(i,`{${e}}`,t)})),i})(e)),S.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),S.element(s)||(s=this.elements.container);if(s[S.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),S.element(this.elements.controls)||Pe.findElements.call(this),!S.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>F(e,t),set(i=!1){R(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{S.array(t)||S.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(M.isEdge&&P(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=U.call(this,i);Array.from(s).forEach((e=>{R(e,this.config.classNames.hidden,!1),R(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let a=null;const l=`${this.config.classNames.tooltip}--visible`,r=e=>R(a,l,e);i.forEach((e=>{const t=$(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";a&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(a.style.left=i,a.innerHTML=e.label,r(!0))})),t.addEventListener(\"mouseleave\",(()=>{r(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(a=$(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(a)),this.elements.markers={points:n,tip:a},this.elements.progress.appendChild(s)}};function Me(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function Ne(e){const t=new URLSearchParams;return S.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const xe={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!K.textTracks)return void(S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Pe.setCaptionsMenu.call(this));var e,t;if(S.element(this.elements.captions)||(this.elements.captions=$(\"div\",D(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),e=this.elements.captions,t=this.elements.wrapper,S.element(e)&&S.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)),M.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=Me(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&Te(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{O(e)}))}))}const i=se((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let s=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===s&&([s]=i);let n=this.storage.get(\"captions\");if(S.boolean(n)||({active:n}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:n,language:s,languages:i}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";X.call(this,this.media.textTracks,e,xe.update.bind(this))}setTimeout(xe.update.bind(this),0)},update(){const e=xe.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,a=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),X.call(this,e,\"cuechange\",(()=>xe.updateCues.call(this)))})),(a&&this.language!==i||!e.includes(n))&&(xe.setLanguage.call(this,i),xe.toggle.call(this,t&&a)),this.elements&&R(this.elements.container,this.config.classNames.captions.enabled,!S.empty(e)),S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Pe.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=S.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=xe.getTracks.call(this),t=xe.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void xe.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),R(this.elements.container,s,n),this.captions.toggled=n,Pe.updateSetting.call(this,\"captions\"),Z.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=xe.getTracks.call(this);if(-1!==e)if(S.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,Pe.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),Z.call(this,this.media,\"languagechange\")}xe.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&xe.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else xe.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!S.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=xe.getTracks.call(this),n=xe.findTrack.call(this,[i]);xe.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=xe.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let a;return e.every((e=>(a=n.find((t=>t.language===e)),!a))),a||(t?n[0]:void 0)},getCurrentTrack(){return xe.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!S.track(t)&&K.textTracks&&this.captions.toggled&&(t=xe.getCurrentTrack.call(this)),S.track(t)?S.empty(t.label)?S.empty(t.language)?ve.get(\"enabled\",this.config):e.language.toUpperCase():t.label:ve.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!S.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!S.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=xe.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(ye)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){j(this.elements.captions);const e=$(\"span\",D(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),Z.call(this,this.media,\"cuechange\")}}},Le={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:{download:null,vimeo:{sdk:\"https://player.vimeo.com/api/player.js\",iframe:\"https://player.vimeo.com/video/{0}?{1}\",api:\"https://vimeo.com/api/oembed.json?url={0}\"},youtube:{sdk:\"https://www.youtube.com/iframe_api\",api:\"https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}\"},googleIMA:{sdk:\"https://imasdk.googleapis.com/js/sdkloader/ima3.js\"}},listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},Ie=\"picture-in-picture\",$e=\"inline\",_e={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},Oe=\"audio\",je=\"video\";const qe=()=>{};class De{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):qe}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):qe}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):qe}}class He{constructor(t){e(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;S.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;Z.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),e(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",R(this.target,this.player.config.classNames.fullscreen.fallback,e),M.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=S.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),e(this,\"trapFocus\",(e=>{if(M.isIos||M.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=U.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),e(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":He.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");R(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),e(this,\"enter\",(()=>{this.supported&&(M.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!He.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?S.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),e(this,\"exit\",(()=>{if(this.supported)if(M.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),ie(this.player.play());else if(!He.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!S.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),e(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=t,this.prefix=He.prefix,this.property=He.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===t.config.fullscreen.fallback,this.player.elements.fullscreen=t.config.fullscreen.container&&function(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(V.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}(this.player.elements.container,t.config.fullscreen.container),X.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),X.call(this.player,this.player.elements.container,\"dblclick\",(e=>{S.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),X.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return He.nativeSupported&&!this.forceFallback}static get prefix(){if(S.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!S.function(document[`${t}ExitFullscreen`])&&!S.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,He.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||He.nativeSupported||!M.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!He.nativeSupported||this.forceFallback)return F(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return M.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function Re(e,t=1){return new Promise(((i,s)=>{const n=new Image,a=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:a,onerror:a,src:e})}))}const Fe={addStyleHook(){R(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),R(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void Fe.toggleNativeControls.call(this,!0);S.element(this.elements.controls)||(Pe.inject.call(this),this.listeners.controls()),Fe.toggleNativeControls.call(this),this.isHTML5&&xe.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,Pe.updateVolume.call(this),Pe.timeUpdate.call(this),Pe.durationUpdate.call(this),Fe.checkPlaying.call(this),R(this.elements.container,this.config.classNames.pip.supported,K.pip&&this.isHTML5&&this.isVideo),R(this.elements.container,this.config.classNames.airplay.supported,K.airplay&&this.isHTML5),R(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{Z.call(this,this.media,\"ready\")}),0),Fe.setTitle.call(this),this.poster&&Fe.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&Pe.durationUpdate.call(this),this.config.mediaMetadata&&Pe.setMediaMetadata.call(this)},setTitle(){let e=ve.get(\"play\",this.config);if(S.string(this.config.title)&&!S.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=B.call(this,\"iframe\");if(!S.element(e))return;const t=S.empty(this.config.title)?\"video\":this.config.title,i=ve.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){R(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),te.call(this).then((()=>Re(e))).catch((t=>{throw e===this.poster&&Fe.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),Fe.togglePoster.call(this,!0),e))))},checkPlaying(e){R(this.elements.container,this.config.classNames.playing,this.playing),R(this.elements.container,this.config.classNames.paused,this.paused),R(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",ve.get(this.playing?\"pause\":\"play\",this.config))})),S.event(e)&&\"timeupdate\"===e.type||Fe.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{R(this.elements.container,this.config.classNames.loading,this.loading),Fe.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!S.empty(e)&&S.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),S.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Ve{constructor(t){e(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,R(t.container,e.config.classNames.isTouch,!0)})),e(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&Q.call(t,window,\"keydown keyup\",this.handleKey,e,!1),Q.call(t,document.body,\"click\",this.toggleMenu,e),G.call(t,document.body,\"touchstart\",this.firstTouch)})),e(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&X.call(e,i.container,\"keydown keyup\",this.handleKey,!1),X.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let a=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(Fe.toggleControls.call(e,!0),a=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>Fe.toggleControls.call(e,!1)),a)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,a]=ce.call(e),l=ae(`aspect-ratio: ${n} / ${a}`);if(!s)return void(l?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[r,o]=[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)],c=r/o>n/a;l?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?o/a*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},a=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};X.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&S.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?X:J).call(e,window,\"resize\",a)}))})),e(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(X.call(e,e.media,\"timeupdate seeking seeked\",(t=>Pe.timeUpdate.call(e,t))),X.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>Pe.durationUpdate.call(e,t))),X.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),X.call(e,e.media,\"progress playing seeking seeked\",(t=>Pe.updateProgress.call(e,t))),X.call(e,e.media,\"volumechange\",(t=>Pe.updateVolume.call(e,t))),X.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>Fe.checkPlaying.call(e,t))),X.call(e,e.media,\"waiting canplay seeked playing\",(t=>Fe.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=B.call(e,`.${e.config.classNames.video}`);if(!S.element(i))return;X.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{ie(e.play())}),\"play\")):this.proxy(s,(()=>{ie(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&X.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),X.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),X.call(e,e.media,\"ratechange\",(()=>{Pe.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),X.call(e,e.media,\"qualitychange\",(t=>{Pe.updateSetting.call(e,\"quality\",null,t.detail.quality)})),X.call(e,e.media,\"ready qualitychange\",(()=>{Pe.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");X.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),Z.call(e,t.container,i.type,!0,s)}))})),e(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let a=!0;S.function(n)&&(a=n.call(s,e)),!1!==a&&S.function(t)&&t.call(s,e)})),e(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:a}=this,l=a.config.listeners[s],r=S.function(l);X.call(a,e,t,(e=>this.proxy(e,i,s)),n&&!r)})),e(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=M.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{ie(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{Z.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),Pe.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),Pe.toggleMenu.call(e,t)):Pe.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&Pe.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(S.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),a=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&a?(i.removeAttribute(s),ie(e.play())):!a&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),M.isIos){const t=U.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>P(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");S.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>Pe.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),M.isWebKit&&Array.from(U.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>Pe.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!S.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,Pe.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;R(t.controls,i.classNames.noTransition,!0),Fe.toggleControls.call(e,!0),setTimeout((()=>{R(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>Fe.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),a=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(a/50);const{volume:l}=e.media;(1===a&&l<1||-1===a&&l>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=t,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:a,ctrlKey:l,metaKey:r,shiftKey:o}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(a||l||r||o)return;if(!s)return;if(c){const n=document.activeElement;if(S.element(n)){const{editable:s}=t.config.selectors,{seek:a}=i.inputs;if(n!==a&&V(n,s))return;if(\" \"===e.key&&V(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||ie(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){Pe.toggleMenu.call(this.player,e)}}\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self&&self;var Ue=function(e,t){return e(t={exports:{}},t.exports),t.exports}((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,a,l,r=[],o=e.length,c=o;for(n=function(e,i){i.length&&r.push(e),--c||t(r)};o--;)a=e[o],(l=i[a])?n(a,l):(s[a]=s[a]||[]).push(n)}function a(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function l(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function r(t,i,s,n){var a,l,o=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((l=o.createElement(\"link\")).rel=\"stylesheet\",l.href=m,(a=\"hideFocus\"in l)&&l.relList&&(a=0,l.rel=\"preload\",l.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(l=o.createElement(\"img\")).src=m:((l=o.createElement(\"script\")).src=t,l.async=void 0===c||c),l.onload=l.onerror=l.onbeforeload=function(e){var o=e.type[0];if(a)try{l.sheet.cssText.length||(o=\"e\")}catch(e){18!=e.code&&(o=\"e\")}if(\"e\"==o){if((n+=1)<u)return r(t,i,s,n)}else if(\"preload\"==l.rel&&\"style\"==l.as)return l.rel=\"stylesheet\";i(t,o,e.defaultPrevented)},!1!==h(t,l)&&o.head.appendChild(l)}function o(e,t,i){var s,n,a=(e=e.push?e:[e]).length,l=a,o=[];for(s=function(e,i,s){if(\"e\"==i&&o.push(e),\"b\"==i){if(!s)return;o.push(e)}--a||t(o)},n=0;n<l;n++)r(e[n],s,i)}function c(e,i,s){var n,r;if(i&&i.trim&&(n=i),r=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){o(e,(function(e){l(r,e),t&&l({success:t,error:i},e),a(n,e)}),r)}if(r.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){l(t,e)})),c},c.done=function(e){a(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function Be(e){return new Promise(((t,i)=>{Ue(e,{success:t,error:i})}))}function We(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,Z.call(this,this.media,e?\"play\":\"pause\"))}const ze={setup(){const e=this;R(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,ue.call(e),S.object(window.Vimeo)?ze.ready.call(e):Be(e.config.urls.vimeo.sdk).then((()=>{ze.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let a=e.media.getAttribute(\"src\"),l=\"\";S.empty(a)?(a=e.media.getAttribute(e.config.attributes.embed.id),l=e.media.getAttribute(e.config.attributes.embed.hash)):l=function(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}(a);const r=l?{h:l}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const o=Ne({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...r,...n}),c=(u=a,S.empty(u)?null:S.number(Number(u))?u:u.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:u);var u;const h=$(\"iframe\"),d=me(e.config.urls.vimeo.iframe,c,o);if(h.setAttribute(\"src\",d),h.setAttribute(\"allowfullscreen\",\"\"),h.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),S.empty(s)||h.setAttribute(\"referrerPolicy\",s),i||!t.customControls)h.setAttribute(\"data-poster\",e.poster),e.media=q(h,e.media);else{const t=$(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(h),e.media=q(t,e.media)}t.customControls||Te(me(e.config.urls.vimeo.api,d)).then((t=>{!S.empty(t)&&t.thumbnail_url&&Fe.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(h,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(We.call(e,!0),e.embed.play()),e.media.pause=()=>(We.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:m}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>m,set(t){const{embed:i,media:s,paused:n,volume:a}=e,l=n&&!i.hasPlayed;s.seeking=!0,Z.call(e,s,\"seeking\"),Promise.resolve(l&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>l&&i.pause())).then((()=>l&&i.setVolume(a))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,Z.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:g}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>g,set(t){e.embed.setVolume(t).then((()=>{g=t,Z.call(e,e.media,\"volumechange\")}))}});let{muted:f}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>f,set(t){const i=!!S.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{f=i,Z.call(e,e.media,\"volumechange\")}))}});let y,{loop:b}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>b,set(t){const i=S.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{b=i}))}}),e.embed.getVideoUrl().then((t=>{y=t,Pe.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>y}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=he(i,s),ue.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,Fe.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{m=t,Z.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,Z.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,xe.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>function(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}(e.text)));xe.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{We.call(e,!t),t||Z.call(e,e.media,\"playing\")})),S.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{Z.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{Z.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{We.call(e,!0),Z.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{We.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,m=t.seconds,Z.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,Z.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&Z.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,Z.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,Z.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,Z.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,Z.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>Fe.build.call(e)),0)}};function Ke(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,Z.call(this,this.media,e?\"play\":\"pause\"))}function Ye(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const Qe={setup(){if(R(this.elements.wrapper,this.config.classNames.embed,!0),S.object(window.YT)&&S.function(window.YT.Player))Qe.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{S.function(e)&&e(),Qe.ready.call(this)},Be(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){Te(me(this.config.urls.youtube.api,e)).then((e=>{if(S.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,Fe.setTitle.call(this),this.embed.ratio=he(s,i)}ue.call(this)})).catch((()=>{ue.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!S.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");S.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=(a=s,S.empty(a)?null:a.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:a);var a;const l=$(\"div\",{id:`${e.provider}-${Math.floor(1e4*Math.random())}`,\"data-poster\":t.customControls?e.poster:void 0});if(e.media=q(l,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;Re(t(\"maxres\"),121).catch((()=>Re(t(\"sd\"),121))).catch((()=>Re(t(\"hq\")))).then((t=>Fe.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:Ye(t),playerVars:x({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},Z.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),Z.call(e,e.media,\"ratechange\")},onReady(i){if(S.function(e.media.play))return;const s=i.target;Qe.getTitle.call(e,n),e.media.play=()=>{Ke.call(e,!0),s.playVideo()},e.media.pause=()=>{Ke.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,Z.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:a}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>a,set(t){a=t,s.setVolume(100*a),Z.call(e,e.media,\"volumechange\")}});let{muted:l}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>l,set(t){const i=S.boolean(t)?t:l;l=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*a),Z.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const r=s.getAvailablePlaybackRates();e.options.speed=r.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),Z.call(e,e.media,\"timeupdate\"),Z.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&Z.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),Z.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>Fe.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,Z.call(e,e.media,\"seeked\")),i.data){case-1:Z.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),Z.call(e,e.media,\"progress\");break;case 0:Ke.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):Z.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(Ke.call(e,!0),Z.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{Z.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),Z.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),Ke.call(e,!1);break;case 3:Z.call(e,e.media,\"waiting\")}Z.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},Xe={setup(){this.media?(R(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),R(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&R(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=$(\"div\",{class:this.config.classNames.video}),L(this.media,this.elements.wrapper),this.elements.poster=$(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?de.setup.call(this):this.isYouTube?Qe.setup.call(this):this.isVimeo&&ze.setup.call(this)):this.debug.warn(\"No media element found!\")}};class Je{constructor(t){e(this,\"load\",(()=>{this.enabled&&(S.object(window.google)&&S.object(window.google.ima)?this.ready():Be(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),e(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),e(this,\"setupIMA\",(()=>{this.elements.container=$(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),e(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),e(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=Ee(Math.max(this.manager.getRemainingTime(),0)),t=`${ve.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),e(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),e(this,\"addCuePoints\",(()=>{S.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(S.element(t)){const i=100/this.player.duration*e,s=$(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),e(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{Z.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),e(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),e(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;S.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),e(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),e(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,ie(this.player.media.play())})),e(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),e(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),e(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),e(this,\"trigger\",((e,...t)=>{const i=this.events[e];S.array(i)&&i.forEach((e=>{S.function(e)&&e.apply(this,t)}))})),e(this,\"on\",((e,t)=>(S.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),e(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),e(this,\"clearSafetyTimer\",(e=>{S.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=t,this.config=t.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!S.empty(e.publisherId)||S.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(S.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${Ne({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function Ge(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const Ze=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(S.number(i.startTime)){if(!S.empty(e.trim())&&S.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},et=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class tt{constructor(t){e(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),e(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(S.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(S.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(S.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),e(this,\"getThumbnail\",(e=>new Promise((t=>{Te(e).then((i=>{const s={frames:Ze(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),e(this,\"startMove\",(e=>{if(this.loaded&&S.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=Ee(this.seekTime);const a=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));a&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${a.label}<br>`)}this.showImageAtCurrentTime()}})),e(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),e(this,\"startScrubbing\",(e=>{(S.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),e(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):G.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),e(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),e(this,\"render\",(()=>{this.elements.thumb.container=$(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=$(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=$(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=$(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),S.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=$(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),e(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),e(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),e(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],a=i.frames[t].text,l=s+a;if(this.currentImageElement&&this.currentImageElement.dataset.filename===a)this.showImage(this.currentImageElement,n,e,t,a,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=l,i.dataset.index=t,i.dataset.filename=a,this.showingThumbFilename=a,this.player.debug.log(`Loading image: ${l}`),i.onload=()=>this.showImage(i,n,e,t,a,!0),this.loadingImage=i,this.removeOldImages(i)}})),e(this,\"showImage\",((e,t,i,s,n,a=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${a}`),this.setImageSizeAndOffset(e,t),a&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),e(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),e(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let a=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){a=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),a||i()}}),300)})))),e(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),e(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),e(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),e(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),e(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),e(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,a=this.mousePosX-e.left-i.clientWidth/2,l=Ge(a,s,n);i.style.left=`${l}px`,i.style.setProperty(\"--preview-arrow-offset\",a-l+\"px\")})),e(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=et(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),e(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=t,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=et(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const it={insertElements(e,t){S.string(t)?_(e,this.media,{src:t}):S.array(t)&&t.forEach((t=>{_(e,this.media,t)}))},change(e){N(e,\"sources.length\")?(de.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],O(this.media),this.media=null,S.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=_e.html5,src:n}]=t,a=\"html5\"===s?i:\"div\",l=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:K.check(i,s,this.config.playsinline),media:$(a,l)}),this.elements.container.appendChild(this.media),S.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),S.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),Fe.addStyleHook.call(this),this.isHTML5&&it.insertElements.call(this,\"source\",t),this.config.title=e.title,Xe.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&it.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Fe.build.call(this),this.isHTML5&&this.media.load(),S.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class st{constructor(t,i){if(e(this,\"play\",(()=>S.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>ie(this.media.play()))),this.media.play()):null)),e(this,\"pause\",(()=>this.playing&&S.function(this.media.pause)?this.media.pause():null)),e(this,\"togglePlay\",(e=>(S.boolean(e)?e:!this.playing)?this.play():this.pause())),e(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):S.function(this.media.stop)&&this.media.stop()})),e(this,\"restart\",(()=>{this.currentTime=0})),e(this,\"rewind\",(e=>{this.currentTime-=S.number(e)?e:this.config.seekTime})),e(this,\"forward\",(e=>{this.currentTime+=S.number(e)?e:this.config.seekTime})),e(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(S.number(e)?e:0)})),e(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),e(this,\"airplay\",(()=>{K.airplay&&this.media.webkitShowPlaybackTargetPicker()})),e(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=F(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=R(this.elements.container,this.config.classNames.hideControls,i);if(s&&S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!S.empty(this.config.settings)&&Pe.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";Z.call(this,this.media,e)}return!s}return!1})),e(this,\"on\",((e,t)=>{X.call(this,this.elements.container,e,t)})),e(this,\"once\",((e,t)=>{G.call(this,this.elements.container,e,t)})),e(this,\"off\",((e,t)=>{J(this.elements.container,e,t)})),e(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(O(this.elements.buttons.play),O(this.elements.captions),O(this.elements.controls),O(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),S.function(e)&&e()):(ee.call(this),de.cancelRequests.call(this),q(this.elements.original,this.elements.container),Z.call(this,this.elements.original,\"destroyed\",!0),S.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(Fe.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&S.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),e(this,\"supports\",(e=>K.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=K.touch,this.media=t,S.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||S.nodeList(this.media)||S.array(this.media))&&(this.media=this.media[0]),this.config=x({},Le,st.defaults,i||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new De(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",K),S.nullOrUndefined(this.media)||!S.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!K.check().api)return void this.debug.error(\"Setup failed: no support\");const s=this.media.cloneNode(!0);s.autoplay=!1,this.elements.original=s;const n=this.media.tagName.toLowerCase();let a=null,l=null;switch(n){case\"div\":if(a=this.media.querySelector(\"iframe\"),S.element(a)){if(l=Me(a.getAttribute(\"src\")),this.provider=function(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?_e.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?_e.vimeo:null}(l.toString()),this.elements.container=this.media,this.media=a,this.elements.container.className=\"\",l.search.length){const e=[\"1\",\"true\"];e.includes(l.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(l.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(l.searchParams.get(\"playsinline\")),this.config.youtube.hl=l.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(S.empty(this.provider)||!Object.values(_e).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=je;break;case\"video\":case\"audio\":this.type=n,this.provider=_e.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=K.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Ve(this),this.storage=new we(this),this.media.plyr=this,S.element(this.elements.container)||(this.elements.container=$(\"div\"),L(this.media,this.elements.container)),Fe.migrateStyles.call(this),Fe.addStyleHook.call(this),Xe.setup.call(this),this.config.debug&&X.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new He(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Fe.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Je(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>ie(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===_e.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===_e.youtube}get isVimeo(){return this.provider===_e.vimeo}get isVideo(){return this.type===je}get isAudio(){return this.type===Oe}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=S.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return S.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=S.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;S.string(t)&&(t=Number(t)),S.number(t)||(t=this.storage.get(\"volume\")),S.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!S.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;S.boolean(t)||(t=this.storage.get(\"muted\")),S.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;S.number(e)&&(t=e),S.number(t)||(t=this.storage.get(\"speed\")),S.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=Ge(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!S.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(S.number),n=!0;if(!i.includes(s)){const e=ne(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=S.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){it.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return S.url(e)?e:this.source}set download(e){S.url(e)&&(this.config.urls.download=e,Pe.setDownloadUrl.call(this))}set poster(e){this.isVideo?Fe.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=oe(ce.call(this));return S.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?S.string(e)&&re(e)?(this.config.ratio=oe(e),ue.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=S.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){xe.toggle.call(this,e,!1)}set currentTrack(e){xe.set.call(this,e,!1),xe.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){xe.setLanguage.call(this,e,!1)}get language(){return(xe.getCurrentTrack.call(this)||{}).language}set pip(e){if(!K.pip)return;const t=S.boolean(e)?e:!this.pip;S.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?Ie:$e),S.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return K.pip?S.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===Ie:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))}static supported(e,t){return K.check(e,t)}static loadSprite(e,t){return ke(e,t)}static setup(e,t={}){let i=null;return S.string(e)?i=Array.from(document.querySelectorAll(e)):S.nodeList(e)?i=Array.from(e):S.array(e)&&(i=e.filter(S.element)),S.empty(i)?null:i.map((e=>new st(e,t)))}}var nt;return st.defaults=(nt=Le,JSON.parse(JSON.stringify(nt))),st}));\n+\"object\"==typeof navigator&&function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(\"Plyr\",t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).Plyr=t()}(this,(function(){\"use strict\";function e(e,t,i){return(t=function(e){var t=function(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}(e,\"string\");return\"symbol\"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function t(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function i(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function s(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function n(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?s(Object(n),!0).forEach((function(t){i(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):s(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var a={addCSS:!0,thumbWidth:15,watch:!0};var l=function(e){return null!=e?e.constructor:null},r=function(e,t){return!!(e&&t&&e instanceof t)},o=function(e){return null==e},c=function(e){return l(e)===Object},u=function(e){return l(e)===String},h=function(e){return Array.isArray(e)},d=function(e){return r(e,NodeList)},m={nullOrUndefined:o,object:c,number:function(e){return l(e)===Number&&!Number.isNaN(e)},string:u,boolean:function(e){return l(e)===Boolean},function:function(e){return l(e)===Function},array:h,nodeList:d,element:function(e){return r(e,Element)},event:function(e){return r(e,Event)},empty:function(e){return o(e)||(u(e)||h(e)||d(e))&&!e.length||c(e)&&!Object.keys(e).length}};function p(e,t){if(1>t){var i=function(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var g=function(){function e(t,i){(function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")})(this,e),m.element(t)?this.element=t:m.string(t)&&(this.element=document.querySelector(t)),m.element(this.element)&&m.empty(this.element.rangeTouch)&&(this.config=n({},a,{},i),this.init())}return function(e,i,s){i&&t(e.prototype,i),s&&t(e,s)}(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!m.event(t))return null;var i,s=t.target,n=t.changedTouches[0],a=parseFloat(s.getAttribute(\"min\"))||0,l=parseFloat(s.getAttribute(\"max\"))||100,r=parseFloat(s.getAttribute(\"step\"))||1,o=s.getBoundingClientRect(),c=100/o.width*(this.config.thumbWidth/2)/100;return 0>(i=100/o.width*(n.clientX-o.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),a+p(i/100*(l-a),r)}},{key:\"set\",value:function(t){e.enabled&&m.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),function(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(m.empty(t)||m.string(t)?s=Array.from(document.querySelectorAll(m.string(t)?t:'input[type=\"range\"]')):m.element(t)?s=[t]:m.nodeList(t)?s=Array.from(t):m.array(t)&&(s=t.filter(m.element)),m.empty(s))return null;var l=n({},a,{},i);if(m.string(t)&&l.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){m.element(i)&&function(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}(i,t)&&new e(i,l)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const f=e=>null!=e?e.constructor:null,y=(e,t)=>Boolean(e&&t&&e instanceof t),b=e=>null==e,v=e=>f(e)===Object,w=e=>f(e)===String,T=e=>\"function\"==typeof e,k=e=>Array.isArray(e),C=e=>y(e,NodeList),A=e=>b(e)||(w(e)||k(e)||C(e))&&!e.length||v(e)&&!Object.keys(e).length;var S={nullOrUndefined:b,object:v,number:e=>f(e)===Number&&!Number.isNaN(e),string:w,boolean:e=>f(e)===Boolean,function:T,array:k,weakMap:e=>y(e,WeakMap),nodeList:C,element:e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,textNode:e=>f(e)===Text,event:e=>y(e,Event),keyboardEvent:e=>y(e,KeyboardEvent),cue:e=>y(e,window.TextTrackCue)||y(e,window.VTTCue),track:e=>y(e,TextTrack)||!b(e)&&w(e.kind),promise:e=>y(e,Promise)&&T(e.then),url:e=>{if(y(e,window.URL))return!0;if(!w(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!A(new URL(t).hostname)}catch(e){return!1}},empty:A};const E=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!S.string(i)&&t[i]})();function P(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}var M={isIE:Boolean(window.document.documentMode),isEdge:/Edge/g.test(navigator.userAgent),isWebKit:\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone:/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS:\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos:/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1};function N(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function x(e={},...t){if(!t.length)return e;const i=t.shift();return S.object(i)?(Object.keys(i).forEach((t=>{S.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),x(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),x(e,...t)):e}function L(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,a=e.nextSibling;s.appendChild(e),a?n.insertBefore(s,a):n.appendChild(s)}))}function I(e,t){S.element(e)&&!S.empty(t)&&Object.entries(t).filter((([,e])=>!S.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function $(e,t,i){const s=document.createElement(e);return S.object(t)&&I(s,t),S.string(i)&&(s.innerText=i),s}function _(e,t,i,s){S.element(t)&&t.appendChild($(e,i,s))}function O(e){S.nodeList(e)||S.array(e)?Array.from(e).forEach(O):S.element(e)&&S.element(e.parentNode)&&e.parentNode.removeChild(e)}function j(e){if(!S.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function q(e,t){return S.element(t)&&S.element(t.parentNode)&&S.element(e)?(t.parentNode.replaceChild(e,t),e):null}function D(e,t){if(!S.string(e)||S.empty(e))return{};const i={},s=x({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),a=t.replace(/[[\\]]/g,\"\").split(\"=\"),[l]=a,r=a.length>1?a[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":S.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[l]=r}})),x(s,i)}function H(e,t){if(!S.element(e))return;let i=t;S.boolean(i)||(i=!e.hidden),e.hidden=i}function R(e,t,i){if(S.nodeList(e))return Array.from(e).map((e=>R(e,t,i)));if(S.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function F(e,t){return S.element(e)&&e.classList.contains(t)}function V(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function U(e){return this.elements.container.querySelectorAll(e)}function B(e){return this.elements.container.querySelector(e)}function W(e=null,t=!1){S.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const z={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},K={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=K[e]||\"html5\"!==t;return{api:i,ui:i&&K.rangeInput}},pip:!(M.isIPhone||!S.function($(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||$(\"video\").disablePictureInPicture)),airplay:S.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(S.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(z).includes(i)&&(i+=`; codecs=\"${z[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==E,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},Y=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function Q(e,t,i,s=!1,n=!0,a=!1){if(!e||!(\"addEventListener\"in e)||S.empty(t)||!S.function(i))return;const l=t.split(\" \");let r=a;Y&&(r={passive:n,capture:a}),l.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:r}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,r)}))}function X(e,t=\"\",i,s=!0,n=!1){Q.call(this,e,t,i,!0,s,n)}function J(e,t=\"\",i,s=!0,n=!1){Q.call(this,e,t,i,!1,s,n)}function G(e,t=\"\",i,s=!0,n=!1){const a=(...l)=>{J(e,t,a,s,n),i.apply(this,l)};Q.call(this,e,t,a,!0,s,n)}function Z(e,t=\"\",i=!1,s={}){if(!S.element(e)||S.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function ee(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function te(){return new Promise((e=>this.ready?setTimeout(e,0):X.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function ie(e){S.promise(e)&&e.then(null,(()=>{}))}function se(e){return S.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function ne(e,t){return S.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function ae(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const le=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function re(e){if(!(S.array(e)||S.string(e)&&e.includes(\":\")))return!1;return(S.array(e)?e:e.split(\":\")).map(Number).every(S.number)}function oe(e){if(!S.array(e)||!e.every(S.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function ce(e){const t=e=>re(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!S.empty(this.embed)&&S.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return oe(i)}function ue(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=ce.call(this,e);if(!S.array(i))return{};const[s,n]=oe(i),a=100/s*n;if(ae(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${a}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-a)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:a,ratio:i}}function he(e,t,i=.05){const s=e/t,n=ne(Object.keys(le),s);return Math.abs(n-s)<=i?le[n]:[e,t]}const de={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!S.empty(t)||K.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:de.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,S.empty(this.config.ratio)||ue.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=de.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&S.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=de.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:a,readyState:l,playbackRate:r}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==a||l)&&(e.once(\"loadedmetadata\",(()=>{e.speed=r,e.currentTime=s,n||ie(e.play())})),e.media.load())}Z.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(O(de.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function me(e,...t){return S.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}const pe=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),ge=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function fe(e=\"\"){let t=e.toString();return t=function(e=\"\"){let t=e.toString();return t=pe(t,\"-\",\" \"),t=pe(t,\"_\",\" \"),t=ge(t),pe(t,\" \",\"\")}(t),t.charAt(0).toLowerCase()+t.slice(1)}function ye(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const be={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},ve={get(e=\"\",t={}){if(S.empty(e)||S.empty(t))return\"\";let i=N(t.i18n,e);if(S.empty(i))return Object.keys(be).includes(e)?be[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=pe(i,e,t)})),i}};class we{constructor(t){e(this,\"get\",(e=>{if(!we.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(S.empty(t))return null;const i=JSON.parse(t);return S.string(e)&&e.length?i[e]:i})),e(this,\"set\",(e=>{if(!we.supported||!this.enabled)return;if(!S.object(e))return;let t=this.get();S.empty(t)&&(t={}),x(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=t.config.storage.enabled,this.key=t.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function Te(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function ke(e,t){if(!S.string(e))return;const i=\"cache\",s=S.string(t);let n=!1;const a=()=>null!==document.getElementById(t),l=(e,t)=>{e.innerHTML=t,s&&a()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!a()){const a=we.supported,r=document.createElement(\"div\");if(r.setAttribute(\"hidden\",\"\"),s&&r.setAttribute(\"id\",t),a){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);l(r,t.content)}}Te(e).then((e=>{if(!S.empty(e)){if(a)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}l(r,e)}})).catch((()=>{}))}}const Ce=e=>Math.trunc(e/60/60%60,10),Ae=e=>Math.trunc(e/60%60,10),Se=e=>Math.trunc(e%60,10);function Ee(e=0,t=!1,i=!1){if(!S.number(e))return Ee(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=Ce(e);const a=Ae(e),l=Se(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(a)}:${s(l)}`}const Pe={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||M.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=B.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:U.call(this,this.config.selectors.buttons.play),pause:B.call(this,this.config.selectors.buttons.pause),restart:B.call(this,this.config.selectors.buttons.restart),rewind:B.call(this,this.config.selectors.buttons.rewind),fastForward:B.call(this,this.config.selectors.buttons.fastForward),mute:B.call(this,this.config.selectors.buttons.mute),pip:B.call(this,this.config.selectors.buttons.pip),airplay:B.call(this,this.config.selectors.buttons.airplay),settings:B.call(this,this.config.selectors.buttons.settings),captions:B.call(this,this.config.selectors.buttons.captions),fullscreen:B.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=B.call(this,this.config.selectors.progress),this.elements.inputs={seek:B.call(this,this.config.selectors.inputs.seek),volume:B.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:B.call(this,this.config.selectors.display.buffer),currentTime:B.call(this,this.config.selectors.display.currentTime),duration:B.call(this,this.config.selectors.display.duration)},S.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=Pe.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,a=document.createElementNS(i,\"svg\");I(a,x(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const l=document.createElementNS(i,\"use\"),r=`${n}-${e}`;return\"href\"in l&&l.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",r),l.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",r),a.appendChild(l),a},createLabel(e,t={}){const i=ve.get(e,this.config);return $(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(S.empty(e))return null;const t=$(\"span\",{class:this.config.classNames.menu.value});return t.appendChild($(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=x({},t);let s=fe(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||x(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:S.empty(n.label)&&(n.label=s),S.empty(n.icon)&&(n.icon=e)}const a=$(n.element);return n.toggle?(a.appendChild(Pe.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),a.appendChild(Pe.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),a.appendChild(Pe.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),a.appendChild(Pe.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(a.appendChild(Pe.createIcon.call(this,n.icon)),a.appendChild(Pe.createLabel.call(this,n.label))),x(i,D(this.config.selectors.buttons[s],i)),I(a,i),\"play\"===s?(S.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(a)):this.elements.buttons[s]=a,a},createRange(e,t){const i=$(\"input\",x(D(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":ve.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,Pe.updateRangeFill.call(this,i),g.setup(i),i},createProgress(e,t){const i=$(\"progress\",x(D(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild($(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?ve.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=D(this.config.selectors.display[e],t),s=$(\"div\",x(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":ve.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){X.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=V(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))Pe.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,S.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,S.element(t)||(t=e.parentNode.lastElementChild)),W.call(this,t,!0))}}),!1),X.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&Pe.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:a=!1}){const l=D(this.config.selectors.inputs[i]),r=$(\"button\",x(l,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${l.class?l.class:\"\"}`.trim(),\"aria-checked\":a,value:e})),o=$(\"span\");o.innerHTML=s,S.element(n)&&o.appendChild(n),r.appendChild(o),Object.defineProperty(r,\"checked\",{enumerable:!0,get:()=>\"true\"===r.getAttribute(\"aria-checked\"),set(e){e&&Array.from(r.parentNode.children).filter((e=>V(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),r.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(r,\"click keyup\",(t=>{if(!S.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),r.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}Pe.showMenuPanel.call(this,\"home\",S.keyboardEvent(t))}}),i,!1),Pe.bindMenuItemShortcuts.call(this,r,i),t.appendChild(r)},formatTime(e=0,t=!1){if(!S.number(e))return e;return Ee(e,Ce(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){S.element(e)&&S.number(t)&&(e.innerText=Pe.formatTime(t,i))},updateVolume(){this.supported.ui&&(S.element(this.elements.inputs.volume)&&Pe.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),S.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){S.element(e)&&(e.value=t,Pe.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!S.event(e))return;let t=0;const i=(e,t)=>{const i=S.number(t)?t:0,s=S.element(e)?e:this.elements.display.buffer;if(S.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];S.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":s=this.currentTime,n=this.duration,t=0===s||0===n||Number.isNaN(s)||Number.isNaN(n)?0:(s/n*100).toFixed(2),\"timeupdate\"===e.type&&Pe.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}var s,n},updateRangeFill(e){const t=S.event(e)?e.target:e;if(S.element(t)&&\"range\"===t.getAttribute(\"type\")){if(V(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=Pe.formatTime(this.currentTime),i=Pe.formatTime(this.duration),s=ve.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(V(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(M.isWebKit||M.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!S.element(this.elements.inputs.seek)||!S.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,a=e=>R(s,n,e);if(this.touch)return void a(!1);let l=0;const r=this.elements.progress.getBoundingClientRect();if(S.event(e))l=100/r.width*(e.pageX-r.left);else{if(!F(s,n))return;l=parseFloat(s.style.left,10)}l<0?l=0:l>100&&(l=100);const o=this.duration/100*l;s.innerText=Pe.formatTime(o);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(o)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${l}%`,S.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&a(\"mouseenter\"===e.type)},timeUpdate(e){const t=!S.element(this.elements.display.duration)&&this.config.invertTime;Pe.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||Pe.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return H(this.elements.display.currentTime,!0),void H(this.elements.progress,!0);S.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=S.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&Pe.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&Pe.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&Pe.setMarkers.call(this),Pe.updateSeekTooltip.call(this)},toggleMenuButton(e,t){H(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,a=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=S.empty(i)?this[e]:i,S.empty(n)&&(n=this.config[e].default),!S.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(S.element(a)||(a=s&&s.querySelector('[role=\"menu\"]')),!S.element(a))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=Pe.getLabel.call(this,e,n);const l=a&&a.querySelector(`[value=\"${n}\"]`);S.element(l)&&(l.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?ve.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(S.number(t)){const e=ve.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return ge(t);case\"captions\":return xe.getLabel.call(this);default:return null}},setQualityMenu(e){if(!S.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');S.array(e)&&(this.options.quality=se(e).filter((e=>this.config.quality.options.includes(e))));const s=!S.empty(this.options.quality)&&this.options.quality.length>1;if(Pe.toggleMenuButton.call(this,t,s),j(i),Pe.checkMenu.call(this),!s)return;const n=e=>{const t=ve.get(`qualityBadge.${e}`,this.config);return t.length?Pe.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{Pe.createMenuItem.call(this,{value:e,list:i,type:t,title:Pe.getLabel.call(this,\"quality\",e),badge:n(e)})})),Pe.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!S.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=xe.getTracks.call(this),s=Boolean(i.length);if(Pe.toggleMenuButton.call(this,e,s),j(t),Pe.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:xe.getLabel.call(this,e),badge:e.language&&Pe.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:ve.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(Pe.createMenuItem.bind(this)),Pe.updateSetting.call(this,e,t)},setSpeedMenu(){if(!S.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!S.empty(this.options.speed)&&this.options.speed.length>1;Pe.toggleMenuButton.call(this,e,i),j(t),Pe.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{Pe.createMenuItem.call(this,{value:i,list:t,type:e,title:Pe.getLabel.call(this,\"speed\",i)})})),Pe.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!S.empty(e)&&Object.values(e).some((e=>!e.hidden));H(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;S.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');W.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!S.element(t)||!S.element(i))return;const{hidden:s}=t;let n=s;if(S.boolean(e))n=e;else if(S.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(S.event(e)){const s=S.function(e.composedPath)?e.composedPath()[0]:e.target,a=t.contains(s);if(a||!a&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),H(t,!n),R(this.elements.container,this.config.classNames.menu.open,n),n&&S.keyboardEvent(e)?Pe.focusFirstMenuItem.call(this,null,!0):n||s||W.call(this,i,S.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return O(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!S.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(K.transitions&&!K.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=Pe.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",J.call(this,s,E,t))};X.call(this,s,E,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}H(n,!0),H(i,!1),Pe.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;S.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:a,setQualityMenu:l,setSpeedMenu:r,showMenuPanel:o}=Pe;this.elements.controls=null,S.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=$(\"div\",D(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return se(S.array(this.config.controls)?this.config.controls:[]).forEach((l=>{if(\"restart\"===l&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===l&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===l&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===l&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===l){const t=$(\"div\",{class:`${u.class} plyr__progress__container`}),i=$(\"div\",D(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=$(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===l&&c.appendChild(a.call(this,\"currentTime\",u)),\"duration\"===l&&c.appendChild(a.call(this,\"duration\",u)),\"mute\"===l||\"volume\"===l){let{volume:t}=this.elements;if(S.element(t)&&c.contains(t)||(t=$(\"div\",x({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===l&&t.appendChild(i.call(this,\"mute\")),\"volume\"===l&&!M.isIos&&!M.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",x(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===l&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===l&&!S.empty(this.config.settings)){const s=$(\"div\",x({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=$(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),a=$(\"div\"),l=$(\"div\",{id:`plyr-settings-${e.id}-home`}),r=$(\"div\",{role:\"menu\"});l.appendChild(r),a.appendChild(l),this.elements.settings.panels.home=l,this.config.settings.forEach((i=>{const s=$(\"button\",x(D(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),X.call(this,s,\"click\",(()=>{o.call(this,i,!1)}));const n=$(\"span\",null,ve.get(i,this.config)),l=$(\"span\",{class:this.config.classNames.menu.value});l.innerHTML=e[i],n.appendChild(l),s.appendChild(n),r.appendChild(s);const c=$(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=$(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild($(\"span\",{\"aria-hidden\":!0},ve.get(i,this.config))),u.appendChild($(\"span\",{class:this.config.classNames.hidden},ve.get(\"menuBack\",this.config))),X.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),o.call(this,\"home\",!0))}),!1),X.call(this,u,\"click\",(()=>{o.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild($(\"div\",{role:\"menu\"})),a.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(a),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===l&&K.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===l&&K.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===l){const e=x({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!S.url(t)&&this.isEmbed&&x(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===l&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&l.call(this,de.getQualityOptions.call(this)),r.call(this),c},inject(){if(this.config.loadSprite){const e=Pe.getIconUrl.call(this);e.cors&&ke(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;S.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),S.element(this.config.controls)||S.string(this.config.controls)?e=this.config.controls:(e=Pe.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:xe.getLabel.call(this)}),i=!1);let s;i&&S.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=pe(i,`{${e}}`,t)})),i})(e)),S.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),S.element(s)||(s=this.elements.container);if(s[S.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),S.element(this.elements.controls)||Pe.findElements.call(this),!S.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>F(e,t),set(i=!1){R(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{S.array(t)||S.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(M.isEdge&&P(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=U.call(this,i);Array.from(s).forEach((e=>{R(e,this.config.classNames.hidden,!1),R(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let a=null;const l=`${this.config.classNames.tooltip}--visible`,r=e=>R(a,l,e);i.forEach((e=>{const t=$(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";a&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(a.style.left=i,a.innerHTML=e.label,r(!0))})),t.addEventListener(\"mouseleave\",(()=>{r(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(a=$(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(a)),this.elements.markers={points:n,tip:a},this.elements.progress.appendChild(s)}};function Me(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function Ne(e){const t=new URLSearchParams;return S.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const xe={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!K.textTracks)return void(S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Pe.setCaptionsMenu.call(this));var e,t;if(S.element(this.elements.captions)||(this.elements.captions=$(\"div\",D(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),e=this.elements.captions,t=this.elements.wrapper,S.element(e)&&S.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)),M.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=Me(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&Te(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{O(e)}))}))}const i=se((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let s=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===s&&([s]=i);let n=this.storage.get(\"captions\");if(S.boolean(n)||({active:n}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:n,language:s,languages:i}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";X.call(this,this.media.textTracks,e,xe.update.bind(this))}setTimeout(xe.update.bind(this),0)},update(){const e=xe.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,a=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),X.call(this,e,\"cuechange\",(()=>xe.updateCues.call(this)))})),(a&&this.language!==i||!e.includes(n))&&(xe.setLanguage.call(this,i),xe.toggle.call(this,t&&a)),this.elements&&R(this.elements.container,this.config.classNames.captions.enabled,!S.empty(e)),S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Pe.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=S.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=xe.getTracks.call(this),t=xe.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void xe.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),R(this.elements.container,s,n),this.captions.toggled=n,Pe.updateSetting.call(this,\"captions\"),Z.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=xe.getTracks.call(this);if(-1!==e)if(S.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,Pe.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),Z.call(this,this.media,\"languagechange\")}xe.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&xe.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else xe.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!S.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=xe.getTracks.call(this),n=xe.findTrack.call(this,[i]);xe.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=xe.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let a;return e.every((e=>(a=n.find((t=>t.language===e)),!a))),a||(t?n[0]:void 0)},getCurrentTrack(){return xe.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!S.track(t)&&K.textTracks&&this.captions.toggled&&(t=xe.getCurrentTrack.call(this)),S.track(t)?S.empty(t.label)?S.empty(t.language)?ve.get(\"enabled\",this.config):e.language.toUpperCase():t.label:ve.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!S.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!S.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=xe.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(ye)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){j(this.elements.captions);const e=$(\"span\",D(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),Z.call(this,this.media,\"cuechange\")}}},Le={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:null,listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},Ie=\"picture-in-picture\",$e=\"inline\",_e={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},Oe=\"audio\",je=\"video\";const qe=()=>{};class De{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):qe}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):qe}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):qe}}class He{constructor(t){e(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;S.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;Z.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),e(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",R(this.target,this.player.config.classNames.fullscreen.fallback,e),M.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=S.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),e(this,\"trapFocus\",(e=>{if(M.isIos||M.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=U.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),e(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":He.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");R(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),e(this,\"enter\",(()=>{this.supported&&(M.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!He.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?S.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),e(this,\"exit\",(()=>{if(this.supported)if(M.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),ie(this.player.play());else if(!He.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!S.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),e(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=t,this.prefix=He.prefix,this.property=He.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===t.config.fullscreen.fallback,this.player.elements.fullscreen=t.config.fullscreen.container&&function(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(V.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}(this.player.elements.container,t.config.fullscreen.container),X.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),X.call(this.player,this.player.elements.container,\"dblclick\",(e=>{S.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),X.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return He.nativeSupported&&!this.forceFallback}static get prefix(){if(S.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!S.function(document[`${t}ExitFullscreen`])&&!S.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,He.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||He.nativeSupported||!M.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!He.nativeSupported||this.forceFallback)return F(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return M.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function Re(e,t=1){return new Promise(((i,s)=>{const n=new Image,a=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:a,onerror:a,src:e})}))}const Fe={addStyleHook(){R(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),R(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void Fe.toggleNativeControls.call(this,!0);S.element(this.elements.controls)||(Pe.inject.call(this),this.listeners.controls()),Fe.toggleNativeControls.call(this),this.isHTML5&&xe.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,Pe.updateVolume.call(this),Pe.timeUpdate.call(this),Pe.durationUpdate.call(this),Fe.checkPlaying.call(this),R(this.elements.container,this.config.classNames.pip.supported,K.pip&&this.isHTML5&&this.isVideo),R(this.elements.container,this.config.classNames.airplay.supported,K.airplay&&this.isHTML5),R(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{Z.call(this,this.media,\"ready\")}),0),Fe.setTitle.call(this),this.poster&&Fe.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&Pe.durationUpdate.call(this),this.config.mediaMetadata&&Pe.setMediaMetadata.call(this)},setTitle(){let e=ve.get(\"play\",this.config);if(S.string(this.config.title)&&!S.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=B.call(this,\"iframe\");if(!S.element(e))return;const t=S.empty(this.config.title)?\"video\":this.config.title,i=ve.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){R(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),te.call(this).then((()=>Re(e))).catch((t=>{throw e===this.poster&&Fe.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),Fe.togglePoster.call(this,!0),e))))},checkPlaying(e){R(this.elements.container,this.config.classNames.playing,this.playing),R(this.elements.container,this.config.classNames.paused,this.paused),R(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",ve.get(this.playing?\"pause\":\"play\",this.config))})),S.event(e)&&\"timeupdate\"===e.type||Fe.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{R(this.elements.container,this.config.classNames.loading,this.loading),Fe.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!S.empty(e)&&S.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),S.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Ve{constructor(t){e(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,R(t.container,e.config.classNames.isTouch,!0)})),e(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&Q.call(t,window,\"keydown keyup\",this.handleKey,e,!1),Q.call(t,document.body,\"click\",this.toggleMenu,e),G.call(t,document.body,\"touchstart\",this.firstTouch)})),e(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&X.call(e,i.container,\"keydown keyup\",this.handleKey,!1),X.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let a=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(Fe.toggleControls.call(e,!0),a=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>Fe.toggleControls.call(e,!1)),a)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,a]=ce.call(e),l=ae(`aspect-ratio: ${n} / ${a}`);if(!s)return void(l?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[r,o]=[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)],c=r/o>n/a;l?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?o/a*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},a=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};X.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&S.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?X:J).call(e,window,\"resize\",a)}))})),e(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(X.call(e,e.media,\"timeupdate seeking seeked\",(t=>Pe.timeUpdate.call(e,t))),X.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>Pe.durationUpdate.call(e,t))),X.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),X.call(e,e.media,\"progress playing seeking seeked\",(t=>Pe.updateProgress.call(e,t))),X.call(e,e.media,\"volumechange\",(t=>Pe.updateVolume.call(e,t))),X.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>Fe.checkPlaying.call(e,t))),X.call(e,e.media,\"waiting canplay seeked playing\",(t=>Fe.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=B.call(e,`.${e.config.classNames.video}`);if(!S.element(i))return;X.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{ie(e.play())}),\"play\")):this.proxy(s,(()=>{ie(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&X.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),X.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),X.call(e,e.media,\"ratechange\",(()=>{Pe.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),X.call(e,e.media,\"qualitychange\",(t=>{Pe.updateSetting.call(e,\"quality\",null,t.detail.quality)})),X.call(e,e.media,\"ready qualitychange\",(()=>{Pe.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");X.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),Z.call(e,t.container,i.type,!0,s)}))})),e(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let a=!0;S.function(n)&&(a=n.call(s,e)),!1!==a&&S.function(t)&&t.call(s,e)})),e(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:a}=this,l=a.config.listeners[s],r=S.function(l);X.call(a,e,t,(e=>this.proxy(e,i,s)),n&&!r)})),e(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=M.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{ie(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{Z.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),Pe.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),Pe.toggleMenu.call(e,t)):Pe.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&Pe.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(S.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),a=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&a?(i.removeAttribute(s),ie(e.play())):!a&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),M.isIos){const t=U.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>P(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");S.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>Pe.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),M.isWebKit&&Array.from(U.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>Pe.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!S.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,Pe.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;R(t.controls,i.classNames.noTransition,!0),Fe.toggleControls.call(e,!0),setTimeout((()=>{R(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>Fe.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),a=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(a/50);const{volume:l}=e.media;(1===a&&l<1||-1===a&&l>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=t,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:a,ctrlKey:l,metaKey:r,shiftKey:o}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(a||l||r||o)return;if(!s)return;if(c){const n=document.activeElement;if(S.element(n)){const{editable:s}=t.config.selectors,{seek:a}=i.inputs;if(n!==a&&V(n,s))return;if(\" \"===e.key&&V(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||ie(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){Pe.toggleMenu.call(this.player,e)}}\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self&&self;var Ue=function(e,t){return e(t={exports:{}},t.exports),t.exports}((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,a,l,r=[],o=e.length,c=o;for(n=function(e,i){i.length&&r.push(e),--c||t(r)};o--;)a=e[o],(l=i[a])?n(a,l):(s[a]=s[a]||[]).push(n)}function a(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function l(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function r(t,i,s,n){var a,l,o=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((l=o.createElement(\"link\")).rel=\"stylesheet\",l.href=m,(a=\"hideFocus\"in l)&&l.relList&&(a=0,l.rel=\"preload\",l.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(l=o.createElement(\"img\")).src=m:((l=o.createElement(\"script\")).src=t,l.async=void 0===c||c),l.onload=l.onerror=l.onbeforeload=function(e){var o=e.type[0];if(a)try{l.sheet.cssText.length||(o=\"e\")}catch(e){18!=e.code&&(o=\"e\")}if(\"e\"==o){if((n+=1)<u)return r(t,i,s,n)}else if(\"preload\"==l.rel&&\"style\"==l.as)return l.rel=\"stylesheet\";i(t,o,e.defaultPrevented)},!1!==h(t,l)&&o.head.appendChild(l)}function o(e,t,i){var s,n,a=(e=e.push?e:[e]).length,l=a,o=[];for(s=function(e,i,s){if(\"e\"==i&&o.push(e),\"b\"==i){if(!s)return;o.push(e)}--a||t(o)},n=0;n<l;n++)r(e[n],s,i)}function c(e,i,s){var n,r;if(i&&i.trim&&(n=i),r=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){o(e,(function(e){l(r,e),t&&l({success:t,error:i},e),a(n,e)}),r)}if(r.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){l(t,e)})),c},c.done=function(e){a(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function Be(e){return new Promise(((t,i)=>{Ue(e,{success:t,error:i})}))}function We(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,Z.call(this,this.media,e?\"play\":\"pause\"))}const ze={setup(){const e=this;R(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,ue.call(e),S.object(window.Vimeo)?ze.ready.call(e):Be(e.config.urls.vimeo.sdk).then((()=>{ze.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let a=e.media.getAttribute(\"src\"),l=\"\";S.empty(a)?(a=e.media.getAttribute(e.config.attributes.embed.id),l=e.media.getAttribute(e.config.attributes.embed.hash)):l=function(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}(a);const r=l?{h:l}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const o=Ne({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...r,...n}),c=(u=a,S.empty(u)?null:S.number(Number(u))?u:u.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:u);var u;const h=$(\"iframe\"),d=me(e.config.urls.vimeo.iframe,c,o);if(h.setAttribute(\"src\",d),h.setAttribute(\"allowfullscreen\",\"\"),h.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),S.empty(s)||h.setAttribute(\"referrerPolicy\",s),i||!t.customControls)h.setAttribute(\"data-poster\",e.poster),e.media=q(h,e.media);else{const t=$(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(h),e.media=q(t,e.media)}t.customControls||Te(me(e.config.urls.vimeo.api,d)).then((t=>{!S.empty(t)&&t.thumbnail_url&&Fe.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(h,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(We.call(e,!0),e.embed.play()),e.media.pause=()=>(We.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:m}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>m,set(t){const{embed:i,media:s,paused:n,volume:a}=e,l=n&&!i.hasPlayed;s.seeking=!0,Z.call(e,s,\"seeking\"),Promise.resolve(l&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>l&&i.pause())).then((()=>l&&i.setVolume(a))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,Z.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:g}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>g,set(t){e.embed.setVolume(t).then((()=>{g=t,Z.call(e,e.media,\"volumechange\")}))}});let{muted:f}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>f,set(t){const i=!!S.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{f=i,Z.call(e,e.media,\"volumechange\")}))}});let y,{loop:b}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>b,set(t){const i=S.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{b=i}))}}),e.embed.getVideoUrl().then((t=>{y=t,Pe.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>y}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=he(i,s),ue.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,Fe.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{m=t,Z.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,Z.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,xe.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>function(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}(e.text)));xe.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{We.call(e,!t),t||Z.call(e,e.media,\"playing\")})),S.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{Z.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{Z.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{We.call(e,!0),Z.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{We.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,m=t.seconds,Z.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,Z.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&Z.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,Z.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,Z.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,Z.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,Z.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>Fe.build.call(e)),0)}};function Ke(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,Z.call(this,this.media,e?\"play\":\"pause\"))}function Ye(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const Qe={setup(){if(R(this.elements.wrapper,this.config.classNames.embed,!0),S.object(window.YT)&&S.function(window.YT.Player))Qe.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{S.function(e)&&e(),Qe.ready.call(this)},Be(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){Te(me(this.config.urls.youtube.api,e)).then((e=>{if(S.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,Fe.setTitle.call(this),this.embed.ratio=he(s,i)}ue.call(this)})).catch((()=>{ue.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!S.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");S.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=(a=s,S.empty(a)?null:a.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:a);var a;const l=$(\"div\",{id:`${e.provider}-${Math.floor(1e4*Math.random())}`,\"data-poster\":t.customControls?e.poster:void 0});if(e.media=q(l,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;Re(t(\"maxres\"),121).catch((()=>Re(t(\"sd\"),121))).catch((()=>Re(t(\"hq\")))).then((t=>Fe.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:Ye(t),playerVars:x({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},Z.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),Z.call(e,e.media,\"ratechange\")},onReady(i){if(S.function(e.media.play))return;const s=i.target;Qe.getTitle.call(e,n),e.media.play=()=>{Ke.call(e,!0),s.playVideo()},e.media.pause=()=>{Ke.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,Z.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:a}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>a,set(t){a=t,s.setVolume(100*a),Z.call(e,e.media,\"volumechange\")}});let{muted:l}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>l,set(t){const i=S.boolean(t)?t:l;l=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*a),Z.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const r=s.getAvailablePlaybackRates();e.options.speed=r.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),Z.call(e,e.media,\"timeupdate\"),Z.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&Z.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),Z.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>Fe.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,Z.call(e,e.media,\"seeked\")),i.data){case-1:Z.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),Z.call(e,e.media,\"progress\");break;case 0:Ke.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):Z.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(Ke.call(e,!0),Z.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{Z.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),Z.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),Ke.call(e,!1);break;case 3:Z.call(e,e.media,\"waiting\")}Z.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},Xe={setup(){this.media?(R(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),R(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&R(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=$(\"div\",{class:this.config.classNames.video}),L(this.media,this.elements.wrapper),this.elements.poster=$(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?de.setup.call(this):this.isYouTube?Qe.setup.call(this):this.isVimeo&&ze.setup.call(this)):this.debug.warn(\"No media element found!\")}};class Je{constructor(t){e(this,\"load\",(()=>{this.enabled&&(S.object(window.google)&&S.object(window.google.ima)?this.ready():Be(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),e(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),e(this,\"setupIMA\",(()=>{this.elements.container=$(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),e(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),e(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=Ee(Math.max(this.manager.getRemainingTime(),0)),t=`${ve.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),e(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),e(this,\"addCuePoints\",(()=>{S.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(S.element(t)){const i=100/this.player.duration*e,s=$(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),e(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{Z.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),e(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),e(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;S.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),e(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),e(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,ie(this.player.media.play())})),e(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),e(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),e(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),e(this,\"trigger\",((e,...t)=>{const i=this.events[e];S.array(i)&&i.forEach((e=>{S.function(e)&&e.apply(this,t)}))})),e(this,\"on\",((e,t)=>(S.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),e(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),e(this,\"clearSafetyTimer\",(e=>{S.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=t,this.config=t.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!S.empty(e.publisherId)||S.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(S.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${Ne({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function Ge(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const Ze=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(S.number(i.startTime)){if(!S.empty(e.trim())&&S.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},et=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class tt{constructor(t){e(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),e(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(S.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(S.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(S.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),e(this,\"getThumbnail\",(e=>new Promise((t=>{Te(e).then((i=>{const s={frames:Ze(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),e(this,\"startMove\",(e=>{if(this.loaded&&S.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=Ee(this.seekTime);const a=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));a&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${a.label}<br>`)}this.showImageAtCurrentTime()}})),e(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),e(this,\"startScrubbing\",(e=>{(S.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),e(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):G.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),e(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),e(this,\"render\",(()=>{this.elements.thumb.container=$(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=$(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=$(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=$(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),S.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=$(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),e(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),e(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),e(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],a=i.frames[t].text,l=s+a;if(this.currentImageElement&&this.currentImageElement.dataset.filename===a)this.showImage(this.currentImageElement,n,e,t,a,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=l,i.dataset.index=t,i.dataset.filename=a,this.showingThumbFilename=a,this.player.debug.log(`Loading image: ${l}`),i.onload=()=>this.showImage(i,n,e,t,a,!0),this.loadingImage=i,this.removeOldImages(i)}})),e(this,\"showImage\",((e,t,i,s,n,a=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${a}`),this.setImageSizeAndOffset(e,t),a&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),e(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),e(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let a=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){a=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),a||i()}}),300)})))),e(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),e(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),e(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),e(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),e(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),e(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,a=this.mousePosX-e.left-i.clientWidth/2,l=Ge(a,s,n);i.style.left=`${l}px`,i.style.setProperty(\"--preview-arrow-offset\",a-l+\"px\")})),e(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=et(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),e(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=t,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=et(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const it={insertElements(e,t){S.string(t)?_(e,this.media,{src:t}):S.array(t)&&t.forEach((t=>{_(e,this.media,t)}))},change(e){N(e,\"sources.length\")?(de.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],O(this.media),this.media=null,S.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=_e.html5,src:n}]=t,a=\"html5\"===s?i:\"div\",l=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:K.check(i,s,this.config.playsinline),media:$(a,l)}),this.elements.container.appendChild(this.media),S.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),S.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),Fe.addStyleHook.call(this),this.isHTML5&&it.insertElements.call(this,\"source\",t),this.config.title=e.title,Xe.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&it.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Fe.build.call(this),this.isHTML5&&this.media.load(),S.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class st{constructor(t,i){if(e(this,\"play\",(()=>S.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>ie(this.media.play()))),this.media.play()):null)),e(this,\"pause\",(()=>this.playing&&S.function(this.media.pause)?this.media.pause():null)),e(this,\"togglePlay\",(e=>(S.boolean(e)?e:!this.playing)?this.play():this.pause())),e(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):S.function(this.media.stop)&&this.media.stop()})),e(this,\"restart\",(()=>{this.currentTime=0})),e(this,\"rewind\",(e=>{this.currentTime-=S.number(e)?e:this.config.seekTime})),e(this,\"forward\",(e=>{this.currentTime+=S.number(e)?e:this.config.seekTime})),e(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(S.number(e)?e:0)})),e(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),e(this,\"airplay\",(()=>{K.airplay&&this.media.webkitShowPlaybackTargetPicker()})),e(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=F(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=R(this.elements.container,this.config.classNames.hideControls,i);if(s&&S.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!S.empty(this.config.settings)&&Pe.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";Z.call(this,this.media,e)}return!s}return!1})),e(this,\"on\",((e,t)=>{X.call(this,this.elements.container,e,t)})),e(this,\"once\",((e,t)=>{G.call(this,this.elements.container,e,t)})),e(this,\"off\",((e,t)=>{J(this.elements.container,e,t)})),e(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(O(this.elements.buttons.play),O(this.elements.captions),O(this.elements.controls),O(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),S.function(e)&&e()):(ee.call(this),de.cancelRequests.call(this),q(this.elements.original,this.elements.container),Z.call(this,this.elements.original,\"destroyed\",!0),S.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(Fe.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&S.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),e(this,\"supports\",(e=>K.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=K.touch,this.media=t,S.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||S.nodeList(this.media)||S.array(this.media))&&(this.media=this.media[0]),this.config=x({},Le,st.defaults,i||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new De(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",K),S.nullOrUndefined(this.media)||!S.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!K.check().api)return void this.debug.error(\"Setup failed: no support\");const s=this.media.cloneNode(!0);s.autoplay=!1,this.elements.original=s;const n=this.media.tagName.toLowerCase();let a=null,l=null;switch(n){case\"div\":if(a=this.media.querySelector(\"iframe\"),S.element(a)){if(l=Me(a.getAttribute(\"src\")),this.provider=function(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?_e.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?_e.vimeo:null}(l.toString()),this.elements.container=this.media,this.media=a,this.elements.container.className=\"\",l.search.length){const e=[\"1\",\"true\"];e.includes(l.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(l.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(l.searchParams.get(\"playsinline\")),this.config.youtube.hl=l.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(S.empty(this.provider)||!Object.values(_e).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=je;break;case\"video\":case\"audio\":this.type=n,this.provider=_e.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=K.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Ve(this),this.storage=new we(this),this.media.plyr=this,S.element(this.elements.container)||(this.elements.container=$(\"div\"),L(this.media,this.elements.container)),Fe.migrateStyles.call(this),Fe.addStyleHook.call(this),Xe.setup.call(this),this.config.debug&&X.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new He(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Fe.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Je(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>ie(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===_e.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===_e.youtube}get isVimeo(){return this.provider===_e.vimeo}get isVideo(){return this.type===je}get isAudio(){return this.type===Oe}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=S.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return S.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=S.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;S.string(t)&&(t=Number(t)),S.number(t)||(t=this.storage.get(\"volume\")),S.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!S.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;S.boolean(t)||(t=this.storage.get(\"muted\")),S.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;S.number(e)&&(t=e),S.number(t)||(t=this.storage.get(\"speed\")),S.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=Ge(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!S.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(S.number),n=!0;if(!i.includes(s)){const e=ne(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=S.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){it.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return S.url(e)?e:this.source}set download(e){S.url(e)&&(this.config.urls.download=e,Pe.setDownloadUrl.call(this))}set poster(e){this.isVideo?Fe.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=oe(ce.call(this));return S.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?S.string(e)&&re(e)?(this.config.ratio=oe(e),ue.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=S.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){xe.toggle.call(this,e,!1)}set currentTrack(e){xe.set.call(this,e,!1),xe.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){xe.setLanguage.call(this,e,!1)}get language(){return(xe.getCurrentTrack.call(this)||{}).language}set pip(e){if(!K.pip)return;const t=S.boolean(e)?e:!this.pip;S.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?Ie:$e),S.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return K.pip?S.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===Ie:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new tt(this))}static supported(e,t){return K.check(e,t)}static loadSprite(e,t){return ke(e,t)}static setup(e,t={}){let i=null;return S.string(e)?i=Array.from(document.querySelectorAll(e)):S.nodeList(e)?i=Array.from(e):S.array(e)&&(i=e.filter(S.element)),S.empty(i)?null:i.map((e=>new st(e,t)))}}var nt;return st.defaults=(nt=Le,JSON.parse(JSON.stringify(nt))),st}));\n //# sourceMappingURL=plyr.min.js.map\ndiff --git a/node_modules/plyr/dist/plyr.min.js.map b/node_modules/plyr/dist/plyr.min.js.map\nindex 4bf012a..7336008 100644\n--- a/node_modules/plyr/dist/plyr.min.js.map\n+++ b/node_modules/plyr/dist/plyr.min.js.map\n@@ -1 +1 @@\n-{\"version\":3,\"sources\":[\"plyr.js\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"navigator\",\"global\",\"factory\",\"exports\",\"module\",\"define\",\"amd\",\"globalThis\",\"self\",\"Plyr\",\"this\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"arg\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"call\",\"TypeError\",\"String\",\"Number\",\"_toPrimitive\",\"_toPropertyKey\",\"Object\",\"defineProperty\",\"enumerable\",\"configurable\",\"writable\",\"_defineProperties\",\"e\",\"t\",\"n\",\"length\",\"r\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isString\",\"isArray\",\"Array\",\"isNodeList\",\"NodeList\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"isNaN\",\"string\",\"boolean\",\"Boolean\",\"function\",\"Function\",\"array\",\"nodeList\",\"element\",\"Element\",\"event\",\"Event\",\"empty\",\"round\",\"concat\",\"match\",\"Math\",\"max\",\"getDecimalPlaces\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"_classCallCheck\",\"document\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"prototype\",\"_createClass\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"set\",\"target\",\"i\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"u\",\"c\",\"getBoundingClientRect\",\"a\",\"width\",\"clientX\",\"left\",\"disabled\",\"preventDefault\",\"get\",\"bubbles\",\"dispatchEvent\",\"trigger\",\"type\",\"from\",\"querySelectorAll\",\"MutationObserver\",\"addedNodes\",\"includes\",\"matches\",\"observe\",\"body\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isFunction\",\"isEmpty\",\"weakMap\",\"WeakMap\",\"nodeType\",\"ownerDocument\",\"textNode\",\"Text\",\"keyboardEvent\",\"KeyboardEvent\",\"cue\",\"window\",\"TextTrackCue\",\"VTTCue\",\"track\",\"TextTrack\",\"kind\",\"promise\",\"Promise\",\"then\",\"url\",\"URL\",\"startsWith\",\"hostname\",\"_\",\"transitionEndEvent\",\"createElement\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"browser\",\"isIE\",\"documentMode\",\"isEdge\",\"test\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"getDeep\",\"path\",\"split\",\"reduce\",\"extend\",\"sources\",\"source\",\"shift\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"appendChild\",\"insertBefore\",\"setAttributes\",\"attributes\",\"entries\",\"setAttribute\",\"text\",\"innerText\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"replace\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"method\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"callback\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"detail\",\"CustomEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"indexOf\",\"closest\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"parse\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"format\",\"toString\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"slice\",\"toLowerCase\",\"toCamelCase\",\"toPascalCase\",\"getHTML\",\"innerHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"JSON\",\"storage\",\"setItem\",\"stringify\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"Error\",\"status\",\"open\",\"send\",\"error\",\"loadSprite\",\"prefix\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"location\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"join\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"current\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sort\",\"b\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"values\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"href\",\"urls\",\"isEmbed\",\"inject\",\"floor\",\"random\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"createDocumentFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"params\",\"URLSearchParams\",\"isYouTube\",\"protocol\",\"blob\",\"createObjectURL\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"has\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"providers\",\"types\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"head\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"el\",\"parentElement\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"attribute\",\"hasAttribute\",\"done\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"fn\",\"createCommonjsModule\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"doc\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathname\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"defaultPrevented\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"loadScript\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"found\",\"parseHash\",\"hashParam\",\"sidedock\",\"gesture\",\"$2\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"fragment\",\"firstChild\",\"stripHTML\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"setInterval\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"Ads\",\"google\",\"ima\",\"manager\",\"destroy\",\"displayContainer\",\"remove\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"getProviderByUrl\",\"search\",\"truthy\",\"searchParams\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.min.js\",\"sourcesContent\":[\"typeof navigator === \\\"object\\\" && (function (global, factory) {\\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\\n  typeof define === 'function' && define.amd ? define('Plyr', factory) :\\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());\\n})(this, (function () { 'use strict';\\n\\n  function _defineProperty$1(obj, key, value) {\\n    key = _toPropertyKey(key);\\n    if (key in obj) {\\n      Object.defineProperty(obj, key, {\\n        value: value,\\n        enumerable: true,\\n        configurable: true,\\n        writable: true\\n      });\\n    } else {\\n      obj[key] = value;\\n    }\\n    return obj;\\n  }\\n  function _toPrimitive(input, hint) {\\n    if (typeof input !== \\\"object\\\" || input === null) return input;\\n    var prim = input[Symbol.toPrimitive];\\n    if (prim !== undefined) {\\n      var res = prim.call(input, hint || \\\"default\\\");\\n      if (typeof res !== \\\"object\\\") return res;\\n      throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n    }\\n    return (hint === \\\"string\\\" ? String : Number)(input);\\n  }\\n  function _toPropertyKey(arg) {\\n    var key = _toPrimitive(arg, \\\"string\\\");\\n    return typeof key === \\\"symbol\\\" ? key : String(key);\\n  }\\n\\n  function _classCallCheck(e, t) {\\n    if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n  }\\n  function _defineProperties(e, t) {\\n    for (var n = 0; n < t.length; n++) {\\n      var r = t[n];\\n      r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n    }\\n  }\\n  function _createClass(e, t, n) {\\n    return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n  }\\n  function _defineProperty(e, t, n) {\\n    return t in e ? Object.defineProperty(e, t, {\\n      value: n,\\n      enumerable: !0,\\n      configurable: !0,\\n      writable: !0\\n    }) : e[t] = n, e;\\n  }\\n  function ownKeys(e, t) {\\n    var n = Object.keys(e);\\n    if (Object.getOwnPropertySymbols) {\\n      var r = Object.getOwnPropertySymbols(e);\\n      t && (r = r.filter(function (t) {\\n        return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n      })), n.push.apply(n, r);\\n    }\\n    return n;\\n  }\\n  function _objectSpread2(e) {\\n    for (var t = 1; t < arguments.length; t++) {\\n      var n = null != arguments[t] ? arguments[t] : {};\\n      t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n        _defineProperty(e, t, n[t]);\\n      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n      });\\n    }\\n    return e;\\n  }\\n  var defaults$1 = {\\n    addCSS: !0,\\n    thumbWidth: 15,\\n    watch: !0\\n  };\\n  function matches$1(e, t) {\\n    return function () {\\n      return Array.from(document.querySelectorAll(t)).includes(this);\\n    }.call(e, t);\\n  }\\n  function trigger(e, t) {\\n    if (e && t) {\\n      var n = new Event(t, {\\n        bubbles: !0\\n      });\\n      e.dispatchEvent(n);\\n    }\\n  }\\n  var getConstructor$1 = function (e) {\\n      return null != e ? e.constructor : null;\\n    },\\n    instanceOf$1 = function (e, t) {\\n      return !!(e && t && e instanceof t);\\n    },\\n    isNullOrUndefined$1 = function (e) {\\n      return null == e;\\n    },\\n    isObject$1 = function (e) {\\n      return getConstructor$1(e) === Object;\\n    },\\n    isNumber$1 = function (e) {\\n      return getConstructor$1(e) === Number && !Number.isNaN(e);\\n    },\\n    isString$1 = function (e) {\\n      return getConstructor$1(e) === String;\\n    },\\n    isBoolean$1 = function (e) {\\n      return getConstructor$1(e) === Boolean;\\n    },\\n    isFunction$1 = function (e) {\\n      return getConstructor$1(e) === Function;\\n    },\\n    isArray$1 = function (e) {\\n      return Array.isArray(e);\\n    },\\n    isNodeList$1 = function (e) {\\n      return instanceOf$1(e, NodeList);\\n    },\\n    isElement$1 = function (e) {\\n      return instanceOf$1(e, Element);\\n    },\\n    isEvent$1 = function (e) {\\n      return instanceOf$1(e, Event);\\n    },\\n    isEmpty$1 = function (e) {\\n      return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n    },\\n    is$1 = {\\n      nullOrUndefined: isNullOrUndefined$1,\\n      object: isObject$1,\\n      number: isNumber$1,\\n      string: isString$1,\\n      boolean: isBoolean$1,\\n      function: isFunction$1,\\n      array: isArray$1,\\n      nodeList: isNodeList$1,\\n      element: isElement$1,\\n      event: isEvent$1,\\n      empty: isEmpty$1\\n    };\\n  function getDecimalPlaces(e) {\\n    var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n    return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n  }\\n  function round(e, t) {\\n    if (1 > t) {\\n      var n = getDecimalPlaces(t);\\n      return parseFloat(e.toFixed(n));\\n    }\\n    return Math.round(e / t) * t;\\n  }\\n  var RangeTouch = function () {\\n    function e(t, n) {\\n      _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n    }\\n    return _createClass(e, [{\\n      key: \\\"init\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n      }\\n    }, {\\n      key: \\\"destroy\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n      }\\n    }, {\\n      key: \\\"listeners\\\",\\n      value: function (e) {\\n        var t = this,\\n          n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n        [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n          t.element[n](e, function (e) {\\n            return t.set(e);\\n          }, !1);\\n        });\\n      }\\n    }, {\\n      key: \\\"get\\\",\\n      value: function (t) {\\n        if (!e.enabled || !is$1.event(t)) return null;\\n        var n,\\n          r = t.target,\\n          i = t.changedTouches[0],\\n          o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n          s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n          u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n          c = r.getBoundingClientRect(),\\n          a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n        return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n      }\\n    }, {\\n      key: \\\"set\\\",\\n      value: function (t) {\\n        e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n      }\\n    }], [{\\n      key: \\\"setup\\\",\\n      value: function (t) {\\n        var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n          r = null;\\n        if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n        var i = _objectSpread2({}, defaults$1, {}, n);\\n        if (is$1.string(t) && i.watch) {\\n          var o = new MutationObserver(function (n) {\\n            Array.from(n).forEach(function (n) {\\n              Array.from(n.addedNodes).forEach(function (n) {\\n                is$1.element(n) && matches$1(n, t) && new e(n, i);\\n              });\\n            });\\n          });\\n          o.observe(document.body, {\\n            childList: !0,\\n            subtree: !0\\n          });\\n        }\\n        return r.map(function (t) {\\n          return new e(t, n);\\n        });\\n      }\\n    }, {\\n      key: \\\"enabled\\\",\\n      get: function () {\\n        return \\\"ontouchstart\\\" in document.documentElement;\\n      }\\n    }]), e;\\n  }();\\n\\n  // ==========================================================================\\n  // Type checking utils\\n  // ==========================================================================\\n\\n  const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\n  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\n  const isNullOrUndefined = input => input === null || typeof input === 'undefined';\\n  const isObject = input => getConstructor(input) === Object;\\n  const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\n  const isString = input => getConstructor(input) === String;\\n  const isBoolean = input => getConstructor(input) === Boolean;\\n  const isFunction = input => typeof input === 'function';\\n  const isArray = input => Array.isArray(input);\\n  const isWeakMap = input => instanceOf(input, WeakMap);\\n  const isNodeList = input => instanceOf(input, NodeList);\\n  const isTextNode = input => getConstructor(input) === Text;\\n  const isEvent = input => instanceOf(input, Event);\\n  const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\n  const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\n  const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\n  const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\n  const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\n  const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\n  const isUrl = input => {\\n    // Accept a URL object\\n    if (instanceOf(input, window.URL)) {\\n      return true;\\n    }\\n\\n    // Must be string from here\\n    if (!isString(input)) {\\n      return false;\\n    }\\n\\n    // Add the protocol if required\\n    let string = input;\\n    if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n      string = `http://${input}`;\\n    }\\n    try {\\n      return !isEmpty(new URL(string).hostname);\\n    } catch (_) {\\n      return false;\\n    }\\n  };\\n  var is = {\\n    nullOrUndefined: isNullOrUndefined,\\n    object: isObject,\\n    number: isNumber,\\n    string: isString,\\n    boolean: isBoolean,\\n    function: isFunction,\\n    array: isArray,\\n    weakMap: isWeakMap,\\n    nodeList: isNodeList,\\n    element: isElement,\\n    textNode: isTextNode,\\n    event: isEvent,\\n    keyboardEvent: isKeyboardEvent,\\n    cue: isCue,\\n    track: isTrack,\\n    promise: isPromise,\\n    url: isUrl,\\n    empty: isEmpty\\n  };\\n\\n  // ==========================================================================\\n  const transitionEndEvent = (() => {\\n    const element = document.createElement('span');\\n    const events = {\\n      WebkitTransition: 'webkitTransitionEnd',\\n      MozTransition: 'transitionend',\\n      OTransition: 'oTransitionEnd otransitionend',\\n      transition: 'transitionend'\\n    };\\n    const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n    return is.string(type) ? events[type] : false;\\n  })();\\n\\n  // Force repaint of element\\n  function repaint(element, delay) {\\n    setTimeout(() => {\\n      try {\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = true;\\n\\n        // eslint-disable-next-line no-unused-expressions\\n        element.offsetHeight;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = false;\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    }, delay);\\n  }\\n\\n  // ==========================================================================\\n  // Browser sniffing\\n  // Unfortunately, due to mixed support, UA sniffing is required\\n  // ==========================================================================\\n\\n  const isIE = Boolean(window.document.documentMode);\\n  const isEdge = /Edge/g.test(navigator.userAgent);\\n  const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\n  const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  // navigator.platform may be deprecated but this check is still required\\n  const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\n  const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  var browser = {\\n    isIE,\\n    isEdge,\\n    isWebKit,\\n    isIPhone,\\n    isIPadOS,\\n    isIos\\n  };\\n\\n  // ==========================================================================\\n\\n  // Clone nested objects\\n  function cloneDeep(object) {\\n    return JSON.parse(JSON.stringify(object));\\n  }\\n\\n  // Get a nested value in an object\\n  function getDeep(object, path) {\\n    return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n  }\\n\\n  // Deep extend destination object with N more objects\\n  function extend(target = {}, ...sources) {\\n    if (!sources.length) {\\n      return target;\\n    }\\n    const source = sources.shift();\\n    if (!is.object(source)) {\\n      return target;\\n    }\\n    Object.keys(source).forEach(key => {\\n      if (is.object(source[key])) {\\n        if (!Object.keys(target).includes(key)) {\\n          Object.assign(target, {\\n            [key]: {}\\n          });\\n        }\\n        extend(target[key], source[key]);\\n      } else {\\n        Object.assign(target, {\\n          [key]: source[key]\\n        });\\n      }\\n    });\\n    return extend(target, ...sources);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Wrap an element\\n  function wrap(elements, wrapper) {\\n    // Convert `elements` to an array, if necessary.\\n    const targets = elements.length ? elements : [elements];\\n\\n    // Loops backwards to prevent having to clone the wrapper on the\\n    // first element (see `child` below).\\n    Array.from(targets).reverse().forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n  }\\n\\n  // Set attributes\\n  function setAttributes(element, attributes) {\\n    if (!is.element(element) || is.empty(attributes)) return;\\n\\n    // Assume null and undefined attributes should be left out,\\n    // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n    Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n  }\\n\\n  // Create a DocumentFragment\\n  function createElement(type, attributes, text) {\\n    // Create a new <element>\\n    const element = document.createElement(type);\\n\\n    // Set all passed attributes\\n    if (is.object(attributes)) {\\n      setAttributes(element, attributes);\\n    }\\n\\n    // Add text node\\n    if (is.string(text)) {\\n      element.innerText = text;\\n    }\\n\\n    // Return built element\\n    return element;\\n  }\\n\\n  // Insert an element after another\\n  function insertAfter(element, target) {\\n    if (!is.element(element) || !is.element(target)) return;\\n    target.parentNode.insertBefore(element, target.nextSibling);\\n  }\\n\\n  // Insert a DocumentFragment\\n  function insertElement(type, parent, attributes, text) {\\n    if (!is.element(parent)) return;\\n    parent.appendChild(createElement(type, attributes, text));\\n  }\\n\\n  // Remove element(s)\\n  function removeElement(element) {\\n    if (is.nodeList(element) || is.array(element)) {\\n      Array.from(element).forEach(removeElement);\\n      return;\\n    }\\n    if (!is.element(element) || !is.element(element.parentNode)) {\\n      return;\\n    }\\n    element.parentNode.removeChild(element);\\n  }\\n\\n  // Remove all child elements\\n  function emptyElement(element) {\\n    if (!is.element(element)) return;\\n    let {\\n      length\\n    } = element.childNodes;\\n    while (length > 0) {\\n      element.removeChild(element.lastChild);\\n      length -= 1;\\n    }\\n  }\\n\\n  // Replace element\\n  function replaceElement(newChild, oldChild) {\\n    if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n    oldChild.parentNode.replaceChild(newChild, oldChild);\\n    return newChild;\\n  }\\n\\n  // Get an attribute object from a string selector\\n  function getAttributesFromSelector(sel, existingAttributes) {\\n    // For example:\\n    // '.test' to { class: 'test' }\\n    // '#test' to { id: 'test' }\\n    // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n    if (!is.string(sel) || is.empty(sel)) return {};\\n    const attributes = {};\\n    const existing = extend({}, existingAttributes);\\n    sel.split(',').forEach(s => {\\n      // Remove whitespace\\n      const selector = s.trim();\\n      const className = selector.replace('.', '');\\n      const stripped = selector.replace(/[[\\\\]]/g, '');\\n      // Get the parts and value\\n      const parts = stripped.split('=');\\n      const [key] = parts;\\n      const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n      // Get the first character\\n      const start = selector.charAt(0);\\n      switch (start) {\\n        case '.':\\n          // Add to existing classname\\n          if (is.string(existing.class)) {\\n            attributes.class = `${existing.class} ${className}`;\\n          } else {\\n            attributes.class = className;\\n          }\\n          break;\\n        case '#':\\n          // ID selector\\n          attributes.id = selector.replace('#', '');\\n          break;\\n        case '[':\\n          // Attribute selector\\n          attributes[key] = value;\\n          break;\\n      }\\n    });\\n    return extend(existing, attributes);\\n  }\\n\\n  // Toggle hidden\\n  function toggleHidden(element, hidden) {\\n    if (!is.element(element)) return;\\n    let hide = hidden;\\n    if (!is.boolean(hide)) {\\n      hide = !element.hidden;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    element.hidden = hide;\\n  }\\n\\n  // Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\n  function toggleClass(element, className, force) {\\n    if (is.nodeList(element)) {\\n      return Array.from(element).map(e => toggleClass(e, className, force));\\n    }\\n    if (is.element(element)) {\\n      let method = 'toggle';\\n      if (typeof force !== 'undefined') {\\n        method = force ? 'add' : 'remove';\\n      }\\n      element.classList[method](className);\\n      return element.classList.contains(className);\\n    }\\n    return false;\\n  }\\n\\n  // Has class name\\n  function hasClass(element, className) {\\n    return is.element(element) && element.classList.contains(className);\\n  }\\n\\n  // Element matches selector\\n  function matches(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n    function match() {\\n      return Array.from(document.querySelectorAll(selector)).includes(this);\\n    }\\n    const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n    return method.call(element, selector);\\n  }\\n\\n  // Closest ancestor element matching selector (also tests element itself)\\n  function closest$1(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n\\n    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n    function closestElement() {\\n      let el = this;\\n      do {\\n        if (matches.matches(el, selector)) return el;\\n        el = el.parentElement || el.parentNode;\\n      } while (el !== null && el.nodeType === 1);\\n      return null;\\n    }\\n    const method = prototype.closest || closestElement;\\n    return method.call(element, selector);\\n  }\\n\\n  // Find all elements\\n  function getElements(selector) {\\n    return this.elements.container.querySelectorAll(selector);\\n  }\\n\\n  // Find a single element\\n  function getElement(selector) {\\n    return this.elements.container.querySelector(selector);\\n  }\\n\\n  // Set focus and tab focus class\\n  function setFocus(element = null, focusVisible = false) {\\n    if (!is.element(element)) return;\\n\\n    // Set regular focus\\n    element.focus({\\n      preventScroll: true,\\n      focusVisible\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Default codecs for checking mimetype support\\n  const defaultCodecs = {\\n    'audio/ogg': 'vorbis',\\n    'audio/wav': '1',\\n    'video/webm': 'vp8, vorbis',\\n    'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n    'video/ogg': 'theora'\\n  };\\n\\n  // Check for feature support\\n  const support = {\\n    // Basic support\\n    audio: 'canPlayType' in document.createElement('audio'),\\n    video: 'canPlayType' in document.createElement('video'),\\n    // Check for support\\n    // Basic functionality vs full UI\\n    check(type, provider) {\\n      const api = support[type] || provider !== 'html5';\\n      const ui = api && support.rangeInput;\\n      return {\\n        api,\\n        ui\\n      };\\n    },\\n    // Picture-in-picture support\\n    // Safari & Chrome only currently\\n    pip: (() => {\\n      // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n      // It will throw the following error when trying to enter picture-in-picture\\n      // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n      if (browser.isIPhone) {\\n        return false;\\n      }\\n\\n      // Safari\\n      // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n      if (is.function(createElement('video').webkitSetPresentationMode)) {\\n        return true;\\n      }\\n\\n      // Chrome\\n      // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n      if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n        return true;\\n      }\\n      return false;\\n    })(),\\n    // Airplay support\\n    // Safari only currently\\n    airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n    // Inline playback support\\n    // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n    playsinline: 'playsInline' in document.createElement('video'),\\n    // Check for mime type support against a player instance\\n    // Credits: http://diveintohtml5.info/everything.html\\n    // Related: http://www.leanbackplayer.com/test/h5mt.html\\n    mime(input) {\\n      if (is.empty(input)) {\\n        return false;\\n      }\\n      const [mediaType] = input.split('/');\\n      let type = input;\\n\\n      // Verify we're using HTML5 and there's no media type mismatch\\n      if (!this.isHTML5 || mediaType !== this.type) {\\n        return false;\\n      }\\n\\n      // Add codec if required\\n      if (Object.keys(defaultCodecs).includes(type)) {\\n        type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n      }\\n      try {\\n        return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n      } catch (_) {\\n        return false;\\n      }\\n    },\\n    // Check for textTracks support\\n    textTracks: 'textTracks' in document.createElement('video'),\\n    // <input type=\\\"range\\\"> Sliders\\n    rangeInput: (() => {\\n      const range = document.createElement('input');\\n      range.type = 'range';\\n      return range.type === 'range';\\n    })(),\\n    // Touch\\n    // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n    touch: 'ontouchstart' in document.documentElement,\\n    // Detect transitions support\\n    transitions: transitionEndEvent !== false,\\n    // Reduced motion iOS & MacOS setting\\n    // https://webkit.org/blog/7551/responsive-design-for-motion/\\n    reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n  };\\n\\n  // ==========================================================================\\n\\n  // Check for passive event listener support\\n  // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n  // https://www.youtube.com/watch?v=NPM6172J22g\\n  const supportsPassiveListeners = (() => {\\n    // Test via a getter in the options object to see if the passive property is accessed\\n    let supported = false;\\n    try {\\n      const options = Object.defineProperty({}, 'passive', {\\n        get() {\\n          supported = true;\\n          return null;\\n        }\\n      });\\n      window.addEventListener('test', null, options);\\n      window.removeEventListener('test', null, options);\\n    } catch (_) {\\n      // Do nothing\\n    }\\n    return supported;\\n  })();\\n\\n  // Toggle event listener\\n  function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n    // Bail if no element, event, or callback\\n    if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n      return;\\n    }\\n\\n    // Allow multiple events\\n    const events = event.split(' ');\\n    // Build options\\n    // Default to just the capture boolean for browsers with no passive listener support\\n    let options = capture;\\n\\n    // If passive events listeners are supported\\n    if (supportsPassiveListeners) {\\n      options = {\\n        // Whether the listener can be passive (i.e. default never prevented)\\n        passive,\\n        // Whether the listener is a capturing listener or not\\n        capture\\n      };\\n    }\\n\\n    // If a single node is passed, bind the event listener\\n    events.forEach(type => {\\n      if (this && this.eventListeners && toggle) {\\n        // Cache event listener\\n        this.eventListeners.push({\\n          element,\\n          type,\\n          callback,\\n          options\\n        });\\n      }\\n      element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n    });\\n  }\\n\\n  // Bind event handler\\n  function on(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, true, passive, capture);\\n  }\\n\\n  // Unbind event handler\\n  function off(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, false, passive, capture);\\n  }\\n\\n  // Bind once-only event handler\\n  function once(element, events = '', callback, passive = true, capture = false) {\\n    const onceCallback = (...args) => {\\n      off(element, events, onceCallback, passive, capture);\\n      callback.apply(this, args);\\n    };\\n    toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n  }\\n\\n  // Trigger event\\n  function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n    // Bail if no element\\n    if (!is.element(element) || is.empty(type)) {\\n      return;\\n    }\\n\\n    // Create and dispatch the event\\n    const event = new CustomEvent(type, {\\n      bubbles,\\n      detail: {\\n        ...detail,\\n        plyr: this\\n      }\\n    });\\n\\n    // Dispatch the event\\n    element.dispatchEvent(event);\\n  }\\n\\n  // Unbind all cached event listeners\\n  function unbindListeners() {\\n    if (this && this.eventListeners) {\\n      this.eventListeners.forEach(item => {\\n        const {\\n          element,\\n          type,\\n          callback,\\n          options\\n        } = item;\\n        element.removeEventListener(type, callback, options);\\n      });\\n      this.eventListeners = [];\\n    }\\n  }\\n\\n  // Run method when / if player is ready\\n  function ready() {\\n    return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n  }\\n\\n  /**\\n   * Silence a Promise-like object.\\n   * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n   * play promise\\\" rejection error messages.\\n   * @param  {Object} value An object that may or may not be `Promise`-like.\\n   */\\n  function silencePromise(value) {\\n    if (is.promise(value)) {\\n      value.then(null, () => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Remove duplicates in an array\\n  function dedupe(array) {\\n    if (!is.array(array)) {\\n      return array;\\n    }\\n    return array.filter((item, index) => array.indexOf(item) === index);\\n  }\\n\\n  // Get the closest value in an array\\n  function closest(array, value) {\\n    if (!is.array(array) || !array.length) {\\n      return null;\\n    }\\n    return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Check support for a CSS declaration\\n  function supportsCSS(declaration) {\\n    if (!window || !window.CSS) {\\n      return false;\\n    }\\n    return window.CSS.supports(declaration);\\n  }\\n\\n  // Standard/common aspect ratios\\n  const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n    ...out,\\n    [x / y]: [x, y]\\n  }), {});\\n\\n  // Validate an aspect ratio\\n  function validateAspectRatio(input) {\\n    if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n      return false;\\n    }\\n    const ratio = is.array(input) ? input : input.split(':');\\n    return ratio.map(Number).every(is.number);\\n  }\\n\\n  // Reduce an aspect ratio to it's lowest form\\n  function reduceAspectRatio(ratio) {\\n    if (!is.array(ratio) || !ratio.every(is.number)) {\\n      return null;\\n    }\\n    const [width, height] = ratio;\\n    const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n    const divider = getDivider(width, height);\\n    return [width / divider, height / divider];\\n  }\\n\\n  // Calculate an aspect ratio\\n  function getAspectRatio(input) {\\n    const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n    // Try provided ratio\\n    let ratio = parse(input);\\n\\n    // Get from config\\n    if (ratio === null) {\\n      ratio = parse(this.config.ratio);\\n    }\\n\\n    // Get from embed\\n    if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n      ({\\n        ratio\\n      } = this.embed);\\n    }\\n\\n    // Get from HTML5 video\\n    if (ratio === null && this.isHTML5) {\\n      const {\\n        videoWidth,\\n        videoHeight\\n      } = this.media;\\n      ratio = [videoWidth, videoHeight];\\n    }\\n    return reduceAspectRatio(ratio);\\n  }\\n\\n  // Set aspect ratio for responsive container\\n  function setAspectRatio(input) {\\n    if (!this.isVideo) {\\n      return {};\\n    }\\n    const {\\n      wrapper\\n    } = this.elements;\\n    const ratio = getAspectRatio.call(this, input);\\n    if (!is.array(ratio)) {\\n      return {};\\n    }\\n    const [x, y] = reduceAspectRatio(ratio);\\n    const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n    const padding = 100 / x * y;\\n    if (useNative) {\\n      wrapper.style.aspectRatio = `${x}/${y}`;\\n    } else {\\n      wrapper.style.paddingBottom = `${padding}%`;\\n    }\\n\\n    // For Vimeo we have an extra <div> to hide the standard controls and UI\\n    if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n      const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n      const offset = (height - padding) / (height / 50);\\n      if (this.fullscreen.active) {\\n        wrapper.style.paddingBottom = null;\\n      } else {\\n        this.media.style.transform = `translateY(-${offset}%)`;\\n      }\\n    } else if (this.isHTML5) {\\n      wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n    }\\n    return {\\n      padding,\\n      ratio\\n    };\\n  }\\n\\n  // Round an aspect ratio to closest standard ratio\\n  function roundAspectRatio(x, y, tolerance = 0.05) {\\n    const ratio = x / y;\\n    const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n    // Check match is within tolerance\\n    if (Math.abs(closestRatio - ratio) <= tolerance) {\\n      return standardRatios[closestRatio];\\n    }\\n\\n    // No match\\n    return [x, y];\\n  }\\n\\n  // Get the size of the viewport\\n  // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\n  function getViewportSize() {\\n    const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n    const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n    return [width, height];\\n  }\\n\\n  // ==========================================================================\\n  const html5 = {\\n    getSources() {\\n      if (!this.isHTML5) {\\n        return [];\\n      }\\n      const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n      // Filter out unsupported sources (if type is specified)\\n      return sources.filter(source => {\\n        const type = source.getAttribute('type');\\n        if (is.empty(type)) {\\n          return true;\\n        }\\n        return support.mime.call(this, type);\\n      });\\n    },\\n    // Get quality levels\\n    getQualityOptions() {\\n      // Whether we're forcing all options (e.g. for streaming)\\n      if (this.config.quality.forced) {\\n        return this.config.quality.options;\\n      }\\n\\n      // Get sizes from <source> elements\\n      return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n    },\\n    setup() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n      const player = this;\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set aspect ratio if fixed\\n      if (!is.empty(this.config.ratio)) {\\n        setAspectRatio.call(player);\\n      }\\n\\n      // Quality\\n      Object.defineProperty(player.media, 'quality', {\\n        get() {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n          // Return size, if match is found\\n          return source && Number(source.getAttribute('size'));\\n        },\\n        set(input) {\\n          if (player.quality === input) {\\n            return;\\n          }\\n\\n          // If we're using an external handler...\\n          if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n            player.config.quality.onChange(input);\\n          } else {\\n            // Get sources\\n            const sources = html5.getSources.call(player);\\n            // Get first match for requested size\\n            const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n            // No matching source found\\n            if (!source) {\\n              return;\\n            }\\n\\n            // Get current state\\n            const {\\n              currentTime,\\n              paused,\\n              preload,\\n              readyState,\\n              playbackRate\\n            } = player.media;\\n\\n            // Set new source\\n            player.media.src = source.getAttribute('src');\\n\\n            // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n            if (preload !== 'none' || readyState) {\\n              // Restore time\\n              player.once('loadedmetadata', () => {\\n                player.speed = playbackRate;\\n                player.currentTime = currentTime;\\n\\n                // Resume playing\\n                if (!paused) {\\n                  silencePromise(player.play());\\n                }\\n              });\\n\\n              // Load new source\\n              player.media.load();\\n            }\\n          }\\n\\n          // Trigger change event\\n          triggerEvent.call(player, player.media, 'qualitychange', false, {\\n            quality: input\\n          });\\n        }\\n      });\\n    },\\n    // Cancel current network requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    cancelRequests() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n\\n      // Remove child sources\\n      removeElement(html5.getSources.call(this));\\n\\n      // Set blank video src attribute\\n      // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n      // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n      this.media.setAttribute('src', this.config.blankVideo);\\n\\n      // Load the new empty source\\n      // This will cancel existing requests\\n      // See https://github.com/sampotts/plyr/issues/174\\n      this.media.load();\\n\\n      // Debugging\\n      this.debug.log('Cancelled network requests');\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Generate a random ID\\n  function generateId(prefix) {\\n    return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n  }\\n\\n  // Format string\\n  function format(input, ...args) {\\n    if (is.empty(input)) return input;\\n    return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n  }\\n\\n  // Get percentage\\n  function getPercentage(current, max) {\\n    if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n      return 0;\\n    }\\n    return (current / max * 100).toFixed(2);\\n  }\\n\\n  // Replace all occurrences of a string in a string\\n  const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n  // Convert to title case\\n  const toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n  // Convert string to pascalCase\\n  function toPascalCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert kebab case\\n    string = replaceAll(string, '-', ' ');\\n\\n    // Convert snake case\\n    string = replaceAll(string, '_', ' ');\\n\\n    // Convert to title case\\n    string = toTitleCase(string);\\n\\n    // Convert to pascal case\\n    return replaceAll(string, ' ', '');\\n  }\\n\\n  // Convert string to pascalCase\\n  function toCamelCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert to pascal case\\n    string = toPascalCase(string);\\n\\n    // Convert first character to lowercase\\n    return string.charAt(0).toLowerCase() + string.slice(1);\\n  }\\n\\n  // Remove HTML from a string\\n  function stripHTML(source) {\\n    const fragment = document.createDocumentFragment();\\n    const element = document.createElement('div');\\n    fragment.appendChild(element);\\n    element.innerHTML = source;\\n    return fragment.firstChild.innerText;\\n  }\\n\\n  // Like outerHTML, but also works for DocumentFragment\\n  function getHTML(element) {\\n    const wrapper = document.createElement('div');\\n    wrapper.appendChild(element);\\n    return wrapper.innerHTML;\\n  }\\n\\n  // ==========================================================================\\n\\n  // Skip i18n for abbreviations and brand names\\n  const resources = {\\n    pip: 'PIP',\\n    airplay: 'AirPlay',\\n    html5: 'HTML5',\\n    vimeo: 'Vimeo',\\n    youtube: 'YouTube'\\n  };\\n  const i18n = {\\n    get(key = '', config = {}) {\\n      if (is.empty(key) || is.empty(config)) {\\n        return '';\\n      }\\n      let string = getDeep(config.i18n, key);\\n      if (is.empty(string)) {\\n        if (Object.keys(resources).includes(key)) {\\n          return resources[key];\\n        }\\n        return '';\\n      }\\n      const replace = {\\n        '{seektime}': config.seekTime,\\n        '{title}': config.title\\n      };\\n      Object.entries(replace).forEach(([k, v]) => {\\n        string = replaceAll(string, k, v);\\n      });\\n      return string;\\n    }\\n  };\\n\\n  class Storage {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"get\\\", key => {\\n        if (!Storage.supported || !this.enabled) {\\n          return null;\\n        }\\n        const store = window.localStorage.getItem(this.key);\\n        if (is.empty(store)) {\\n          return null;\\n        }\\n        const json = JSON.parse(store);\\n        return is.string(key) && key.length ? json[key] : json;\\n      });\\n      _defineProperty$1(this, \\\"set\\\", object => {\\n        // Bail if we don't have localStorage support or it's disabled\\n        if (!Storage.supported || !this.enabled) {\\n          return;\\n        }\\n\\n        // Can only store objectst\\n        if (!is.object(object)) {\\n          return;\\n        }\\n\\n        // Get current storage\\n        let storage = this.get();\\n\\n        // Default to empty object\\n        if (is.empty(storage)) {\\n          storage = {};\\n        }\\n\\n        // Update the working copy of the values\\n        extend(storage, object);\\n\\n        // Update storage\\n        try {\\n          window.localStorage.setItem(this.key, JSON.stringify(storage));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      });\\n      this.enabled = player.config.storage.enabled;\\n      this.key = player.config.storage.key;\\n    }\\n\\n    // Check for actual support (see if we can use it)\\n    static get supported() {\\n      try {\\n        if (!('localStorage' in window)) {\\n          return false;\\n        }\\n        const test = '___test';\\n\\n        // Try to use it (it might be disabled, e.g. user is in private mode)\\n        // see: https://github.com/sampotts/plyr/issues/131\\n        window.localStorage.setItem(test, test);\\n        window.localStorage.removeItem(test);\\n        return true;\\n      } catch (_) {\\n        return false;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Fetch wrapper\\n  // Using XHR to avoid issues with older browsers\\n  // ==========================================================================\\n\\n  function fetch(url, responseType = 'text') {\\n    return new Promise((resolve, reject) => {\\n      try {\\n        const request = new XMLHttpRequest();\\n\\n        // Check for CORS support\\n        if (!('withCredentials' in request)) {\\n          return;\\n        }\\n        request.addEventListener('load', () => {\\n          if (responseType === 'text') {\\n            try {\\n              resolve(JSON.parse(request.responseText));\\n            } catch (_) {\\n              resolve(request.responseText);\\n            }\\n          } else {\\n            resolve(request.response);\\n          }\\n        });\\n        request.addEventListener('error', () => {\\n          throw new Error(request.status);\\n        });\\n        request.open('GET', url, true);\\n\\n        // Set the required response type\\n        request.responseType = responseType;\\n        request.send();\\n      } catch (error) {\\n        reject(error);\\n      }\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Load an external SVG sprite\\n  function loadSprite(url, id) {\\n    if (!is.string(url)) {\\n      return;\\n    }\\n    const prefix = 'cache';\\n    const hasId = is.string(id);\\n    let isCached = false;\\n    const exists = () => document.getElementById(id) !== null;\\n    const update = (container, data) => {\\n      // eslint-disable-next-line no-param-reassign\\n      container.innerHTML = data;\\n\\n      // Check again incase of race condition\\n      if (hasId && exists()) {\\n        return;\\n      }\\n\\n      // Inject the SVG to the body\\n      document.body.insertAdjacentElement('afterbegin', container);\\n    };\\n\\n    // Only load once if ID set\\n    if (!hasId || !exists()) {\\n      const useStorage = Storage.supported;\\n      // Create container\\n      const container = document.createElement('div');\\n      container.setAttribute('hidden', '');\\n      if (hasId) {\\n        container.setAttribute('id', id);\\n      }\\n\\n      // Check in cache\\n      if (useStorage) {\\n        const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n        isCached = cached !== null;\\n        if (isCached) {\\n          const data = JSON.parse(cached);\\n          update(container, data.content);\\n        }\\n      }\\n\\n      // Get the sprite\\n      fetch(url).then(result => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n              content: result\\n            }));\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n        update(container, result);\\n      }).catch(() => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Time helpers\\n  const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\n  const getMinutes = value => Math.trunc(value / 60 % 60, 10);\\n  const getSeconds = value => Math.trunc(value % 60, 10);\\n\\n  // Format time to UI friendly string\\n  function formatTime(time = 0, displayHours = false, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return formatTime(undefined, displayHours, inverted);\\n    }\\n\\n    // Format time component to add leading zero\\n    const format = value => `0${value}`.slice(-2);\\n    // Breakdown to hours, mins, secs\\n    let hours = getHours(time);\\n    const mins = getMinutes(time);\\n    const secs = getSeconds(time);\\n\\n    // Do we need to display hours?\\n    if (displayHours || hours > 0) {\\n      hours = `${hours}:`;\\n    } else {\\n      hours = '';\\n    }\\n\\n    // Render\\n    return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n  }\\n\\n  // ==========================================================================\\n\\n  // TODO: Don't export a massive object - break down and create class\\n  const controls = {\\n    // Get icon URL\\n    getIconUrl() {\\n      const url = new URL(this.config.iconUrl, window.location);\\n      const host = window.location.host ? window.location.host : window.top.location.host;\\n      const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n      return {\\n        url: this.config.iconUrl,\\n        cors\\n      };\\n    },\\n    // Find the UI controls\\n    findElements() {\\n      try {\\n        this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n        // Buttons\\n        this.elements.buttons = {\\n          play: getElements.call(this, this.config.selectors.buttons.play),\\n          pause: getElement.call(this, this.config.selectors.buttons.pause),\\n          restart: getElement.call(this, this.config.selectors.buttons.restart),\\n          rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n          fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n          mute: getElement.call(this, this.config.selectors.buttons.mute),\\n          pip: getElement.call(this, this.config.selectors.buttons.pip),\\n          airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n          settings: getElement.call(this, this.config.selectors.buttons.settings),\\n          captions: getElement.call(this, this.config.selectors.buttons.captions),\\n          fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n        };\\n\\n        // Progress\\n        this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n        // Inputs\\n        this.elements.inputs = {\\n          seek: getElement.call(this, this.config.selectors.inputs.seek),\\n          volume: getElement.call(this, this.config.selectors.inputs.volume)\\n        };\\n\\n        // Display\\n        this.elements.display = {\\n          buffer: getElement.call(this, this.config.selectors.display.buffer),\\n          currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n          duration: getElement.call(this, this.config.selectors.display.duration)\\n        };\\n\\n        // Seek tooltip\\n        if (is.element(this.elements.progress)) {\\n          this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n        }\\n        return true;\\n      } catch (error) {\\n        // Log it\\n        this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n        // Restore native video controls\\n        this.toggleNativeControls(true);\\n        return false;\\n      }\\n    },\\n    // Create <svg> icon\\n    createIcon(type, attributes) {\\n      const namespace = 'http://www.w3.org/2000/svg';\\n      const iconUrl = controls.getIconUrl.call(this);\\n      const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n      // Create <svg>\\n      const icon = document.createElementNS(namespace, 'svg');\\n      setAttributes(icon, extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false'\\n      }));\\n\\n      // Create the <use> to reference sprite\\n      const use = document.createElementNS(namespace, 'use');\\n      const path = `${iconPath}-${type}`;\\n\\n      // Set `href` attributes\\n      // https://github.com/sampotts/plyr/issues/460\\n      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n      if ('href' in use) {\\n        use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n      }\\n\\n      // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n      // Add <use> to <svg>\\n      icon.appendChild(use);\\n      return icon;\\n    },\\n    // Create hidden text label\\n    createLabel(key, attr = {}) {\\n      const text = i18n.get(key, this.config);\\n      const attributes = {\\n        ...attr,\\n        class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n      };\\n      return createElement('span', attributes, text);\\n    },\\n    // Create a badge\\n    createBadge(text) {\\n      if (is.empty(text)) {\\n        return null;\\n      }\\n      const badge = createElement('span', {\\n        class: this.config.classNames.menu.value\\n      });\\n      badge.appendChild(createElement('span', {\\n        class: this.config.classNames.menu.badge\\n      }, text));\\n      return badge;\\n    },\\n    // Create a <button>\\n    createButton(buttonType, attr) {\\n      const attributes = extend({}, attr);\\n      let type = toCamelCase(buttonType);\\n      const props = {\\n        element: 'button',\\n        toggle: false,\\n        label: null,\\n        icon: null,\\n        labelPressed: null,\\n        iconPressed: null\\n      };\\n      ['element', 'icon', 'label'].forEach(key => {\\n        if (Object.keys(attributes).includes(key)) {\\n          props[key] = attributes[key];\\n          delete attributes[key];\\n        }\\n      });\\n\\n      // Default to 'button' type to prevent form submission\\n      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n        attributes.type = 'button';\\n      }\\n\\n      // Set class name\\n      if (Object.keys(attributes).includes('class')) {\\n        if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n          extend(attributes, {\\n            class: `${attributes.class} ${this.config.classNames.control}`\\n          });\\n        }\\n      } else {\\n        attributes.class = this.config.classNames.control;\\n      }\\n\\n      // Large play button\\n      switch (buttonType) {\\n        case 'play':\\n          props.toggle = true;\\n          props.label = 'play';\\n          props.labelPressed = 'pause';\\n          props.icon = 'play';\\n          props.iconPressed = 'pause';\\n          break;\\n        case 'mute':\\n          props.toggle = true;\\n          props.label = 'mute';\\n          props.labelPressed = 'unmute';\\n          props.icon = 'volume';\\n          props.iconPressed = 'muted';\\n          break;\\n        case 'captions':\\n          props.toggle = true;\\n          props.label = 'enableCaptions';\\n          props.labelPressed = 'disableCaptions';\\n          props.icon = 'captions-off';\\n          props.iconPressed = 'captions-on';\\n          break;\\n        case 'fullscreen':\\n          props.toggle = true;\\n          props.label = 'enterFullscreen';\\n          props.labelPressed = 'exitFullscreen';\\n          props.icon = 'enter-fullscreen';\\n          props.iconPressed = 'exit-fullscreen';\\n          break;\\n        case 'play-large':\\n          attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n          type = 'play';\\n          props.label = 'play';\\n          props.icon = 'play';\\n          break;\\n        default:\\n          if (is.empty(props.label)) {\\n            props.label = type;\\n          }\\n          if (is.empty(props.icon)) {\\n            props.icon = buttonType;\\n          }\\n      }\\n      const button = createElement(props.element);\\n\\n      // Setup toggle icon and labels\\n      if (props.toggle) {\\n        // Icon\\n        button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed'\\n        }));\\n        button.appendChild(controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed'\\n        }));\\n\\n        // Label/Tooltip\\n        button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed'\\n        }));\\n        button.appendChild(controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed'\\n        }));\\n      } else {\\n        button.appendChild(controls.createIcon.call(this, props.icon));\\n        button.appendChild(controls.createLabel.call(this, props.label));\\n      }\\n\\n      // Merge and set attributes\\n      extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n      setAttributes(button, attributes);\\n\\n      // We have multiple play buttons\\n      if (type === 'play') {\\n        if (!is.array(this.elements.buttons[type])) {\\n          this.elements.buttons[type] = [];\\n        }\\n        this.elements.buttons[type].push(button);\\n      } else {\\n        this.elements.buttons[type] = button;\\n      }\\n      return button;\\n    },\\n    // Create an <input type='range'>\\n    createRange(type, attributes) {\\n      // Seek input\\n      const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n        type: 'range',\\n        min: 0,\\n        max: 100,\\n        step: 0.01,\\n        value: 0,\\n        autocomplete: 'off',\\n        // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n        role: 'slider',\\n        'aria-label': i18n.get(type, this.config),\\n        'aria-valuemin': 0,\\n        'aria-valuemax': 100,\\n        'aria-valuenow': 0\\n      }, attributes));\\n      this.elements.inputs[type] = input;\\n\\n      // Set the fill for webkit now\\n      controls.updateRangeFill.call(this, input);\\n\\n      // Improve support on touch devices\\n      RangeTouch.setup(input);\\n      return input;\\n    },\\n    // Create a <progress>\\n    createProgress(type, attributes) {\\n      const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n        min: 0,\\n        max: 100,\\n        value: 0,\\n        role: 'progressbar',\\n        'aria-hidden': true\\n      }, attributes));\\n\\n      // Create the label inside\\n      if (type !== 'volume') {\\n        progress.appendChild(createElement('span', null, '0'));\\n        const suffixKey = {\\n          played: 'played',\\n          buffer: 'buffered'\\n        }[type];\\n        const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n        progress.innerText = `% ${suffix.toLowerCase()}`;\\n      }\\n      this.elements.display[type] = progress;\\n      return progress;\\n    },\\n    // Create time display\\n    createTime(type, attrs) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n      const container = createElement('div', extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer'\\n      }), '00:00');\\n\\n      // Reference for updates\\n      this.elements.display[type] = container;\\n      return container;\\n    },\\n    // Bind keyboard shortcuts for a menu item\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    bindMenuItemShortcuts(menuItem, type) {\\n      // Navigate through menus via arrow keys and space\\n      on.call(this, menuItem, 'keydown keyup', event => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n              target = menuItem.nextElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      }, false);\\n\\n      // Enter will fire a `click` event but we still need to manage focus\\n      // So we bind to keyup which fires after and set focus here\\n      on.call(this, menuItem, 'keyup', event => {\\n        if (event.key !== 'Return') return;\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      });\\n    },\\n    // Create a settings menu item\\n    createMenuItem({\\n      value,\\n      list,\\n      type,\\n      title,\\n      badge = null,\\n      checked = false\\n    }) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n      const menuItem = createElement('button', extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value\\n      }));\\n      const flex = createElement('span');\\n\\n      // We have to set as HTML incase of special characters\\n      flex.innerHTML = title;\\n      if (is.element(badge)) {\\n        flex.appendChild(badge);\\n      }\\n      menuItem.appendChild(flex);\\n\\n      // Replicate radio button behavior\\n      Object.defineProperty(menuItem, 'checked', {\\n        enumerable: true,\\n        get() {\\n          return menuItem.getAttribute('aria-checked') === 'true';\\n        },\\n        set(check) {\\n          // Ensure exclusivity\\n          if (check) {\\n            Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n          }\\n          menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n        }\\n      });\\n      this.listeners.bind(menuItem, 'click keyup', event => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n        event.preventDefault();\\n        event.stopPropagation();\\n        menuItem.checked = true;\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n        }\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      }, type, false);\\n      controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n      list.appendChild(menuItem);\\n    },\\n    // Format a time for display\\n    formatTime(time = 0, inverted = false) {\\n      // Bail if the value isn't a number\\n      if (!is.number(time)) {\\n        return time;\\n      }\\n\\n      // Always display hours if duration is over an hour\\n      const forceHours = getHours(this.duration) > 0;\\n      return formatTime(time, forceHours, inverted);\\n    },\\n    // Update the displayed time\\n    updateTimeDisplay(target = null, time = 0, inverted = false) {\\n      // Bail if there's no element to display or the value isn't a number\\n      if (!is.element(target) || !is.number(time)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line no-param-reassign\\n      target.innerText = controls.formatTime(time, inverted);\\n    },\\n    // Update volume UI and storage\\n    updateVolume() {\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Update range\\n      if (is.element(this.elements.inputs.volume)) {\\n        controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n      }\\n\\n      // Update mute state\\n      if (is.element(this.elements.buttons.mute)) {\\n        this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n      }\\n    },\\n    // Update seek value and lower fill\\n    setRange(target, value = 0) {\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line\\n      target.value = value;\\n\\n      // Webkit range fill\\n      controls.updateRangeFill.call(this, target);\\n    },\\n    // Update <progress> elements\\n    updateProgress(event) {\\n      if (!this.supported.ui || !is.event(event)) {\\n        return;\\n      }\\n      let value = 0;\\n      const setProgress = (target, input) => {\\n        const val = is.number(input) ? input : 0;\\n        const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n        // Update value and label\\n        if (is.element(progress)) {\\n          progress.value = val;\\n\\n          // Update text label inside\\n          const label = progress.getElementsByTagName('span')[0];\\n          if (is.element(label)) {\\n            label.childNodes[0].nodeValue = val;\\n          }\\n        }\\n      };\\n      if (event) {\\n        switch (event.type) {\\n          // Video playing\\n          case 'timeupdate':\\n          case 'seeking':\\n          case 'seeked':\\n            value = getPercentage(this.currentTime, this.duration);\\n\\n            // Set seek range value only if it's a 'natural' time event\\n            if (event.type === 'timeupdate') {\\n              controls.setRange.call(this, this.elements.inputs.seek, value);\\n            }\\n            break;\\n\\n          // Check buffer status\\n          case 'playing':\\n          case 'progress':\\n            setProgress(this.elements.display.buffer, this.buffered * 100);\\n            break;\\n        }\\n      }\\n    },\\n    // Webkit polyfill for lower fill range\\n    updateRangeFill(target) {\\n      // Get range from event if event passed\\n      const range = is.event(target) ? target.target : target;\\n\\n      // Needs to be a valid <input type='range'>\\n      if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n        return;\\n      }\\n\\n      // Set aria values for https://github.com/sampotts/plyr/issues/905\\n      if (matches(range, this.config.selectors.inputs.seek)) {\\n        range.setAttribute('aria-valuenow', this.currentTime);\\n        const currentTime = controls.formatTime(this.currentTime);\\n        const duration = controls.formatTime(this.duration);\\n        const format = i18n.get('seekLabel', this.config);\\n        range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n      } else if (matches(range, this.config.selectors.inputs.volume)) {\\n        const percent = range.value * 100;\\n        range.setAttribute('aria-valuenow', percent);\\n        range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n      } else {\\n        range.setAttribute('aria-valuenow', range.value);\\n      }\\n\\n      // WebKit only\\n      if (!browser.isWebKit && !browser.isIPadOS) {\\n        return;\\n      }\\n\\n      // Set CSS custom property\\n      range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n    },\\n    // Update hover tooltip for seeking\\n    updateSeekTooltip(event) {\\n      var _this$config$markers, _this$config$markers$;\\n      // Bail if setting not true\\n      if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n        return;\\n      }\\n      const tipElement = this.elements.display.seekTooltip;\\n      const visible = `${this.config.classNames.tooltip}--visible`;\\n      const toggle = show => toggleClass(tipElement, visible, show);\\n\\n      // Hide on touch\\n      if (this.touch) {\\n        toggle(false);\\n        return;\\n      }\\n\\n      // Determine percentage, if already visible\\n      let percent = 0;\\n      const clientRect = this.elements.progress.getBoundingClientRect();\\n      if (is.event(event)) {\\n        percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n      } else if (hasClass(tipElement, visible)) {\\n        percent = parseFloat(tipElement.style.left, 10);\\n      } else {\\n        return;\\n      }\\n\\n      // Set bounds\\n      if (percent < 0) {\\n        percent = 0;\\n      } else if (percent > 100) {\\n        percent = 100;\\n      }\\n      const time = this.duration / 100 * percent;\\n\\n      // Display the time a click would seek to\\n      tipElement.innerText = controls.formatTime(time);\\n\\n      // Get marker point for time\\n      const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n        time: t\\n      }) => t === Math.round(time));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n\\n      // Set position\\n      tipElement.style.left = `${percent}%`;\\n\\n      // Show/hide the tooltip\\n      // If the event is a moues in/out and percentage is inside bounds\\n      if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n        toggle(event.type === 'mouseenter');\\n      }\\n    },\\n    // Handle time change event\\n    timeUpdate(event) {\\n      // Only invert if only one time element is displayed and used for both duration and currentTime\\n      const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n      // Duration\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n      // Ignore updates while seeking\\n      if (event && event.type === 'timeupdate' && this.media.seeking) {\\n        return;\\n      }\\n\\n      // Playing progress\\n      controls.updateProgress.call(this, event);\\n    },\\n    // Show the duration on metadataloaded or durationchange events\\n    durationUpdate() {\\n      // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n      if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n        return;\\n      }\\n\\n      // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n      // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n      // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n      // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n      if (this.duration >= 2 ** 32) {\\n        toggleHidden(this.elements.display.currentTime, true);\\n        toggleHidden(this.elements.progress, true);\\n        return;\\n      }\\n\\n      // Update ARIA values\\n      if (is.element(this.elements.inputs.seek)) {\\n        this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n      }\\n\\n      // If there's a spot to display duration\\n      const hasDuration = is.element(this.elements.display.duration);\\n\\n      // If there's only one time display, display duration there\\n      if (!hasDuration && this.config.displayDuration && this.paused) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n      }\\n\\n      // If there's a duration element, update content\\n      if (hasDuration) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n      }\\n      if (this.config.markers.enabled) {\\n        controls.setMarkers.call(this);\\n      }\\n\\n      // Update the tooltip (if visible)\\n      controls.updateSeekTooltip.call(this);\\n    },\\n    // Hide/show a tab\\n    toggleMenuButton(setting, toggle) {\\n      toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n    },\\n    // Update the selected setting\\n    updateSetting(setting, container, input) {\\n      const pane = this.elements.settings.panels[setting];\\n      let value = null;\\n      let list = container;\\n      if (setting === 'captions') {\\n        value = this.currentTrack;\\n      } else {\\n        value = !is.empty(input) ? input : this[setting];\\n\\n        // Get default\\n        if (is.empty(value)) {\\n          value = this.config[setting].default;\\n        }\\n\\n        // Unsupported value\\n        if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n          this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n          return;\\n        }\\n\\n        // Disabled value\\n        if (!this.config[setting].options.includes(value)) {\\n          this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n          return;\\n        }\\n      }\\n\\n      // Get the list if we need to\\n      if (!is.element(list)) {\\n        list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n      }\\n\\n      // If there's no list it means it's not been rendered...\\n      if (!is.element(list)) {\\n        return;\\n      }\\n\\n      // Update the label\\n      const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n      label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n      // Find the radio option and check it\\n      const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n      if (is.element(target)) {\\n        target.checked = true;\\n      }\\n    },\\n    // Translate a value into a nice label\\n    getLabel(setting, value) {\\n      switch (setting) {\\n        case 'speed':\\n          return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n        case 'quality':\\n          if (is.number(value)) {\\n            const label = i18n.get(`qualityLabel.${value}`, this.config);\\n            if (!label.length) {\\n              return `${value}p`;\\n            }\\n            return label;\\n          }\\n          return toTitleCase(value);\\n        case 'captions':\\n          return captions.getLabel.call(this);\\n        default:\\n          return null;\\n      }\\n    },\\n    // Set the quality menu\\n    setQualityMenu(options) {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.quality)) {\\n        return;\\n      }\\n      const type = 'quality';\\n      const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Set options if passed and filter based on uniqueness and config\\n      if (is.array(options)) {\\n        this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n      }\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Get the badge HTML for HD, 4K etc\\n      const getBadge = quality => {\\n        const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n        if (!label.length) {\\n          return null;\\n        }\\n        return controls.createBadge.call(this, label);\\n      };\\n\\n      // Sort options by the config and then render options\\n      this.options.quality.sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      }).forEach(quality => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set the looping options\\n    /* setLoopMenu() {\\n          // Menu required\\n          if (!is.element(this.elements.settings.panels.loop)) {\\n              return;\\n          }\\n           const options = ['start', 'end', 'all', 'reset'];\\n          const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n           // Show the pane and tab\\n          toggleHidden(this.elements.settings.buttons.loop, false);\\n          toggleHidden(this.elements.settings.panels.loop, false);\\n           // Toggle the pane and tab\\n          const toggle = !is.empty(this.loop.options);\\n          controls.toggleMenuButton.call(this, 'loop', toggle);\\n           // Empty the menu\\n          emptyElement(list);\\n           options.forEach(option => {\\n              const item = createElement('li');\\n               const button = createElement(\\n                  'button',\\n                  extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                      type: 'button',\\n                      class: this.config.classNames.control,\\n                      'data-plyr-loop-action': option,\\n                  }),\\n                  i18n.get(option, this.config)\\n              );\\n               if (['start', 'end'].includes(option)) {\\n                  const badge = controls.createBadge.call(this, '00:00');\\n                  button.appendChild(badge);\\n              }\\n               item.appendChild(button);\\n              list.appendChild(item);\\n          });\\n      }, */\\n\\n    // Get current selected caption language\\n    // TODO: rework this to user the getter in the API?\\n\\n    // Set a list of available captions languages\\n    setCaptionsMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.captions)) {\\n        return;\\n      }\\n\\n      // TODO: Captions or language? Currently it's mixed\\n      const type = 'captions';\\n      const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n      const tracks = captions.getTracks.call(this);\\n      const toggle = Boolean(tracks.length);\\n\\n      // Toggle the pane and tab\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If there's no captions, bail\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Generate options data\\n      const options = tracks.map((track, value) => ({\\n        value,\\n        checked: this.captions.toggled && this.currentTrack === value,\\n        title: captions.getLabel.call(this, track),\\n        badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n        list,\\n        type: 'language'\\n      }));\\n\\n      // Add the \\\"Disabled\\\" option to turn off captions\\n      options.unshift({\\n        value: -1,\\n        checked: !this.captions.toggled,\\n        title: i18n.get('disabled', this.config),\\n        list,\\n        type: 'language'\\n      });\\n\\n      // Generate options\\n      options.forEach(controls.createMenuItem.bind(this));\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set a list of available captions languages\\n    setSpeedMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.speed)) {\\n        return;\\n      }\\n      const type = 'speed';\\n      const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Filter out invalid speeds\\n      this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Create items\\n      this.options.speed.forEach(speed => {\\n        controls.createMenuItem.call(this, {\\n          value: speed,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'speed', speed)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Check if we need to hide/show the settings menu\\n    checkMenu() {\\n      const {\\n        buttons\\n      } = this.elements.settings;\\n      const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n      toggleHidden(this.elements.settings.menu, !visible);\\n    },\\n    // Focus the first menu item in a given (or visible) menu\\n    focusFirstMenuItem(pane, focusVisible = false) {\\n      if (this.elements.settings.popup.hidden) {\\n        return;\\n      }\\n      let target = pane;\\n      if (!is.element(target)) {\\n        target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n      }\\n      const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n      setFocus.call(this, firstItem, focusVisible);\\n    },\\n    // Show/hide menu\\n    toggleMenu(input) {\\n      const {\\n        popup\\n      } = this.elements.settings;\\n      const button = this.elements.buttons.settings;\\n\\n      // Menu and button are required\\n      if (!is.element(popup) || !is.element(button)) {\\n        return;\\n      }\\n\\n      // True toggle by default\\n      const {\\n        hidden\\n      } = popup;\\n      let show = hidden;\\n      if (is.boolean(input)) {\\n        show = input;\\n      } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n        show = false;\\n      } else if (is.event(input)) {\\n        // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n        // Element in the shadowDOM. The path, if available, is complete.\\n        const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n        const isMenuItem = popup.contains(target);\\n\\n        // If the click was inside the menu or if the click\\n        // wasn't the button or menu item and we're trying to\\n        // show the menu (a doc click shouldn't show the menu)\\n        if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n          return;\\n        }\\n      }\\n\\n      // Set button attributes\\n      button.setAttribute('aria-expanded', show);\\n\\n      // Show the actual popup\\n      toggleHidden(popup, !show);\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n      // Focus the first item if key interaction\\n      if (show && is.keyboardEvent(input)) {\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      } else if (!show && !hidden) {\\n        // If closing, re-focus the button\\n        setFocus.call(this, button, is.keyboardEvent(input));\\n      }\\n    },\\n    // Get the natural size of a menu panel\\n    getMenuSize(tab) {\\n      const clone = tab.cloneNode(true);\\n      clone.style.position = 'absolute';\\n      clone.style.opacity = 0;\\n      clone.removeAttribute('hidden');\\n\\n      // Append to parent so we get the \\\"real\\\" size\\n      tab.parentNode.appendChild(clone);\\n\\n      // Get the sizes before we remove\\n      const width = clone.scrollWidth;\\n      const height = clone.scrollHeight;\\n\\n      // Remove from the DOM\\n      removeElement(clone);\\n      return {\\n        width,\\n        height\\n      };\\n    },\\n    // Show a panel in the menu\\n    showMenuPanel(type = '', focusVisible = false) {\\n      const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n      // Nothing to show, bail\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // Hide all other panels\\n      const container = target.parentNode;\\n      const current = Array.from(container.children).find(node => !node.hidden);\\n\\n      // If we can do fancy animations, we'll animate the height/width\\n      if (support.transitions && !support.reducedMotion) {\\n        // Set the current width as a base\\n        container.style.width = `${current.scrollWidth}px`;\\n        container.style.height = `${current.scrollHeight}px`;\\n\\n        // Get potential sizes\\n        const size = controls.getMenuSize.call(this, target);\\n\\n        // Restore auto height/width\\n        const restore = event => {\\n          // We're only bothered about height and width on the container\\n          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n            return;\\n          }\\n\\n          // Revert back to auto\\n          container.style.width = '';\\n          container.style.height = '';\\n\\n          // Only listen once\\n          off.call(this, container, transitionEndEvent, restore);\\n        };\\n\\n        // Listen for the transition finishing and restore auto height/width\\n        on.call(this, container, transitionEndEvent, restore);\\n\\n        // Set dimensions to target\\n        container.style.width = `${size.width}px`;\\n        container.style.height = `${size.height}px`;\\n      }\\n\\n      // Set attributes on current tab\\n      toggleHidden(current, true);\\n\\n      // Set attributes on target\\n      toggleHidden(target, false);\\n\\n      // Focus the first item\\n      controls.focusFirstMenuItem.call(this, target, focusVisible);\\n    },\\n    // Set the download URL\\n    setDownloadUrl() {\\n      const button = this.elements.buttons.download;\\n\\n      // Bail if no button\\n      if (!is.element(button)) {\\n        return;\\n      }\\n\\n      // Set attribute\\n      button.setAttribute('href', this.download);\\n    },\\n    // Build the default HTML\\n    create(data) {\\n      const {\\n        bindMenuItemShortcuts,\\n        createButton,\\n        createProgress,\\n        createRange,\\n        createTime,\\n        setQualityMenu,\\n        setSpeedMenu,\\n        showMenuPanel\\n      } = controls;\\n      this.elements.controls = null;\\n\\n      // Larger overlaid play button\\n      if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n        this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n      }\\n\\n      // Create the container\\n      const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n      this.elements.controls = container;\\n\\n      // Default item attributes\\n      const defaultAttributes = {\\n        class: 'plyr__controls__item'\\n      };\\n\\n      // Loop through controls in order\\n      dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n        // Restart button\\n        if (control === 'restart') {\\n          container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n        }\\n\\n        // Rewind button\\n        if (control === 'rewind') {\\n          container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n        }\\n\\n        // Play/Pause button\\n        if (control === 'play') {\\n          container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n        }\\n\\n        // Fast forward button\\n        if (control === 'fast-forward') {\\n          container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n        }\\n\\n        // Progress\\n        if (control === 'progress') {\\n          const progressContainer = createElement('div', {\\n            class: `${defaultAttributes.class} plyr__progress__container`\\n          });\\n          const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n          // Seek range slider\\n          progress.appendChild(createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`\\n          }));\\n\\n          // Buffer progress\\n          progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n          // TODO: Add loop display indicator\\n\\n          // Seek tooltip\\n          if (this.config.tooltips.seek) {\\n            const tooltip = createElement('span', {\\n              class: this.config.classNames.tooltip\\n            }, '00:00');\\n            progress.appendChild(tooltip);\\n            this.elements.display.seekTooltip = tooltip;\\n          }\\n          this.elements.progress = progress;\\n          progressContainer.appendChild(this.elements.progress);\\n          container.appendChild(progressContainer);\\n        }\\n\\n        // Media current time display\\n        if (control === 'current-time') {\\n          container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n        }\\n\\n        // Media duration display\\n        if (control === 'duration') {\\n          container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n        }\\n\\n        // Volume controls\\n        if (control === 'mute' || control === 'volume') {\\n          let {\\n            volume\\n          } = this.elements;\\n\\n          // Create the volume container if needed\\n          if (!is.element(volume) || !container.contains(volume)) {\\n            volume = createElement('div', extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim()\\n            }));\\n            this.elements.volume = volume;\\n            container.appendChild(volume);\\n          }\\n\\n          // Toggle mute button\\n          if (control === 'mute') {\\n            volume.appendChild(createButton.call(this, 'mute'));\\n          }\\n\\n          // Volume range control\\n          // Ignored on iOS as it's handled globally\\n          // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n          if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n            // Set the attributes\\n            const attributes = {\\n              max: 1,\\n              step: 0.05,\\n              value: this.config.volume\\n            };\\n\\n            // Create the volume range slider\\n            volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n              id: `plyr-volume-${data.id}`\\n            })));\\n          }\\n        }\\n\\n        // Toggle captions button\\n        if (control === 'captions') {\\n          container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n        }\\n\\n        // Settings button / menu\\n        if (control === 'settings' && !is.empty(this.config.settings)) {\\n          const wrapper = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: ''\\n          }));\\n          wrapper.appendChild(createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false\\n          }));\\n          const popup = createElement('div', {\\n            class: 'plyr__menu__container',\\n            id: `plyr-settings-${data.id}`,\\n            hidden: ''\\n          });\\n          const inner = createElement('div');\\n          const home = createElement('div', {\\n            id: `plyr-settings-${data.id}-home`\\n          });\\n\\n          // Create the menu\\n          const menu = createElement('div', {\\n            role: 'menu'\\n          });\\n          home.appendChild(menu);\\n          inner.appendChild(home);\\n          this.elements.settings.panels.home = home;\\n\\n          // Build the menu items\\n          this.config.settings.forEach(type => {\\n            // TODO: bundle this with the createMenuItem helper and bindings\\n            const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: ''\\n            }));\\n\\n            // Bind menu shortcuts for keyboard users\\n            bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n            // Show menu on click\\n            on.call(this, menuItem, 'click', () => {\\n              showMenuPanel.call(this, type, false);\\n            });\\n            const flex = createElement('span', null, i18n.get(type, this.config));\\n            const value = createElement('span', {\\n              class: this.config.classNames.menu.value\\n            });\\n\\n            // Speed contains HTML entities\\n            value.innerHTML = data[type];\\n            flex.appendChild(value);\\n            menuItem.appendChild(flex);\\n            menu.appendChild(menuItem);\\n\\n            // Build the panes\\n            const pane = createElement('div', {\\n              id: `plyr-settings-${data.id}-${type}`,\\n              hidden: ''\\n            });\\n\\n            // Back button\\n            const backButton = createElement('button', {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n            });\\n\\n            // Visible label\\n            backButton.appendChild(createElement('span', {\\n              'aria-hidden': true\\n            }, i18n.get(type, this.config)));\\n\\n            // Screen reader label\\n            backButton.appendChild(createElement('span', {\\n              class: this.config.classNames.hidden\\n            }, i18n.get('menuBack', this.config)));\\n\\n            // Go back via keyboard\\n            on.call(this, pane, 'keydown', event => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            }, false);\\n\\n            // Go back via button click\\n            on.call(this, backButton, 'click', () => {\\n              showMenuPanel.call(this, 'home', false);\\n            });\\n\\n            // Add to pane\\n            pane.appendChild(backButton);\\n\\n            // Menu\\n            pane.appendChild(createElement('div', {\\n              role: 'menu'\\n            }));\\n            inner.appendChild(pane);\\n            this.elements.settings.buttons[type] = menuItem;\\n            this.elements.settings.panels[type] = pane;\\n          });\\n          popup.appendChild(inner);\\n          wrapper.appendChild(popup);\\n          container.appendChild(wrapper);\\n          this.elements.settings.popup = popup;\\n          this.elements.settings.menu = wrapper;\\n        }\\n\\n        // Picture in picture button\\n        if (control === 'pip' && support.pip) {\\n          container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n        }\\n\\n        // Airplay button\\n        if (control === 'airplay' && support.airplay) {\\n          container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n        }\\n\\n        // Download button\\n        if (control === 'download') {\\n          const attributes = extend({}, defaultAttributes, {\\n            element: 'a',\\n            href: this.download,\\n            target: '_blank'\\n          });\\n\\n          // Set download attribute for HTML5 only\\n          if (this.isHTML5) {\\n            attributes.download = '';\\n          }\\n          const {\\n            download\\n          } = this.config.urls;\\n          if (!is.url(download) && this.isEmbed) {\\n            extend(attributes, {\\n              icon: `logo-${this.provider}`,\\n              label: this.provider\\n            });\\n          }\\n          container.appendChild(createButton.call(this, 'download', attributes));\\n        }\\n\\n        // Toggle fullscreen button\\n        if (control === 'fullscreen') {\\n          container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n        }\\n      });\\n\\n      // Set available quality levels\\n      if (this.isHTML5) {\\n        setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n      }\\n      setSpeedMenu.call(this);\\n      return container;\\n    },\\n    // Insert controls\\n    inject() {\\n      // Sprite\\n      if (this.config.loadSprite) {\\n        const icon = controls.getIconUrl.call(this);\\n\\n        // Only load external sprite using AJAX\\n        if (icon.cors) {\\n          loadSprite(icon.url, 'sprite-plyr');\\n        }\\n      }\\n\\n      // Create a unique ID\\n      this.id = Math.floor(Math.random() * 10000);\\n\\n      // Null by default\\n      let container = null;\\n      this.elements.controls = null;\\n\\n      // Set template properties\\n      const props = {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        title: this.config.title\\n      };\\n      let update = true;\\n\\n      // If function, run it and use output\\n      if (is.function(this.config.controls)) {\\n        this.config.controls = this.config.controls.call(this, props);\\n      }\\n\\n      // Convert falsy controls to empty array (primarily for empty strings)\\n      if (!this.config.controls) {\\n        this.config.controls = [];\\n      }\\n      if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n        // HTMLElement or Non-empty string passed as the option\\n        container = this.config.controls;\\n      } else {\\n        // Create controls\\n        container = controls.create.call(this, {\\n          id: this.id,\\n          seektime: this.config.seekTime,\\n          speed: this.speed,\\n          quality: this.quality,\\n          captions: captions.getLabel.call(this)\\n          // TODO: Looping\\n          // loop: 'None',\\n        });\\n\\n        update = false;\\n      }\\n\\n      // Replace props with their value\\n      const replace = input => {\\n        let result = input;\\n        Object.entries(props).forEach(([key, value]) => {\\n          result = replaceAll(result, `{${key}}`, value);\\n        });\\n        return result;\\n      };\\n\\n      // Update markup\\n      if (update) {\\n        if (is.string(this.config.controls)) {\\n          container = replace(container);\\n        }\\n      }\\n\\n      // Controls container\\n      let target;\\n\\n      // Inject to custom location\\n      if (is.string(this.config.selectors.controls.container)) {\\n        target = document.querySelector(this.config.selectors.controls.container);\\n      }\\n\\n      // Inject into the container by default\\n      if (!is.element(target)) {\\n        target = this.elements.container;\\n      }\\n\\n      // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n      const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n      target[insertMethod]('afterbegin', container);\\n\\n      // Find the elements if need be\\n      if (!is.element(this.elements.controls)) {\\n        controls.findElements.call(this);\\n      }\\n\\n      // Add pressed property to buttons\\n      if (!is.empty(this.elements.buttons)) {\\n        const addProperty = button => {\\n          const className = this.config.classNames.controlPressed;\\n          button.setAttribute('aria-pressed', 'false');\\n          Object.defineProperty(button, 'pressed', {\\n            configurable: true,\\n            enumerable: true,\\n            get() {\\n              return hasClass(button, className);\\n            },\\n            set(pressed = false) {\\n              toggleClass(button, className, pressed);\\n              button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n            }\\n          });\\n        };\\n\\n        // Toggle classname when pressed property is set\\n        Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n      }\\n\\n      // Edge sometimes doesn't finish the paint so force a repaint\\n      if (browser.isEdge) {\\n        repaint(target);\\n      }\\n\\n      // Setup tooltips\\n      if (this.config.tooltips.controls) {\\n        const {\\n          classNames,\\n          selectors\\n        } = this.config;\\n        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n        const labels = getElements.call(this, selector);\\n        Array.from(labels).forEach(label => {\\n          toggleClass(label, this.config.classNames.hidden, false);\\n          toggleClass(label, this.config.classNames.tooltip, true);\\n        });\\n      }\\n    },\\n    // Set media metadata\\n    setMediaMetadata() {\\n      try {\\n        if ('mediaSession' in navigator) {\\n          navigator.mediaSession.metadata = new window.MediaMetadata({\\n            title: this.config.mediaMetadata.title,\\n            artist: this.config.mediaMetadata.artist,\\n            album: this.config.mediaMetadata.album,\\n            artwork: this.config.mediaMetadata.artwork\\n          });\\n        }\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    },\\n    // Add markers\\n    setMarkers() {\\n      var _this$config$markers2, _this$config$markers3;\\n      if (!this.duration || this.elements.markers) return;\\n\\n      // Get valid points\\n      const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n        time\\n      }) => time > 0 && time < this.duration);\\n      if (!(points !== null && points !== void 0 && points.length)) return;\\n      const containerFragment = document.createDocumentFragment();\\n      const pointsFragment = document.createDocumentFragment();\\n      let tipElement = null;\\n      const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n      const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n      // Inject markers to progress container\\n      points.forEach(point => {\\n        const markerElement = createElement('span', {\\n          class: this.config.classNames.marker\\n        }, '');\\n        const left = `${point.time / this.duration * 100}%`;\\n        if (tipElement) {\\n          // Show on hover\\n          markerElement.addEventListener('mouseenter', () => {\\n            if (point.label) return;\\n            tipElement.style.left = left;\\n            tipElement.innerHTML = point.label;\\n            toggleTip(true);\\n          });\\n\\n          // Hide on leave\\n          markerElement.addEventListener('mouseleave', () => {\\n            toggleTip(false);\\n          });\\n        }\\n        markerElement.addEventListener('click', () => {\\n          this.currentTime = point.time;\\n        });\\n        markerElement.style.left = left;\\n        pointsFragment.appendChild(markerElement);\\n      });\\n      containerFragment.appendChild(pointsFragment);\\n\\n      // Inject a tooltip if needed\\n      if (!this.config.tooltips.seek) {\\n        tipElement = createElement('span', {\\n          class: this.config.classNames.tooltip\\n        }, '');\\n        containerFragment.appendChild(tipElement);\\n      }\\n      this.elements.markers = {\\n        points: pointsFragment,\\n        tip: tipElement\\n      };\\n      this.elements.progress.appendChild(containerFragment);\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  /**\\n   * Parse a string to a URL object\\n   * @param {String} input - the URL to be parsed\\n   * @param {Boolean} safe - failsafe parsing\\n   */\\n  function parseUrl(input, safe = true) {\\n    let url = input;\\n    if (safe) {\\n      const parser = document.createElement('a');\\n      parser.href = url;\\n      url = parser.href;\\n    }\\n    try {\\n      return new URL(url);\\n    } catch (_) {\\n      return null;\\n    }\\n  }\\n\\n  // Convert object to URLSearchParams\\n  function buildUrlParams(input) {\\n    const params = new URLSearchParams();\\n    if (is.object(input)) {\\n      Object.entries(input).forEach(([key, value]) => {\\n        params.set(key, value);\\n      });\\n    }\\n    return params;\\n  }\\n\\n  // ==========================================================================\\n  const captions = {\\n    // Setup captions\\n    setup() {\\n      // Requires UI support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Only Vimeo and HTML5 video supported at this point\\n      if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n        // Clear menu and hide\\n        if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n          controls.setCaptionsMenu.call(this);\\n        }\\n        return;\\n      }\\n\\n      // Inject the container\\n      if (!is.element(this.elements.captions)) {\\n        this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n        this.elements.captions.setAttribute('dir', 'auto');\\n        insertAfter(this.elements.captions, this.elements.wrapper);\\n      }\\n\\n      // Fix IE captions if CORS is used\\n      // Fetch captions and inject as blobs instead (data URIs not supported!)\\n      if (browser.isIE && window.URL) {\\n        const elements = this.media.querySelectorAll('track');\\n        Array.from(elements).forEach(track => {\\n          const src = track.getAttribute('src');\\n          const url = parseUrl(src);\\n          if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n            fetch(src, 'blob').then(blob => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            }).catch(() => {\\n              removeElement(track);\\n            });\\n          }\\n        });\\n      }\\n\\n      // Get and set initial data\\n      // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n      // * languages: Array of user's browser languages.\\n      // * language:  The language preferred by user settings or config\\n      // * active:    The state preferred by user settings or config\\n      // * toggled:   The real captions state\\n\\n      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n      const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n      let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n      // Use first browser language when language is 'auto'\\n      if (language === 'auto') {\\n        [language] = languages;\\n      }\\n      let active = this.storage.get('captions');\\n      if (!is.boolean(active)) {\\n        ({\\n          active\\n        } = this.config.captions);\\n      }\\n      Object.assign(this.captions, {\\n        toggled: false,\\n        active,\\n        language,\\n        languages\\n      });\\n\\n      // Watch changes to textTracks and update captions menu\\n      if (this.isHTML5) {\\n        const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n        on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n      }\\n\\n      // Update available languages in list next tick (the event must not be triggered before the listeners)\\n      setTimeout(captions.update.bind(this), 0);\\n    },\\n    // Update available language options in settings based on tracks\\n    update() {\\n      const tracks = captions.getTracks.call(this, true);\\n      // Get the wanted language\\n      const {\\n        active,\\n        language,\\n        meta,\\n        currentTrackNode\\n      } = this.captions;\\n      const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n      // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n      if (this.isHTML5 && this.isVideo) {\\n        tracks.filter(track => !meta.get(track)).forEach(track => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing'\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n      }\\n\\n      // Update language first time it matches, or if the previous matching track was removed\\n      if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n        captions.setLanguage.call(this, language);\\n        captions.toggle.call(this, active && languageExists);\\n      }\\n\\n      // Enable or disable captions based on track length\\n      if (this.elements) {\\n        toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n      }\\n\\n      // Update available languages in list\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n    },\\n    // Toggle captions display\\n    // Used internally for the toggleCaptions method, with the passive option forced to false\\n    toggle(input, passive = true) {\\n      // If there's no full support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      const {\\n        toggled\\n      } = this.captions; // Current state\\n      const activeClass = this.config.classNames.captions.active;\\n      // Get the next state\\n      // If the method is called without parameter, toggle based on current value\\n      const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n      // Update state and trigger event\\n      if (active !== toggled) {\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.active = active;\\n          this.storage.set({\\n            captions: active\\n          });\\n        }\\n\\n        // Force language if the call isn't passive and there is no matching language to toggle to\\n        if (!this.language && active && !passive) {\\n          const tracks = captions.getTracks.call(this);\\n          const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n          // Override user preferences to avoid switching languages if a matching track is added\\n          this.captions.language = track.language;\\n\\n          // Set caption, but don't store in localStorage as user preference\\n          captions.set.call(this, tracks.indexOf(track));\\n          return;\\n        }\\n\\n        // Toggle button if it's enabled\\n        if (this.elements.buttons.captions) {\\n          this.elements.buttons.captions.pressed = active;\\n        }\\n\\n        // Add class hook\\n        toggleClass(this.elements.container, activeClass, active);\\n        this.captions.toggled = active;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // Trigger event (not used internally)\\n        triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n      }\\n\\n      // Wait for the call stack to clear before setting mode='hidden'\\n      // on the active track - forcing the browser to download it\\n      setTimeout(() => {\\n        if (active && this.captions.toggled) {\\n          this.captions.currentTrackNode.mode = 'hidden';\\n        }\\n      });\\n    },\\n    // Set captions by track index\\n    // Used internally for the currentTrack setter with the passive option forced to false\\n    set(index, passive = true) {\\n      const tracks = captions.getTracks.call(this);\\n\\n      // Disable captions if setting to -1\\n      if (index === -1) {\\n        captions.toggle.call(this, false, passive);\\n        return;\\n      }\\n      if (!is.number(index)) {\\n        this.debug.warn('Invalid caption argument', index);\\n        return;\\n      }\\n      if (!(index in tracks)) {\\n        this.debug.warn('Track not found', index);\\n        return;\\n      }\\n      if (this.captions.currentTrack !== index) {\\n        this.captions.currentTrack = index;\\n        const track = tracks[index];\\n        const {\\n          language\\n        } = track || {};\\n\\n        // Store reference to node for invalidation on remove\\n        this.captions.currentTrackNode = track;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.language = language;\\n          this.storage.set({\\n            language\\n          });\\n        }\\n\\n        // Handle Vimeo captions\\n        if (this.isVimeo) {\\n          this.embed.enableTextTrack(language);\\n        }\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'languagechange');\\n      }\\n\\n      // Show captions\\n      captions.toggle.call(this, true, passive);\\n      if (this.isHTML5 && this.isVideo) {\\n        // If we change the active track while a cue is already displayed we need to update it\\n        captions.updateCues.call(this);\\n      }\\n    },\\n    // Set captions by language\\n    // Used internally for the language setter with the passive option forced to false\\n    setLanguage(input, passive = true) {\\n      if (!is.string(input)) {\\n        this.debug.warn('Invalid language argument', input);\\n        return;\\n      }\\n      // Normalize\\n      const language = input.toLowerCase();\\n      this.captions.language = language;\\n\\n      // Set currentTrack\\n      const tracks = captions.getTracks.call(this);\\n      const track = captions.findTrack.call(this, [language]);\\n      captions.set.call(this, tracks.indexOf(track), passive);\\n    },\\n    // Get current valid caption tracks\\n    // If update is false it will also ignore tracks without metadata\\n    // This is used to \\\"freeze\\\" the language options when captions.update is false\\n    getTracks(update = false) {\\n      // Handle media or textTracks missing or null\\n      const tracks = Array.from((this.media || {}).textTracks || []);\\n      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n      return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n    },\\n    // Match tracks based on languages and get the first\\n    findTrack(languages, force = false) {\\n      const tracks = captions.getTracks.call(this);\\n      const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n      let track;\\n      languages.every(language => {\\n        track = sorted.find(t => t.language === language);\\n        return !track; // Break iteration if there is a match\\n      });\\n\\n      // If no match is found but is required, get first\\n      return track || (force ? sorted[0] : undefined);\\n    },\\n    // Get the current track\\n    getCurrentTrack() {\\n      return captions.getTracks.call(this)[this.currentTrack];\\n    },\\n    // Get UI label for track\\n    getLabel(track) {\\n      let currentTrack = track;\\n      if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n        currentTrack = captions.getCurrentTrack.call(this);\\n      }\\n      if (is.track(currentTrack)) {\\n        if (!is.empty(currentTrack.label)) {\\n          return currentTrack.label;\\n        }\\n        if (!is.empty(currentTrack.language)) {\\n          return track.language.toUpperCase();\\n        }\\n        return i18n.get('enabled', this.config);\\n      }\\n      return i18n.get('disabled', this.config);\\n    },\\n    // Update captions using current track's active cues\\n    // Also optional array argument in case there isn't any track (ex: vimeo)\\n    updateCues(input) {\\n      // Requires UI\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      if (!is.element(this.elements.captions)) {\\n        this.debug.warn('No captions element to render to');\\n        return;\\n      }\\n\\n      // Only accept array or empty input\\n      if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n        this.debug.warn('updateCues: Invalid input', input);\\n        return;\\n      }\\n      let cues = input;\\n\\n      // Get cues from track\\n      if (!cues) {\\n        const track = captions.getCurrentTrack.call(this);\\n        cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n      }\\n\\n      // Set new caption text\\n      const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n      const changed = content !== this.elements.captions.innerHTML;\\n      if (changed) {\\n        // Empty the container and create a new child element\\n        emptyElement(this.elements.captions);\\n        const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n        caption.innerHTML = content;\\n        this.elements.captions.appendChild(caption);\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'cuechange');\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr default config\\n  // ==========================================================================\\n\\n  const defaults = {\\n    // Disable\\n    enabled: true,\\n    // Custom media title\\n    title: '',\\n    // Logging to console\\n    debug: false,\\n    // Auto play (if supported)\\n    autoplay: false,\\n    // Only allow one media playing at once (vimeo only)\\n    autopause: true,\\n    // Allow inline playback on iOS\\n    playsinline: true,\\n    // Default time to skip when rewind/fast forward\\n    seekTime: 10,\\n    // Default volume\\n    volume: 1,\\n    muted: false,\\n    // Pass a custom duration\\n    duration: null,\\n    // Display the media duration on load in the current time position\\n    // If you have opted to display both duration and currentTime, this is ignored\\n    displayDuration: true,\\n    // Invert the current time to be a countdown\\n    invertTime: true,\\n    // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n    toggleInvert: true,\\n    // Force an aspect ratio\\n    // The format must be `'w:h'` (e.g. `'16:9'`)\\n    ratio: null,\\n    // Click video container to play/pause\\n    clickToPlay: true,\\n    // Auto hide the controls\\n    hideControls: true,\\n    // Reset to start when playback ended\\n    resetOnEnd: false,\\n    // Disable the standard context menu\\n    disableContextMenu: true,\\n    // Sprite (for icons)\\n    loadSprite: true,\\n    iconPrefix: 'plyr',\\n    iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n    // Blank video (used to prevent errors on source change)\\n    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n    // Quality default\\n    quality: {\\n      default: 576,\\n      // The options to display in the UI, if available for the source media\\n      options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n      forced: false,\\n      onChange: null\\n    },\\n    // Set loops\\n    loop: {\\n      active: false\\n      // start: null,\\n      // end: null,\\n    },\\n\\n    // Speed default and options to display\\n    speed: {\\n      selected: 1,\\n      // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n      options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n    },\\n    // Keyboard shortcut settings\\n    keyboard: {\\n      focused: true,\\n      global: false\\n    },\\n    // Display tooltips\\n    tooltips: {\\n      controls: false,\\n      seek: true\\n    },\\n    // Captions settings\\n    captions: {\\n      active: false,\\n      language: 'auto',\\n      // Listen to new tracks added after Plyr is initialized.\\n      // This is needed for streaming captions, but may result in unselectable options\\n      update: false\\n    },\\n    // Fullscreen settings\\n    fullscreen: {\\n      enabled: true,\\n      // Allow fullscreen?\\n      fallback: true,\\n      // Fallback using full viewport/window\\n      iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n      // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n      // Non-ancestors of the player element will be ignored\\n      // container: null, // defaults to the player element\\n    },\\n\\n    // Local storage\\n    storage: {\\n      enabled: true,\\n      key: 'plyr'\\n    },\\n    // Default controls\\n    controls: ['play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress', 'current-time',\\n    // 'duration',\\n    'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n    // 'download',\\n    'fullscreen'],\\n    settings: ['captions', 'quality', 'speed'],\\n    // Localisation\\n    i18n: {\\n      restart: 'Restart',\\n      rewind: 'Rewind {seektime}s',\\n      play: 'Play',\\n      pause: 'Pause',\\n      fastForward: 'Forward {seektime}s',\\n      seek: 'Seek',\\n      seekLabel: '{currentTime} of {duration}',\\n      played: 'Played',\\n      buffered: 'Buffered',\\n      currentTime: 'Current time',\\n      duration: 'Duration',\\n      volume: 'Volume',\\n      mute: 'Mute',\\n      unmute: 'Unmute',\\n      enableCaptions: 'Enable captions',\\n      disableCaptions: 'Disable captions',\\n      download: 'Download',\\n      enterFullscreen: 'Enter fullscreen',\\n      exitFullscreen: 'Exit fullscreen',\\n      frameTitle: 'Player for {title}',\\n      captions: 'Captions',\\n      settings: 'Settings',\\n      pip: 'PIP',\\n      menuBack: 'Go back to previous menu',\\n      speed: 'Speed',\\n      normal: 'Normal',\\n      quality: 'Quality',\\n      loop: 'Loop',\\n      start: 'Start',\\n      end: 'End',\\n      all: 'All',\\n      reset: 'Reset',\\n      disabled: 'Disabled',\\n      enabled: 'Enabled',\\n      advertisement: 'Ad',\\n      qualityBadge: {\\n        2160: '4K',\\n        1440: 'HD',\\n        1080: 'HD',\\n        720: 'HD',\\n        576: 'SD',\\n        480: 'SD'\\n      }\\n    },\\n    // URLs\\n    urls: {\\n      download: null,\\n      vimeo: {\\n        sdk: 'https://player.vimeo.com/api/player.js',\\n        iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n        api: 'https://vimeo.com/api/oembed.json?url={0}'\\n      },\\n      youtube: {\\n        sdk: 'https://www.youtube.com/iframe_api',\\n        api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\\n      },\\n      googleIMA: {\\n        sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\\n      }\\n    },\\n    // Custom control listeners\\n    listeners: {\\n      seek: null,\\n      play: null,\\n      pause: null,\\n      restart: null,\\n      rewind: null,\\n      fastForward: null,\\n      mute: null,\\n      volume: null,\\n      captions: null,\\n      download: null,\\n      fullscreen: null,\\n      pip: null,\\n      airplay: null,\\n      speed: null,\\n      quality: null,\\n      loop: null,\\n      language: null\\n    },\\n    // Events to watch and bubble\\n    events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n    // Custom events\\n    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n    // YouTube\\n    'statechange',\\n    // Quality\\n    'qualitychange',\\n    // Ads\\n    'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n    // Selectors\\n    // Change these to match your template if using custom HTML\\n    selectors: {\\n      editable: 'input, textarea, select, [contenteditable]',\\n      container: '.plyr',\\n      controls: {\\n        container: null,\\n        wrapper: '.plyr__controls'\\n      },\\n      labels: '[data-plyr]',\\n      buttons: {\\n        play: '[data-plyr=\\\"play\\\"]',\\n        pause: '[data-plyr=\\\"pause\\\"]',\\n        restart: '[data-plyr=\\\"restart\\\"]',\\n        rewind: '[data-plyr=\\\"rewind\\\"]',\\n        fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n        mute: '[data-plyr=\\\"mute\\\"]',\\n        captions: '[data-plyr=\\\"captions\\\"]',\\n        download: '[data-plyr=\\\"download\\\"]',\\n        fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n        pip: '[data-plyr=\\\"pip\\\"]',\\n        airplay: '[data-plyr=\\\"airplay\\\"]',\\n        settings: '[data-plyr=\\\"settings\\\"]',\\n        loop: '[data-plyr=\\\"loop\\\"]'\\n      },\\n      inputs: {\\n        seek: '[data-plyr=\\\"seek\\\"]',\\n        volume: '[data-plyr=\\\"volume\\\"]',\\n        speed: '[data-plyr=\\\"speed\\\"]',\\n        language: '[data-plyr=\\\"language\\\"]',\\n        quality: '[data-plyr=\\\"quality\\\"]'\\n      },\\n      display: {\\n        currentTime: '.plyr__time--current',\\n        duration: '.plyr__time--duration',\\n        buffer: '.plyr__progress__buffer',\\n        loop: '.plyr__progress__loop',\\n        // Used later\\n        volume: '.plyr__volume--display'\\n      },\\n      progress: '.plyr__progress',\\n      captions: '.plyr__captions',\\n      caption: '.plyr__caption'\\n    },\\n    // Class hooks added to the player in different states\\n    classNames: {\\n      type: 'plyr--{0}',\\n      provider: 'plyr--{0}',\\n      video: 'plyr__video-wrapper',\\n      embed: 'plyr__video-embed',\\n      videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n      embedContainer: 'plyr__video-embed__container',\\n      poster: 'plyr__poster',\\n      posterEnabled: 'plyr__poster-enabled',\\n      ads: 'plyr__ads',\\n      control: 'plyr__control',\\n      controlPressed: 'plyr__control--pressed',\\n      playing: 'plyr--playing',\\n      paused: 'plyr--paused',\\n      stopped: 'plyr--stopped',\\n      loading: 'plyr--loading',\\n      hover: 'plyr--hover',\\n      tooltip: 'plyr__tooltip',\\n      cues: 'plyr__cues',\\n      marker: 'plyr__progress__marker',\\n      hidden: 'plyr__sr-only',\\n      hideControls: 'plyr--hide-controls',\\n      isTouch: 'plyr--is-touch',\\n      uiSupported: 'plyr--full-ui',\\n      noTransition: 'plyr--no-transition',\\n      display: {\\n        time: 'plyr__time'\\n      },\\n      menu: {\\n        value: 'plyr__menu__value',\\n        badge: 'plyr__badge',\\n        open: 'plyr--menu-open'\\n      },\\n      captions: {\\n        enabled: 'plyr--captions-enabled',\\n        active: 'plyr--captions-active'\\n      },\\n      fullscreen: {\\n        enabled: 'plyr--fullscreen-enabled',\\n        fallback: 'plyr--fullscreen-fallback'\\n      },\\n      pip: {\\n        supported: 'plyr--pip-supported',\\n        active: 'plyr--pip-active'\\n      },\\n      airplay: {\\n        supported: 'plyr--airplay-supported',\\n        active: 'plyr--airplay-active'\\n      },\\n      previewThumbnails: {\\n        // Tooltip thumbs\\n        thumbContainer: 'plyr__preview-thumb',\\n        thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n        imageContainer: 'plyr__preview-thumb__image-container',\\n        timeContainer: 'plyr__preview-thumb__time-container',\\n        // Scrubbing\\n        scrubbingContainer: 'plyr__preview-scrubbing',\\n        scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n      }\\n    },\\n    // Embed attributes\\n    attributes: {\\n      embed: {\\n        provider: 'data-plyr-provider',\\n        id: 'data-plyr-embed-id',\\n        hash: 'data-plyr-embed-hash'\\n      }\\n    },\\n    // Advertisements plugin\\n    // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n    ads: {\\n      enabled: false,\\n      publisherId: '',\\n      tagUrl: ''\\n    },\\n    // Preview Thumbnails plugin\\n    previewThumbnails: {\\n      enabled: false,\\n      src: ''\\n    },\\n    // Vimeo plugin\\n    vimeo: {\\n      byline: false,\\n      portrait: false,\\n      title: false,\\n      speed: true,\\n      transparent: false,\\n      // Custom settings from Plyr\\n      customControls: true,\\n      referrerPolicy: null,\\n      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n      // Whether the owner of the video has a Pro or Business account\\n      // (which allows us to properly hide controls without CSS hacks, etc)\\n      premium: false\\n    },\\n    // YouTube plugin\\n    youtube: {\\n      rel: 0,\\n      // No related vids\\n      showinfo: 0,\\n      // Hide info\\n      iv_load_policy: 3,\\n      // Hide annotations\\n      modestbranding: 1,\\n      // Hide logos as much as possible (they still show one in the corner when paused)\\n      // Custom settings from Plyr\\n      customControls: true,\\n      noCookie: false // Whether to use an alternative version of YouTube without cookies\\n    },\\n\\n    // Media Metadata\\n    mediaMetadata: {\\n      title: '',\\n      artist: '',\\n      album: '',\\n      artwork: []\\n    },\\n    // Markers\\n    markers: {\\n      enabled: false,\\n      points: []\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr states\\n  // ==========================================================================\\n\\n  const pip = {\\n    active: 'picture-in-picture',\\n    inactive: 'inline'\\n  };\\n\\n  // ==========================================================================\\n  // Plyr supported types and providers\\n  // ==========================================================================\\n\\n  const providers = {\\n    html5: 'html5',\\n    youtube: 'youtube',\\n    vimeo: 'vimeo'\\n  };\\n  const types = {\\n    audio: 'audio',\\n    video: 'video'\\n  };\\n\\n  /**\\n   * Get provider by URL\\n   * @param {String} url\\n   */\\n  function getProviderByUrl(url) {\\n    // YouTube\\n    if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n      return providers.youtube;\\n    }\\n\\n    // Vimeo\\n    if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n      return providers.vimeo;\\n    }\\n    return null;\\n  }\\n\\n  // ==========================================================================\\n  // Console wrapper\\n  // ==========================================================================\\n\\n  const noop = () => {};\\n  class Console {\\n    constructor(enabled = false) {\\n      this.enabled = window.console && enabled;\\n      if (this.enabled) {\\n        this.log('Debugging enabled');\\n      }\\n    }\\n    get log() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n    }\\n    get warn() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n    }\\n    get error() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n    }\\n  }\\n\\n  class Fullscreen {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"onChange\\\", () => {\\n        if (!this.supported) return;\\n\\n        // Update toggle button\\n        const button = this.player.elements.buttons.fullscreen;\\n        if (is.element(button)) {\\n          button.pressed = this.active;\\n        }\\n\\n        // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n        const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n        // Trigger an event\\n        triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n      });\\n      _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n        // Store or restore scroll position\\n        if (toggle) {\\n          this.scrollPosition = {\\n            x: window.scrollX ?? 0,\\n            y: window.scrollY ?? 0\\n          };\\n        } else {\\n          window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n        }\\n\\n        // Toggle scroll\\n        document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n        // Toggle class hook\\n        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n        // Force full viewport on iPhone X+\\n        if (browser.isIos) {\\n          let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n          const property = 'viewport-fit=cover';\\n\\n          // Inject the viewport meta if required\\n          if (!viewport) {\\n            viewport = document.createElement('meta');\\n            viewport.setAttribute('name', 'viewport');\\n          }\\n\\n          // Check if the property already exists\\n          const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n          if (toggle) {\\n            this.cleanupViewport = !hasProperty;\\n            if (!hasProperty) viewport.content += `,${property}`;\\n          } else if (this.cleanupViewport) {\\n            viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n          }\\n        }\\n\\n        // Toggle button and fire events\\n        this.onChange();\\n      });\\n      // Trap focus inside container\\n      _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n        // Bail if iOS/iPadOS, not active, not the tab key\\n        if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n        // Get the current focused element\\n        const focused = document.activeElement;\\n        const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n        const [first] = focusable;\\n        const last = focusable[focusable.length - 1];\\n        if (focused === last && !event.shiftKey) {\\n          // Move focus to first element that can be tabbed if Shift isn't used\\n          first.focus();\\n          event.preventDefault();\\n        } else if (focused === first && event.shiftKey) {\\n          // Move focus to last element that can be tabbed if Shift is used\\n          last.focus();\\n          event.preventDefault();\\n        }\\n      });\\n      // Update UI\\n      _defineProperty$1(this, \\\"update\\\", () => {\\n        if (this.supported) {\\n          let mode;\\n          if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n          this.player.debug.log(`${mode} fullscreen enabled`);\\n        } else {\\n          this.player.debug.log('Fullscreen not supported and fallback disabled');\\n        }\\n\\n        // Add styling hook to show button\\n        toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n      });\\n      // Make an element fullscreen\\n      _defineProperty$1(this, \\\"enter\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen doesn't need the request step\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.requestFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(true);\\n        } else if (!this.prefix) {\\n          this.target.requestFullscreen({\\n            navigationUI: 'hide'\\n          });\\n        } else if (!is.empty(this.prefix)) {\\n          this.target[`${this.prefix}Request${this.property}`]();\\n        }\\n      });\\n      // Bail from fullscreen\\n      _defineProperty$1(this, \\\"exit\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.exitFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n          silencePromise(this.player.play());\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(false);\\n        } else if (!this.prefix) {\\n          (document.cancelFullScreen || document.exitFullscreen).call(document);\\n        } else if (!is.empty(this.prefix)) {\\n          const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n          document[`${this.prefix}${action}${this.property}`]();\\n        }\\n      });\\n      // Toggle state\\n      _defineProperty$1(this, \\\"toggle\\\", () => {\\n        if (!this.active) this.enter();else this.exit();\\n      });\\n      // Keep reference to parent\\n      this.player = player;\\n\\n      // Get prefix\\n      this.prefix = Fullscreen.prefix;\\n      this.property = Fullscreen.property;\\n\\n      // Scroll position\\n      this.scrollPosition = {\\n        x: 0,\\n        y: 0\\n      };\\n\\n      // Force the use of 'full window/browser' rather than fullscreen\\n      this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n      // Get the fullscreen element\\n      // Checks container is an ancestor, defaults to null\\n      this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n      // Register event listeners\\n      // Handle event (incase user presses escape etc)\\n      on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      });\\n\\n      // Fullscreen toggle on double click\\n      on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n        // Ignore double click in controls\\n        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n          return;\\n        }\\n        this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n      });\\n\\n      // Tap focus when in fullscreen\\n      on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n      // Update the UI\\n      this.update();\\n    }\\n\\n    // Determine if native supported\\n    static get nativeSupported() {\\n      return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n    }\\n\\n    // If we're actually using native\\n    get useNative() {\\n      return Fullscreen.nativeSupported && !this.forceFallback;\\n    }\\n\\n    // Get the prefix for handlers\\n    static get prefix() {\\n      // No prefix\\n      if (is.function(document.exitFullscreen)) return '';\\n\\n      // Check for fullscreen support by vendor prefix\\n      let value = '';\\n      const prefixes = ['webkit', 'moz', 'ms'];\\n      prefixes.some(pre => {\\n        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n          value = pre;\\n          return true;\\n        }\\n        return false;\\n      });\\n      return value;\\n    }\\n    static get property() {\\n      return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n    }\\n\\n    // Determine if fullscreen is supported\\n    get supported() {\\n      return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n    }\\n\\n    // Get active state\\n    get active() {\\n      if (!this.supported) return false;\\n\\n      // Fallback using classname\\n      if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n      }\\n      const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n      return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n    }\\n\\n    // Get target element\\n    get target() {\\n      return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Load image avoiding xhr/fetch CORS issues\\n  // Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n  // By default it checks if it is at least 1px, but you can add a second argument to change this\\n  // ==========================================================================\\n\\n  function loadImage(src, minWidth = 1) {\\n    return new Promise((resolve, reject) => {\\n      const image = new Image();\\n      const handler = () => {\\n        delete image.onload;\\n        delete image.onerror;\\n        (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n      };\\n      Object.assign(image, {\\n        onload: handler,\\n        onerror: handler,\\n        src\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n  const ui = {\\n    addStyleHook() {\\n      toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n      toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n    },\\n    // Toggle native HTML5 media controls\\n    toggleNativeControls(toggle = false) {\\n      if (toggle && this.isHTML5) {\\n        this.media.setAttribute('controls', '');\\n      } else {\\n        this.media.removeAttribute('controls');\\n      }\\n    },\\n    // Setup the UI\\n    build() {\\n      // Re-attach media element listeners\\n      // TODO: Use event bubbling?\\n      this.listeners.media();\\n\\n      // Don't setup interface if no support\\n      if (!this.supported.ui) {\\n        this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n        // Restore native controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Bail\\n        return;\\n      }\\n\\n      // Inject custom controls if not present\\n      if (!is.element(this.elements.controls)) {\\n        // Inject custom controls\\n        controls.inject.call(this);\\n\\n        // Re-attach control listeners\\n        this.listeners.controls();\\n      }\\n\\n      // Remove native controls\\n      ui.toggleNativeControls.call(this);\\n\\n      // Setup captions for HTML5\\n      if (this.isHTML5) {\\n        captions.setup.call(this);\\n      }\\n\\n      // Reset volume\\n      this.volume = null;\\n\\n      // Reset mute state\\n      this.muted = null;\\n\\n      // Reset loop state\\n      this.loop = null;\\n\\n      // Reset quality setting\\n      this.quality = null;\\n\\n      // Reset speed\\n      this.speed = null;\\n\\n      // Reset volume display\\n      controls.updateVolume.call(this);\\n\\n      // Reset time display\\n      controls.timeUpdate.call(this);\\n\\n      // Reset duration display\\n      controls.durationUpdate.call(this);\\n\\n      // Update the UI\\n      ui.checkPlaying.call(this);\\n\\n      // Check for picture-in-picture support\\n      toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n      // Check for airplay support\\n      toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n      // Add touch class\\n      toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n      // Ready for API calls\\n      this.ready = true;\\n\\n      // Ready event at end of execution stack\\n      setTimeout(() => {\\n        triggerEvent.call(this, this.media, 'ready');\\n      }, 0);\\n\\n      // Set the title\\n      ui.setTitle.call(this);\\n\\n      // Assure the poster image is set, if the property was added before the element was created\\n      if (this.poster) {\\n        ui.setPoster.call(this, this.poster, false).catch(() => {});\\n      }\\n\\n      // Manually set the duration if user has overridden it.\\n      // The event listeners for it doesn't get called if preload is disabled (#701)\\n      if (this.config.duration) {\\n        controls.durationUpdate.call(this);\\n      }\\n\\n      // Media metadata\\n      if (this.config.mediaMetadata) {\\n        controls.setMediaMetadata.call(this);\\n      }\\n    },\\n    // Setup aria attribute for play and iframe title\\n    setTitle() {\\n      // Find the current text\\n      let label = i18n.get('play', this.config);\\n\\n      // If there's a media title set, use that for the label\\n      if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n        label += `, ${this.config.title}`;\\n      }\\n\\n      // If there's a play button, set label\\n      Array.from(this.elements.buttons.play || []).forEach(button => {\\n        button.setAttribute('aria-label', label);\\n      });\\n\\n      // Set iframe title\\n      // https://github.com/sampotts/plyr/issues/124\\n      if (this.isEmbed) {\\n        const iframe = getElement.call(this, 'iframe');\\n        if (!is.element(iframe)) {\\n          return;\\n        }\\n\\n        // Default to media type\\n        const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n        const format = i18n.get('frameTitle', this.config);\\n        iframe.setAttribute('title', format.replace('{title}', title));\\n      }\\n    },\\n    // Toggle poster\\n    togglePoster(enable) {\\n      toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n    },\\n    // Set the poster image (async)\\n    // Used internally for the poster setter, with the passive option forced to false\\n    setPoster(poster, passive = true) {\\n      // Don't override if call is passive\\n      if (passive && this.poster) {\\n        return Promise.reject(new Error('Poster already set'));\\n      }\\n\\n      // Set property synchronously to respect the call order\\n      this.media.setAttribute('data-poster', poster);\\n\\n      // Show the poster\\n      this.elements.poster.removeAttribute('hidden');\\n\\n      // Wait until ui is ready\\n      return ready.call(this)\\n      // Load image\\n      .then(() => loadImage(poster)).catch(error => {\\n        // Hide poster on error unless it's been set by another call\\n        if (poster === this.poster) {\\n          ui.togglePoster.call(this, false);\\n        }\\n        // Rethrow\\n        throw error;\\n      }).then(() => {\\n        // Prevent race conditions\\n        if (poster !== this.poster) {\\n          throw new Error('setPoster cancelled by later call to setPoster');\\n        }\\n      }).then(() => {\\n        Object.assign(this.elements.poster.style, {\\n          backgroundImage: `url('${poster}')`,\\n          // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n          backgroundSize: ''\\n        });\\n        ui.togglePoster.call(this, true);\\n        return poster;\\n      });\\n    },\\n    // Check playing state\\n    checkPlaying(event) {\\n      // Class hooks\\n      toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n      toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n      // Set state\\n      Array.from(this.elements.buttons.play || []).forEach(target => {\\n        Object.assign(target, {\\n          pressed: this.playing\\n        });\\n        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n      });\\n\\n      // Only update controls on non timeupdate events\\n      if (is.event(event) && event.type === 'timeupdate') {\\n        return;\\n      }\\n\\n      // Toggle controls\\n      ui.toggleControls.call(this);\\n    },\\n    // Check if media is loading\\n    checkLoading(event) {\\n      this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n      // Clear timer\\n      clearTimeout(this.timers.loading);\\n\\n      // Timer to prevent flicker when seeking\\n      this.timers.loading = setTimeout(() => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      }, this.loading ? 250 : 0);\\n    },\\n    // Toggle controls based on state and `force` argument\\n    toggleControls(force) {\\n      const {\\n        controls: controlsElement\\n      } = this.elements;\\n      if (controlsElement && this.config.hideControls) {\\n        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n        const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n        // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n        this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n      }\\n    },\\n    // Migrate any custom properties from the media to the parent\\n    migrateStyles() {\\n      // Loop through values (as they are the keys when the object is spread 🤔)\\n      Object.values({\\n        ...this.media.style\\n      })\\n      // We're only fussed about Plyr specific properties\\n      .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n      // Remove attribute if empty\\n      if (is.empty(this.media.style)) {\\n        this.media.removeAttribute('style');\\n      }\\n    }\\n  };\\n\\n  class Listeners {\\n    constructor(_player) {\\n      // Device is touch enabled\\n      _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        player.touch = true;\\n\\n        // Add touch class\\n        toggleClass(elements.container, player.config.classNames.isTouch, true);\\n      });\\n      // Global window & document listeners\\n      _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n        const {\\n          player\\n        } = this;\\n\\n        // Keyboard shortcuts\\n        if (player.config.keyboard.global) {\\n          toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n        }\\n\\n        // Click anywhere closes menu\\n        toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n        // Detect touch by events\\n        once.call(player, document.body, 'touchstart', this.firstTouch);\\n      });\\n      // Container listeners\\n      _defineProperty$1(this, \\\"container\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          config,\\n          elements,\\n          timers\\n        } = player;\\n\\n        // Keyboard shortcuts\\n        if (!config.keyboard.global && config.keyboard.focused) {\\n          on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n        }\\n\\n        // Toggle controls on mouse events and entering fullscreen\\n        on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n          const {\\n            controls: controlsElement\\n          } = elements;\\n\\n          // Remove button states for fullscreen\\n          if (controlsElement && event.type === 'enterfullscreen') {\\n            controlsElement.pressed = false;\\n            controlsElement.hover = false;\\n          }\\n\\n          // Show, then hide after a timeout unless another control event occurs\\n          const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n          let delay = 0;\\n          if (show) {\\n            ui.toggleControls.call(player, true);\\n            // Use longer timeout for touch devices\\n            delay = player.touch ? 3000 : 2000;\\n          }\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Set new timer to prevent flicker when seeking\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Set a gutter for Vimeo\\n        const setGutter = () => {\\n          if (!player.isVimeo || player.config.vimeo.premium) {\\n            return;\\n          }\\n          const target = elements.wrapper;\\n          const {\\n            active\\n          } = player.fullscreen;\\n          const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n          const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n          // If not active, remove styles\\n          if (!active) {\\n            if (useNativeAspectRatio) {\\n              target.style.width = null;\\n              target.style.height = null;\\n            } else {\\n              target.style.maxWidth = null;\\n              target.style.margin = null;\\n            }\\n            return;\\n          }\\n\\n          // Determine which dimension will overflow and constrain view\\n          const [viewportWidth, viewportHeight] = getViewportSize();\\n          const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n          if (useNativeAspectRatio) {\\n            target.style.width = overflow ? 'auto' : '100%';\\n            target.style.height = overflow ? '100%' : 'auto';\\n          } else {\\n            target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n            target.style.margin = overflow ? '0 auto' : null;\\n          }\\n        };\\n\\n        // Handle resizing\\n        const resized = () => {\\n          clearTimeout(timers.resized);\\n          timers.resized = setTimeout(setGutter, 50);\\n        };\\n        on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n          const {\\n            target\\n          } = player.fullscreen;\\n\\n          // Ignore events not from target\\n          if (target !== elements.container) {\\n            return;\\n          }\\n\\n          // If it's not an embed and no ratio specified\\n          if (!player.isEmbed && is.empty(player.config.ratio)) {\\n            return;\\n          }\\n\\n          // Set Vimeo gutter\\n          setGutter();\\n\\n          // Watch for resizes\\n          const method = event.type === 'enterfullscreen' ? on : off;\\n          method.call(player, window, 'resize', resized);\\n        });\\n      });\\n      // Listen for media events\\n      _defineProperty$1(this, \\\"media\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n\\n        // Time change on media\\n        on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n        // Display duration\\n        on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n        // Handle the media finishing\\n        on.call(player, player.media, 'ended', () => {\\n          // Show poster on end\\n          if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n            // Restart\\n            player.restart();\\n\\n            // Call pause otherwise IE11 will start playing the video again\\n            player.pause();\\n          }\\n        });\\n\\n        // Check for buffer progress\\n        on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n        // Handle volume changes\\n        on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n        // Handle play/pause\\n        on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n        // Loading state\\n        on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n        // Click video\\n        if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n          // Re-fetch the wrapper\\n          const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n          // Bail if there's no wrapper (this should never happen)\\n          if (!is.element(wrapper)) {\\n            return;\\n          }\\n\\n          // On click play, pause or restart\\n          on.call(player, elements.container, 'click', event => {\\n            const targets = [elements.container, wrapper];\\n\\n            // Ignore if click if not container or in video wrapper\\n            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n              return;\\n            }\\n\\n            // Touch devices will just show controls (if hidden)\\n            if (player.touch && player.config.hideControls) {\\n              return;\\n            }\\n            if (player.ended) {\\n              this.proxy(event, player.restart, 'restart');\\n              this.proxy(event, () => {\\n                silencePromise(player.play());\\n              }, 'play');\\n            } else {\\n              this.proxy(event, () => {\\n                silencePromise(player.togglePlay());\\n              }, 'play');\\n            }\\n          });\\n        }\\n\\n        // Disable right click\\n        if (player.supported.ui && player.config.disableContextMenu) {\\n          on.call(player, elements.wrapper, 'contextmenu', event => {\\n            event.preventDefault();\\n          }, false);\\n        }\\n\\n        // Volume change\\n        on.call(player, player.media, 'volumechange', () => {\\n          // Save to storage\\n          player.storage.set({\\n            volume: player.volume,\\n            muted: player.muted\\n          });\\n        });\\n\\n        // Speed change\\n        on.call(player, player.media, 'ratechange', () => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'speed');\\n\\n          // Save to storage\\n          player.storage.set({\\n            speed: player.speed\\n          });\\n        });\\n\\n        // Quality change\\n        on.call(player, player.media, 'qualitychange', event => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n        });\\n\\n        // Update download link when ready and if quality changes\\n        on.call(player, player.media, 'ready qualitychange', () => {\\n          controls.setDownloadUrl.call(player);\\n        });\\n\\n        // Proxy events to container\\n        // Bubble up key events for Edge\\n        const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n        on.call(player, player.media, proxyEvents, event => {\\n          let {\\n            detail = {}\\n          } = event;\\n\\n          // Get error details from media\\n          if (event.type === 'error') {\\n            detail = player.media.error;\\n          }\\n          triggerEvent.call(player, elements.container, event.type, true, detail);\\n        });\\n      });\\n      // Run default and custom handlers\\n      _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        let returned = true;\\n\\n        // Execute custom handler\\n        if (hasCustomHandler) {\\n          returned = customHandler.call(player, event);\\n        }\\n\\n        // Only call default handler if not prevented in custom handler\\n        if (returned !== false && is.function(defaultHandler)) {\\n          defaultHandler.call(player, event);\\n        }\\n      });\\n      // Trigger custom and default handlers\\n      _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n      });\\n      // Listen for control events\\n      _defineProperty$1(this, \\\"controls\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        // IE doesn't support input event, so we fallback to change\\n        const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n        // Play/pause toggle\\n        if (elements.buttons.play) {\\n          Array.from(elements.buttons.play).forEach(button => {\\n            this.bind(button, 'click', () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          });\\n        }\\n\\n        // Pause\\n        this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n        // Rewind\\n        this.bind(elements.buttons.rewind, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n          player.lastSeekTime = Date.now();\\n          player.rewind();\\n        }, 'rewind');\\n\\n        // Rewind\\n        this.bind(elements.buttons.fastForward, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n          player.lastSeekTime = Date.now();\\n          player.forward();\\n        }, 'fastForward');\\n\\n        // Mute toggle\\n        this.bind(elements.buttons.mute, 'click', () => {\\n          player.muted = !player.muted;\\n        }, 'mute');\\n\\n        // Captions toggle\\n        this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n        // Download\\n        this.bind(elements.buttons.download, 'click', () => {\\n          triggerEvent.call(player, player.media, 'download');\\n        }, 'download');\\n\\n        // Fullscreen toggle\\n        this.bind(elements.buttons.fullscreen, 'click', () => {\\n          player.fullscreen.toggle();\\n        }, 'fullscreen');\\n\\n        // Picture-in-Picture\\n        this.bind(elements.buttons.pip, 'click', () => {\\n          player.pip = 'toggle';\\n        }, 'pip');\\n\\n        // Airplay\\n        this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n        // Settings menu - click toggle\\n        this.bind(elements.buttons.settings, 'click', event => {\\n          // Prevent the document click listener closing the menu\\n          event.stopPropagation();\\n          event.preventDefault();\\n          controls.toggleMenu.call(player, event);\\n        }, null, false); // Can't be passive as we're preventing default\\n\\n        // Settings menu - keyboard toggle\\n        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n        this.bind(elements.buttons.settings, 'keyup', event => {\\n          if (![' ', 'Enter'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Because return triggers a click anyway, all we need to do is set focus\\n          if (event.key === 'Enter') {\\n            controls.focusFirstMenuItem.call(player, null, true);\\n            return;\\n          }\\n\\n          // Prevent scroll\\n          event.preventDefault();\\n\\n          // Prevent playing video (Firefox)\\n          event.stopPropagation();\\n\\n          // Toggle menu\\n          controls.toggleMenu.call(player, event);\\n        }, null, false // Can't be passive as we're preventing default\\n        );\\n\\n        // Escape closes menu\\n        this.bind(elements.settings.menu, 'keydown', event => {\\n          if (event.key === 'Escape') {\\n            controls.toggleMenu.call(player, event);\\n          }\\n        });\\n\\n        // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n        this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n          const rect = elements.progress.getBoundingClientRect();\\n          const percent = 100 / rect.width * (event.pageX - rect.left);\\n          event.currentTarget.setAttribute('seek-value', percent);\\n        });\\n\\n        // Pause while seeking\\n        this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n          const seek = event.currentTarget;\\n          const attribute = 'play-on-seeked';\\n          if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Record seek time so we can prevent hiding controls for a few seconds after seek\\n          player.lastSeekTime = Date.now();\\n\\n          // Was playing before?\\n          const play = seek.hasAttribute(attribute);\\n          // Done seeking\\n          const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n          // If we're done seeking and it was playing, resume playback\\n          if (play && done) {\\n            seek.removeAttribute(attribute);\\n            silencePromise(player.play());\\n          } else if (!done && player.playing) {\\n            seek.setAttribute(attribute, '');\\n            player.pause();\\n          }\\n        });\\n\\n        // Fix range inputs on iOS\\n        // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n        // it takes over further interactions on the page. This is a hack\\n        if (browser.isIos) {\\n          const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n          Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n        }\\n\\n        // Seek\\n        this.bind(elements.inputs.seek, inputEvent, event => {\\n          const seek = event.currentTarget;\\n          // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n          let seekTo = seek.getAttribute('seek-value');\\n          if (is.empty(seekTo)) {\\n            seekTo = seek.value;\\n          }\\n          seek.removeAttribute('seek-value');\\n          player.currentTime = seekTo / seek.max * player.duration;\\n        }, 'seek');\\n\\n        // Seek tooltip\\n        this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n        // Preview thumbnails plugin\\n        // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n        this.bind(elements.progress, 'mousemove touchmove', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startMove(event);\\n          }\\n        });\\n\\n        // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n        this.bind(elements.progress, 'mouseleave touchend click', () => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endMove(false, true);\\n          }\\n        });\\n\\n        // Show scrubbing preview\\n        this.bind(elements.progress, 'mousedown touchstart', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startScrubbing(event);\\n          }\\n        });\\n        this.bind(elements.progress, 'mouseup touchend', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endScrubbing(event);\\n          }\\n        });\\n\\n        // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n        if (browser.isWebKit) {\\n          Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n            this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n          });\\n        }\\n\\n        // Current time invert\\n        // Only if one time element is used for both currentTime and duration\\n        if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n          this.bind(elements.display.currentTime, 'click', () => {\\n            // Do nothing if we're at the start\\n            if (player.currentTime === 0) {\\n              return;\\n            }\\n            player.config.invertTime = !player.config.invertTime;\\n            controls.timeUpdate.call(player);\\n          });\\n        }\\n\\n        // Volume\\n        this.bind(elements.inputs.volume, inputEvent, event => {\\n          player.volume = event.target.value;\\n        }, 'volume');\\n\\n        // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n          elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n        });\\n\\n        // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n        if (elements.fullscreen) {\\n          Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n            this.bind(child, 'mouseenter mouseleave', event => {\\n              if (elements.controls) {\\n                elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n              }\\n            });\\n          });\\n        }\\n\\n        // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n          elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n        });\\n\\n        // Show controls when they receive focus (e.g., when using keyboard tab key)\\n        this.bind(elements.controls, 'focusin', () => {\\n          const {\\n            config,\\n            timers\\n          } = player;\\n\\n          // Skip transition to prevent focus from scrolling the parent element\\n          toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n          // Toggle\\n          ui.toggleControls.call(player, true);\\n\\n          // Restore transition\\n          setTimeout(() => {\\n            toggleClass(elements.controls, config.classNames.noTransition, false);\\n          }, 0);\\n\\n          // Delay a little more for mouse users\\n          const delay = this.touch ? 3000 : 4000;\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Hide again after delay\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Mouse wheel for volume\\n        this.bind(elements.inputs.volume, 'wheel', event => {\\n          // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n          // Other browsers on OS X will be inverted until support improves\\n          const inverted = event.webkitDirectionInvertedFromDevice;\\n          // Get delta from event. Invert if `inverted` is true\\n          const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n          // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n          const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n          // Change the volume by 2%\\n          player.increaseVolume(direction / 50);\\n\\n          // Don't break page scrolling at max and min\\n          const {\\n            volume\\n          } = player.media;\\n          if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n            event.preventDefault();\\n          }\\n        }, 'volume', false);\\n      });\\n      this.player = _player;\\n      this.lastKey = null;\\n      this.focusTimer = null;\\n      this.lastKeyDown = null;\\n      this.handleKey = this.handleKey.bind(this);\\n      this.toggleMenu = this.toggleMenu.bind(this);\\n      this.firstTouch = this.firstTouch.bind(this);\\n    }\\n\\n    // Handle key presses\\n    handleKey(event) {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      const {\\n        key,\\n        type,\\n        altKey,\\n        ctrlKey,\\n        metaKey,\\n        shiftKey\\n      } = event;\\n      const pressed = type === 'keydown';\\n      const repeat = pressed && key === this.lastKey;\\n\\n      // Bail if a modifier key is set\\n      if (altKey || ctrlKey || metaKey || shiftKey) {\\n        return;\\n      }\\n\\n      // If the event is bubbled from the media element\\n      // Firefox doesn't get the key for whatever reason\\n      if (!key) {\\n        return;\\n      }\\n\\n      // Seek by increment\\n      const seekByIncrement = increment => {\\n        // Divide the max duration into 10th's and times by the number value\\n        player.currentTime = player.duration / 10 * increment;\\n      };\\n\\n      // Handle the key on keydown\\n      // Reset on keyup\\n      if (pressed) {\\n        // Check focused element\\n        // and if the focused element is not editable (e.g. text input)\\n        // and any that accept key input http://webaim.org/techniques/keyboard/\\n        const focused = document.activeElement;\\n        if (is.element(focused)) {\\n          const {\\n            editable\\n          } = player.config.selectors;\\n          const {\\n            seek\\n          } = elements.inputs;\\n          if (focused !== seek && matches(focused, editable)) {\\n            return;\\n          }\\n          if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n            return;\\n          }\\n        }\\n\\n        // Which keys should we prevent default\\n        const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n        // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n        if (preventDefault.includes(key)) {\\n          event.preventDefault();\\n          event.stopPropagation();\\n        }\\n        switch (key) {\\n          case '0':\\n          case '1':\\n          case '2':\\n          case '3':\\n          case '4':\\n          case '5':\\n          case '6':\\n          case '7':\\n          case '8':\\n          case '9':\\n            if (!repeat) {\\n              seekByIncrement(parseInt(key, 10));\\n            }\\n            break;\\n          case ' ':\\n          case 'k':\\n            if (!repeat) {\\n              silencePromise(player.togglePlay());\\n            }\\n            break;\\n          case 'ArrowUp':\\n            player.increaseVolume(0.1);\\n            break;\\n          case 'ArrowDown':\\n            player.decreaseVolume(0.1);\\n            break;\\n          case 'm':\\n            if (!repeat) {\\n              player.muted = !player.muted;\\n            }\\n            break;\\n          case 'ArrowRight':\\n            player.forward();\\n            break;\\n          case 'ArrowLeft':\\n            player.rewind();\\n            break;\\n          case 'f':\\n            player.fullscreen.toggle();\\n            break;\\n          case 'c':\\n            if (!repeat) {\\n              player.toggleCaptions();\\n            }\\n            break;\\n          case 'l':\\n            player.loop = !player.loop;\\n            break;\\n        }\\n\\n        // Escape is handle natively when in full screen\\n        // So we only need to worry about non native\\n        if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n          player.fullscreen.toggle();\\n        }\\n\\n        // Store last key for next cycle\\n        this.lastKey = key;\\n      } else {\\n        this.lastKey = null;\\n      }\\n    }\\n\\n    // Toggle menu\\n    toggleMenu(event) {\\n      controls.toggleMenu.call(this.player, event);\\n    }\\n  }\\n\\n  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\n  function createCommonjsModule(fn, module) {\\n  \\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n  }\\n\\n  var loadjs_umd = createCommonjsModule(function (module, exports) {\\n    (function (root, factory) {\\n      {\\n        module.exports = factory();\\n      }\\n    })(commonjsGlobal, function () {\\n      /**\\n       * Global dependencies.\\n       * @global {Object} document - DOM\\n       */\\n\\n      var devnull = function () {},\\n        bundleIdCache = {},\\n        bundleResultCache = {},\\n        bundleCallbackQueue = {};\\n\\n      /**\\n       * Subscribe to bundle load event.\\n       * @param {string[]} bundleIds - Bundle ids\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function subscribe(bundleIds, callbackFn) {\\n        // listify\\n        bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n        var depsNotFound = [],\\n          i = bundleIds.length,\\n          numWaiting = i,\\n          fn,\\n          bundleId,\\n          r,\\n          q;\\n\\n        // define callback function\\n        fn = function (bundleId, pathsNotFound) {\\n          if (pathsNotFound.length) depsNotFound.push(bundleId);\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(depsNotFound);\\n        };\\n\\n        // register callback\\n        while (i--) {\\n          bundleId = bundleIds[i];\\n\\n          // execute callback if in result cache\\n          r = bundleResultCache[bundleId];\\n          if (r) {\\n            fn(bundleId, r);\\n            continue;\\n          }\\n\\n          // add to callback queue\\n          q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n          q.push(fn);\\n        }\\n      }\\n\\n      /**\\n       * Publish bundle load event.\\n       * @param {string} bundleId - Bundle id\\n       * @param {string[]} pathsNotFound - List of files not found\\n       */\\n      function publish(bundleId, pathsNotFound) {\\n        // exit if id isn't defined\\n        if (!bundleId) return;\\n        var q = bundleCallbackQueue[bundleId];\\n\\n        // cache result\\n        bundleResultCache[bundleId] = pathsNotFound;\\n\\n        // exit if queue is empty\\n        if (!q) return;\\n\\n        // empty callback queue\\n        while (q.length) {\\n          q[0](bundleId, pathsNotFound);\\n          q.splice(0, 1);\\n        }\\n      }\\n\\n      /**\\n       * Execute callbacks.\\n       * @param {Object or Function} args - The callback args\\n       * @param {string[]} depsNotFound - List of dependencies not found\\n       */\\n      function executeCallbacks(args, depsNotFound) {\\n        // accept function as argument\\n        if (args.call) args = {\\n          success: args\\n        };\\n\\n        // success and error callbacks\\n        if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n      }\\n\\n      /**\\n       * Load individual file.\\n       * @param {string} path - The file path\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFile(path, callbackFn, args, numTries) {\\n        var doc = document,\\n          async = args.async,\\n          maxTries = (args.numRetries || 0) + 1,\\n          beforeCallbackFn = args.before || devnull,\\n          pathname = path.replace(/[\\\\?|#].*$/, ''),\\n          pathStripped = path.replace(/^(css|img)!/, ''),\\n          isLegacyIECss,\\n          e;\\n        numTries = numTries || 0;\\n        if (/(^css!|\\\\.css$)/.test(pathname)) {\\n          // css\\n          e = doc.createElement('link');\\n          e.rel = 'stylesheet';\\n          e.href = pathStripped;\\n\\n          // tag IE9+\\n          isLegacyIECss = 'hideFocus' in e;\\n\\n          // use preload in IE Edge (to detect load errors)\\n          if (isLegacyIECss && e.relList) {\\n            isLegacyIECss = 0;\\n            e.rel = 'preload';\\n            e.as = 'style';\\n          }\\n        } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n          // image\\n          e = doc.createElement('img');\\n          e.src = pathStripped;\\n        } else {\\n          // javascript\\n          e = doc.createElement('script');\\n          e.src = path;\\n          e.async = async === undefined ? true : async;\\n        }\\n        e.onload = e.onerror = e.onbeforeload = function (ev) {\\n          var result = ev.type[0];\\n\\n          // treat empty stylesheets as failures to get around lack of onerror\\n          // support in IE9-11\\n          if (isLegacyIECss) {\\n            try {\\n              if (!e.sheet.cssText.length) result = 'e';\\n            } catch (x) {\\n              // sheets objects created from load errors don't allow access to\\n              // `cssText` (unless error is Code:18 SecurityError)\\n              if (x.code != 18) result = 'e';\\n            }\\n          }\\n\\n          // handle retries in case of load failure\\n          if (result == 'e') {\\n            // increment counter\\n            numTries += 1;\\n\\n            // exit function and try again\\n            if (numTries < maxTries) {\\n              return loadFile(path, callbackFn, args, numTries);\\n            }\\n          } else if (e.rel == 'preload' && e.as == 'style') {\\n            // activate preloaded stylesheets\\n            return e.rel = 'stylesheet'; // jshint ignore:line\\n          }\\n\\n          // execute callback\\n          callbackFn(path, result, ev.defaultPrevented);\\n        };\\n\\n        // add to document (unless callback returns `false`)\\n        if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n      }\\n\\n      /**\\n       * Load multiple files.\\n       * @param {string[]} paths - The file paths\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFiles(paths, callbackFn, args) {\\n        // listify paths\\n        paths = paths.push ? paths : [paths];\\n        var numWaiting = paths.length,\\n          x = numWaiting,\\n          pathsNotFound = [],\\n          fn,\\n          i;\\n\\n        // define callback function\\n        fn = function (path, result, defaultPrevented) {\\n          // handle error\\n          if (result == 'e') pathsNotFound.push(path);\\n\\n          // handle beforeload event. If defaultPrevented then that means the load\\n          // will be blocked (ex. Ghostery/ABP on Safari)\\n          if (result == 'b') {\\n            if (defaultPrevented) pathsNotFound.push(path);else return;\\n          }\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(pathsNotFound);\\n        };\\n\\n        // load scripts\\n        for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n      }\\n\\n      /**\\n       * Initiate script load and register bundle.\\n       * @param {(string|string[])} paths - The file paths\\n       * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n       *   callback or (3) object literal with success/error arguments, numRetries,\\n       *   etc.\\n       * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n       *   literal with success/error arguments, numRetries, etc.\\n       */\\n      function loadjs(paths, arg1, arg2) {\\n        var bundleId, args;\\n\\n        // bundleId (if string)\\n        if (arg1 && arg1.trim) bundleId = arg1;\\n\\n        // args (default is {})\\n        args = (bundleId ? arg2 : arg1) || {};\\n\\n        // throw error if bundle is already defined\\n        if (bundleId) {\\n          if (bundleId in bundleIdCache) {\\n            throw \\\"LoadJS\\\";\\n          } else {\\n            bundleIdCache[bundleId] = true;\\n          }\\n        }\\n        function loadFn(resolve, reject) {\\n          loadFiles(paths, function (pathsNotFound) {\\n            // execute callbacks\\n            executeCallbacks(args, pathsNotFound);\\n\\n            // resolve Promise\\n            if (resolve) {\\n              executeCallbacks({\\n                success: resolve,\\n                error: reject\\n              }, pathsNotFound);\\n            }\\n\\n            // publish bundle load event\\n            publish(bundleId, pathsNotFound);\\n          }, args);\\n        }\\n        if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n      }\\n\\n      /**\\n       * Execute callbacks when dependencies have been satisfied.\\n       * @param {(string|string[])} deps - List of bundle ids\\n       * @param {Object} args - success/error arguments\\n       */\\n      loadjs.ready = function ready(deps, args) {\\n        // subscribe to bundle load event\\n        subscribe(deps, function (depsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, depsNotFound);\\n        });\\n        return loadjs;\\n      };\\n\\n      /**\\n       * Manually satisfy bundle dependencies.\\n       * @param {string} bundleId - The bundle id\\n       */\\n      loadjs.done = function done(bundleId) {\\n        publish(bundleId, []);\\n      };\\n\\n      /**\\n       * Reset loadjs dependencies statuses\\n       */\\n      loadjs.reset = function reset() {\\n        bundleIdCache = {};\\n        bundleResultCache = {};\\n        bundleCallbackQueue = {};\\n      };\\n\\n      /**\\n       * Determine if bundle has already been defined\\n       * @param String} bundleId - The bundle id\\n       */\\n      loadjs.isDefined = function isDefined(bundleId) {\\n        return bundleId in bundleIdCache;\\n      };\\n\\n      // export\\n      return loadjs;\\n    });\\n  });\\n\\n  // ==========================================================================\\n  function loadScript(url) {\\n    return new Promise((resolve, reject) => {\\n      loadjs_umd(url, {\\n        success: resolve,\\n        error: reject\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Parse Vimeo ID from URL\\n  function parseId$1(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    if (is.number(Number(url))) {\\n      return url;\\n    }\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Try to extract a hash for private videos from the URL\\n  function parseHash(url) {\\n    /* This regex matches a hexadecimal hash if given in any of these forms:\\n     *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n     *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n     *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n     *  - video/{id}/{hash}\\n     * If matched, the hash is available in capture group 4\\n     */\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n    const found = url.match(regex);\\n    return found && found.length === 5 ? found[4] : null;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState$1(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  const vimeo = {\\n    setup() {\\n      const player = this;\\n\\n      // Add embed class for responsive\\n      toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set intial ratio\\n      setAspectRatio.call(player);\\n\\n      // Load the SDK if not already\\n      if (!is.object(window.Vimeo)) {\\n        loadScript(player.config.urls.vimeo.sdk).then(() => {\\n          vimeo.ready.call(player);\\n        }).catch(error => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n      } else {\\n        vimeo.ready.call(player);\\n      }\\n    },\\n    // API Ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.vimeo;\\n      const {\\n        premium,\\n        referrerPolicy,\\n        ...frameParams\\n      } = config;\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n      let hash = '';\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(player.config.attributes.embed.id);\\n        // hash can also be set as attribute on the <div>\\n        hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n      } else {\\n        hash = parseHash(source);\\n      }\\n      const hashParam = hash ? {\\n        h: hash\\n      } : {};\\n\\n      // If the owner has a pro or premium account then we can hide controls etc\\n      if (premium) {\\n        Object.assign(frameParams, {\\n          controls: false,\\n          sidedock: false\\n        });\\n      }\\n\\n      // Get Vimeo params for the iframe\\n      const params = buildUrlParams({\\n        loop: player.config.loop.active,\\n        autoplay: player.autoplay,\\n        muted: player.muted,\\n        gesture: 'media',\\n        playsinline: player.config.playsinline,\\n        // hash has to be added to iframe-URL\\n        ...hashParam,\\n        ...frameParams\\n      });\\n      const id = parseId$1(source);\\n      // Build an iframe\\n      const iframe = createElement('iframe');\\n      const src = format(player.config.urls.vimeo.iframe, id, params);\\n      iframe.setAttribute('src', src);\\n      iframe.setAttribute('allowfullscreen', '');\\n      iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n      // Set the referrer policy if required\\n      if (!is.empty(referrerPolicy)) {\\n        iframe.setAttribute('referrerPolicy', referrerPolicy);\\n      }\\n\\n      // Inject the package\\n      if (premium || !config.customControls) {\\n        iframe.setAttribute('data-poster', player.poster);\\n        player.media = replaceElement(iframe, player.media);\\n      } else {\\n        const wrapper = createElement('div', {\\n          class: player.config.classNames.embedContainer,\\n          'data-poster': player.poster\\n        });\\n        wrapper.appendChild(iframe);\\n        player.media = replaceElement(wrapper, player.media);\\n      }\\n\\n      // Get poster image\\n      if (!config.customControls) {\\n        fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n          if (is.empty(response) || !response.thumbnail_url) {\\n            return;\\n          }\\n\\n          // Set and show poster\\n          ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n        });\\n      }\\n\\n      // Setup instance\\n      // https://github.com/vimeo/player.js\\n      player.embed = new window.Vimeo.Player(iframe, {\\n        autopause: player.config.autopause,\\n        muted: player.muted\\n      });\\n      player.media.paused = true;\\n      player.media.currentTime = 0;\\n\\n      // Disable native text track rendering\\n      if (player.supported.ui) {\\n        player.embed.disableTextTrack();\\n      }\\n\\n      // Create a faux HTML5 API using the Vimeo API\\n      player.media.play = () => {\\n        assurePlaybackState$1.call(player, true);\\n        return player.embed.play();\\n      };\\n      player.media.pause = () => {\\n        assurePlaybackState$1.call(player, false);\\n        return player.embed.pause();\\n      };\\n      player.media.stop = () => {\\n        player.pause();\\n        player.currentTime = 0;\\n      };\\n\\n      // Seeking\\n      let {\\n        currentTime\\n      } = player.media;\\n      Object.defineProperty(player.media, 'currentTime', {\\n        get() {\\n          return currentTime;\\n        },\\n        set(time) {\\n          // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n          // Get current paused state and volume etc\\n          const {\\n            embed,\\n            media,\\n            paused,\\n            volume\\n          } = player;\\n          const restorePause = paused && !embed.hasPlayed;\\n\\n          // Set seeking state and trigger event\\n          media.seeking = true;\\n          triggerEvent.call(player, media, 'seeking');\\n\\n          // If paused, mute until seek is complete\\n          Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n            // Do nothing\\n          });\\n        }\\n      });\\n\\n      // Playback speed\\n      let speed = player.config.speed.selected;\\n      Object.defineProperty(player.media, 'playbackRate', {\\n        get() {\\n          return speed;\\n        },\\n        set(input) {\\n          player.embed.setPlaybackRate(input).then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          }).catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n        }\\n      });\\n\\n      // Volume\\n      let {\\n        volume\\n      } = player.config;\\n      Object.defineProperty(player.media, 'volume', {\\n        get() {\\n          return volume;\\n        },\\n        set(input) {\\n          player.embed.setVolume(input).then(() => {\\n            volume = input;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Muted\\n      let {\\n        muted\\n      } = player.config;\\n      Object.defineProperty(player.media, 'muted', {\\n        get() {\\n          return muted;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : false;\\n          player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n            muted = toggle;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Loop\\n      let {\\n        loop\\n      } = player.config;\\n      Object.defineProperty(player.media, 'loop', {\\n        get() {\\n          return loop;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : player.config.loop.active;\\n          player.embed.setLoop(toggle).then(() => {\\n            loop = toggle;\\n          });\\n        }\\n      });\\n\\n      // Source\\n      let currentSrc;\\n      player.embed.getVideoUrl().then(value => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      }).catch(error => {\\n        this.debug.warn(error);\\n      });\\n      Object.defineProperty(player.media, 'currentSrc', {\\n        get() {\\n          return currentSrc;\\n        }\\n      });\\n\\n      // Ended\\n      Object.defineProperty(player.media, 'ended', {\\n        get() {\\n          return player.currentTime === player.duration;\\n        }\\n      });\\n\\n      // Set aspect ratio based on video size\\n      Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n        const [width, height] = dimensions;\\n        player.embed.ratio = roundAspectRatio(width, height);\\n        setAspectRatio.call(this);\\n      });\\n\\n      // Set autopause\\n      player.embed.setAutopause(player.config.autopause).then(state => {\\n        player.config.autopause = state;\\n      });\\n\\n      // Get title\\n      player.embed.getVideoTitle().then(title => {\\n        player.config.title = title;\\n        ui.setTitle.call(this);\\n      });\\n\\n      // Get current time\\n      player.embed.getCurrentTime().then(value => {\\n        currentTime = value;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n\\n      // Get duration\\n      player.embed.getDuration().then(value => {\\n        player.media.duration = value;\\n        triggerEvent.call(player, player.media, 'durationchange');\\n      });\\n\\n      // Get captions\\n      player.embed.getTextTracks().then(tracks => {\\n        player.media.textTracks = tracks;\\n        captions.setup.call(player);\\n      });\\n      player.embed.on('cuechange', ({\\n        cues = []\\n      }) => {\\n        const strippedCues = cues.map(cue => stripHTML(cue.text));\\n        captions.updateCues.call(player, strippedCues);\\n      });\\n      player.embed.on('loaded', () => {\\n        // Assure state and events are updated on autoplay\\n        player.embed.getPaused().then(paused => {\\n          assurePlaybackState$1.call(player, !paused);\\n          if (!paused) {\\n            triggerEvent.call(player, player.media, 'playing');\\n          }\\n        });\\n        if (is.element(player.embed.element) && player.supported.ui) {\\n          const frame = player.embed.element;\\n\\n          // Fix keyboard focus issues\\n          // https://github.com/sampotts/plyr/issues/317\\n          frame.setAttribute('tabindex', -1);\\n        }\\n      });\\n      player.embed.on('bufferstart', () => {\\n        triggerEvent.call(player, player.media, 'waiting');\\n      });\\n      player.embed.on('bufferend', () => {\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('play', () => {\\n        assurePlaybackState$1.call(player, true);\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('pause', () => {\\n        assurePlaybackState$1.call(player, false);\\n      });\\n      player.embed.on('timeupdate', data => {\\n        player.media.seeking = false;\\n        currentTime = data.seconds;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n      player.embed.on('progress', data => {\\n        player.media.buffered = data.percent;\\n        triggerEvent.call(player, player.media, 'progress');\\n\\n        // Check all loaded\\n        if (parseInt(data.percent, 10) === 1) {\\n          triggerEvent.call(player, player.media, 'canplaythrough');\\n        }\\n\\n        // Get duration as if we do it before load, it gives an incorrect value\\n        // https://github.com/sampotts/plyr/issues/891\\n        player.embed.getDuration().then(value => {\\n          if (value !== player.media.duration) {\\n            player.media.duration = value;\\n            triggerEvent.call(player, player.media, 'durationchange');\\n          }\\n        });\\n      });\\n      player.embed.on('seeked', () => {\\n        player.media.seeking = false;\\n        triggerEvent.call(player, player.media, 'seeked');\\n      });\\n      player.embed.on('ended', () => {\\n        player.media.paused = true;\\n        triggerEvent.call(player, player.media, 'ended');\\n      });\\n      player.embed.on('error', detail => {\\n        player.media.error = detail;\\n        triggerEvent.call(player, player.media, 'error');\\n      });\\n\\n      // Rebuild UI\\n      if (config.customControls) {\\n        setTimeout(() => ui.build.call(player), 0);\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Parse YouTube ID from URL\\n  function parseId(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  function getHost(config) {\\n    if (config.noCookie) {\\n      return 'https://www.youtube-nocookie.com';\\n    }\\n    if (window.location.protocol === 'http:') {\\n      return 'http://www.youtube.com';\\n    }\\n\\n    // Use YouTube's default\\n    return undefined;\\n  }\\n  const youtube = {\\n    setup() {\\n      // Add embed class for responsive\\n      toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n      // Setup API\\n      if (is.object(window.YT) && is.function(window.YT.Player)) {\\n        youtube.ready.call(this);\\n      } else {\\n        // Reference current global callback\\n        const callback = window.onYouTubeIframeAPIReady;\\n\\n        // Set callback to process queue\\n        window.onYouTubeIframeAPIReady = () => {\\n          // Call global callback if set\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n          youtube.ready.call(this);\\n        };\\n\\n        // Load the SDK\\n        loadScript(this.config.urls.youtube.sdk).catch(error => {\\n          this.debug.warn('YouTube API failed to load', error);\\n        });\\n      }\\n    },\\n    // Get the media title\\n    getTitle(videoId) {\\n      const url = format(this.config.urls.youtube.api, videoId);\\n      fetch(url).then(data => {\\n        if (is.object(data)) {\\n          const {\\n            title,\\n            height,\\n            width\\n          } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n        setAspectRatio.call(this);\\n      }).catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n    },\\n    // API ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.youtube;\\n      // Ignore already setup (race condition)\\n      const currentId = player.media && player.media.getAttribute('id');\\n      if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n        return;\\n      }\\n\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(this.config.attributes.embed.id);\\n      }\\n\\n      // Replace the <iframe> with a <div> due to YouTube API issues\\n      const videoId = parseId(source);\\n      const id = generateId(player.provider);\\n      // Replace media element\\n      const container = createElement('div', {\\n        id,\\n        'data-poster': config.customControls ? player.poster : undefined\\n      });\\n      player.media = replaceElement(container, player.media);\\n\\n      // Only load the poster when using custom controls\\n      if (config.customControls) {\\n        const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n        // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n        loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        }).catch(() => {});\\n      }\\n\\n      // Setup instance\\n      // https://developers.google.com/youtube/iframe_api_reference\\n      player.embed = new window.YT.Player(player.media, {\\n        videoId,\\n        host: getHost(config),\\n        playerVars: extend({}, {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null\\n        }, config),\\n        events: {\\n          onError(event) {\\n            // YouTube may fire onError twice, so only handle it once\\n            if (!player.media.error) {\\n              const code = event.data;\\n              // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n              const message = {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n              }[code] || 'An unknown error occurred';\\n              player.media.error = {\\n                code,\\n                message\\n              };\\n              triggerEvent.call(player, player.media, 'error');\\n            }\\n          },\\n          onPlaybackRateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get current speed\\n            player.media.playbackRate = instance.getPlaybackRate();\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          },\\n          onReady(event) {\\n            // Bail if onReady has already been called. See issue #1108\\n            if (is.function(player.media.play)) {\\n              return;\\n            }\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get the title\\n            youtube.getTitle.call(player, videoId);\\n\\n            // Create a faux HTML5 API using the YouTube API\\n            player.media.play = () => {\\n              assurePlaybackState.call(player, true);\\n              instance.playVideo();\\n            };\\n            player.media.pause = () => {\\n              assurePlaybackState.call(player, false);\\n              instance.pauseVideo();\\n            };\\n            player.media.stop = () => {\\n              instance.stopVideo();\\n            };\\n            player.media.duration = instance.getDuration();\\n            player.media.paused = true;\\n\\n            // Seeking\\n            player.media.currentTime = 0;\\n            Object.defineProperty(player.media, 'currentTime', {\\n              get() {\\n                return Number(instance.getCurrentTime());\\n              },\\n              set(time) {\\n                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n                if (player.paused && !player.embed.hasPlayed) {\\n                  player.embed.mute();\\n                }\\n\\n                // Set seeking state and trigger event\\n                player.media.seeking = true;\\n                triggerEvent.call(player, player.media, 'seeking');\\n\\n                // Seek after events sent\\n                instance.seekTo(time);\\n              }\\n            });\\n\\n            // Playback speed\\n            Object.defineProperty(player.media, 'playbackRate', {\\n              get() {\\n                return instance.getPlaybackRate();\\n              },\\n              set(input) {\\n                instance.setPlaybackRate(input);\\n              }\\n            });\\n\\n            // Volume\\n            let {\\n              volume\\n            } = player.config;\\n            Object.defineProperty(player.media, 'volume', {\\n              get() {\\n                return volume;\\n              },\\n              set(input) {\\n                volume = input;\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Muted\\n            let {\\n              muted\\n            } = player.config;\\n            Object.defineProperty(player.media, 'muted', {\\n              get() {\\n                return muted;\\n              },\\n              set(input) {\\n                const toggle = is.boolean(input) ? input : muted;\\n                muted = toggle;\\n                instance[toggle ? 'mute' : 'unMute']();\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Source\\n            Object.defineProperty(player.media, 'currentSrc', {\\n              get() {\\n                return instance.getVideoUrl();\\n              }\\n            });\\n\\n            // Ended\\n            Object.defineProperty(player.media, 'ended', {\\n              get() {\\n                return player.currentTime === player.duration;\\n              }\\n            });\\n\\n            // Get available speeds\\n            const speeds = instance.getAvailablePlaybackRates();\\n            // Filter based on config\\n            player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n            // Set the tabindex to avoid focus entering iframe\\n            if (player.supported.ui && config.customControls) {\\n              player.media.setAttribute('tabindex', -1);\\n            }\\n            triggerEvent.call(player, player.media, 'timeupdate');\\n            triggerEvent.call(player, player.media, 'durationchange');\\n\\n            // Reset timer\\n            clearInterval(player.timers.buffering);\\n\\n            // Setup buffering\\n            player.timers.buffering = setInterval(() => {\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n\\n              // Trigger progress only when we actually buffer something\\n              if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n                triggerEvent.call(player, player.media, 'progress');\\n              }\\n\\n              // Set last buffer point\\n              player.media.lastBuffered = player.media.buffered;\\n\\n              // Bail if we're at 100%\\n              if (player.media.buffered === 1) {\\n                clearInterval(player.timers.buffering);\\n\\n                // Trigger event\\n                triggerEvent.call(player, player.media, 'canplaythrough');\\n              }\\n            }, 200);\\n\\n            // Rebuild UI\\n            if (config.customControls) {\\n              setTimeout(() => ui.build.call(player), 50);\\n            }\\n          },\\n          onStateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Reset timer\\n            clearInterval(player.timers.playing);\\n            const seeked = player.media.seeking && [1, 2].includes(event.data);\\n            if (seeked) {\\n              // Unset seeking and fire seeked event\\n              player.media.seeking = false;\\n              triggerEvent.call(player, player.media, 'seeked');\\n            }\\n\\n            // Handle events\\n            // -1   Unstarted\\n            // 0    Ended\\n            // 1    Playing\\n            // 2    Paused\\n            // 3    Buffering\\n            // 5    Video cued\\n            switch (event.data) {\\n              case -1:\\n                // Update scrubber\\n                triggerEvent.call(player, player.media, 'timeupdate');\\n\\n                // Get loaded % from YouTube\\n                player.media.buffered = instance.getVideoLoadedFraction();\\n                triggerEvent.call(player, player.media, 'progress');\\n                break;\\n              case 0:\\n                assurePlaybackState.call(player, false);\\n\\n                // YouTube doesn't support loop for a single video, so mimick it.\\n                if (player.media.loop) {\\n                  // YouTube needs a call to `stopVideo` before playing again\\n                  instance.stopVideo();\\n                  instance.playVideo();\\n                } else {\\n                  triggerEvent.call(player, player.media, 'ended');\\n                }\\n                break;\\n              case 1:\\n                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                  player.media.pause();\\n                } else {\\n                  assurePlaybackState.call(player, true);\\n                  triggerEvent.call(player, player.media, 'playing');\\n\\n                  // Poll to get playback progress\\n                  player.timers.playing = setInterval(() => {\\n                    triggerEvent.call(player, player.media, 'timeupdate');\\n                  }, 50);\\n\\n                  // Check duration again due to YouTube bug\\n                  // https://github.com/sampotts/plyr/issues/374\\n                  // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                  if (player.media.duration !== instance.getDuration()) {\\n                    player.media.duration = instance.getDuration();\\n                    triggerEvent.call(player, player.media, 'durationchange');\\n                  }\\n                }\\n                break;\\n              case 2:\\n                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (!player.muted) {\\n                  player.embed.unMute();\\n                }\\n                assurePlaybackState.call(player, false);\\n                break;\\n              case 3:\\n                // Trigger waiting event to add loading classes to container as the video buffers.\\n                triggerEvent.call(player, player.media, 'waiting');\\n                break;\\n            }\\n            triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n              code: event.data\\n            });\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  // ==========================================================================\\n  const media = {\\n    // Setup media\\n    setup() {\\n      // If there's no media, bail\\n      if (!this.media) {\\n        this.debug.warn('No media element found!');\\n        return;\\n      }\\n\\n      // Add type class\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n      // Add provider class\\n      toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n      // Add video class for embeds\\n      // This will require changes if audio embeds are added\\n      if (this.isEmbed) {\\n        toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n      }\\n\\n      // Inject the player wrapper\\n      if (this.isVideo) {\\n        // Create the wrapper div\\n        this.elements.wrapper = createElement('div', {\\n          class: this.config.classNames.video\\n        });\\n\\n        // Wrap the video in a container\\n        wrap(this.media, this.elements.wrapper);\\n\\n        // Poster image container\\n        this.elements.poster = createElement('div', {\\n          class: this.config.classNames.poster\\n        });\\n        this.elements.wrapper.appendChild(this.elements.poster);\\n      }\\n      if (this.isHTML5) {\\n        html5.setup.call(this);\\n      } else if (this.isYouTube) {\\n        youtube.setup.call(this);\\n      } else if (this.isVimeo) {\\n        vimeo.setup.call(this);\\n      }\\n    }\\n  };\\n\\n  const destroy = instance => {\\n    // Destroy our adsManager\\n    if (instance.manager) {\\n      instance.manager.destroy();\\n    }\\n\\n    // Destroy our adsManager\\n    if (instance.elements.displayContainer) {\\n      instance.elements.displayContainer.destroy();\\n    }\\n    instance.elements.container.remove();\\n  };\\n  class Ads {\\n    /**\\n     * Ads constructor.\\n     * @param {Object} player\\n     * @return {Ads}\\n     */\\n    constructor(player) {\\n      /**\\n       * Load the IMA SDK\\n       */\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Check if the Google IMA3 SDK is loaded or load it ourselves\\n        if (!is.object(window.google) || !is.object(window.google.ima)) {\\n          loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n            this.ready();\\n          }).catch(() => {\\n            // Script failed to load or is blocked\\n            this.trigger('error', new Error('Google IMA SDK failed to load'));\\n          });\\n        } else {\\n          this.ready();\\n        }\\n      });\\n      /**\\n       * Get the ads instance ready\\n       */\\n      _defineProperty$1(this, \\\"ready\\\", () => {\\n        // Double check we're enabled\\n        if (!this.enabled) {\\n          destroy(this);\\n        }\\n\\n        // Start ticking our safety timer. If the whole advertisement\\n        // thing doesn't resolve within our set time; we bail\\n        this.startSafetyTimer(12000, 'ready()');\\n\\n        // Clear the safety timer\\n        this.managerPromise.then(() => {\\n          this.clearSafetyTimer('onAdsManagerLoaded()');\\n        });\\n\\n        // Set listeners on the Plyr instance\\n        this.listeners();\\n\\n        // Setup the IMA SDK\\n        this.setupIMA();\\n      });\\n      /**\\n       * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n       * so here we define our ad container. This div is set up to render on top of the video player.\\n       * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n       * handle to the content video player - the SDK will poll the current time of our player to\\n       * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n       * mobile devices, this initialization is done as the result of a user action.\\n       */\\n      _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n        // Create the container for our advertisements\\n        this.elements.container = createElement('div', {\\n          class: this.player.config.classNames.ads\\n        });\\n        this.player.elements.container.appendChild(this.elements.container);\\n\\n        // So we can run VPAID2\\n        google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n        // Set language\\n        google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n        // Set playback for iOS10+\\n        google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n        // We assume the adContainer is the video container of the plyr element that will house the ads\\n        this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n        // Create ads loader\\n        this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n        // Listen and respond to ads loaded and error events\\n        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n        // Request video ads to be pre-loaded\\n        this.requestAds();\\n      });\\n      /**\\n       * Request advertisements\\n       */\\n      _defineProperty$1(this, \\\"requestAds\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        try {\\n          // Request video ads\\n          const request = new google.ima.AdsRequest();\\n          request.adTagUrl = this.tagUrl;\\n\\n          // Specify the linear and nonlinear slot sizes. This helps the SDK\\n          // to select the correct creative if multiple are returned\\n          request.linearAdSlotWidth = container.offsetWidth;\\n          request.linearAdSlotHeight = container.offsetHeight;\\n          request.nonLinearAdSlotWidth = container.offsetWidth;\\n          request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n          // We only overlay ads as we only support video.\\n          request.forceNonLinearFullSlot = false;\\n\\n          // Mute based on current state\\n          request.setAdWillPlayMuted(!this.player.muted);\\n          this.loader.requestAds(request);\\n        } catch (error) {\\n          this.onAdError(error);\\n        }\\n      });\\n      /**\\n       * Update the ad countdown\\n       * @param {Boolean} start\\n       */\\n      _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n        if (!start) {\\n          clearInterval(this.countdownTimer);\\n          this.elements.container.removeAttribute('data-badge-text');\\n          return;\\n        }\\n        const update = () => {\\n          const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n          const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n          this.elements.container.setAttribute('data-badge-text', label);\\n        };\\n        this.countdownTimer = setInterval(update, 100);\\n      });\\n      /**\\n       * This method is called whenever the ads are ready inside the AdDisplayContainer\\n       * @param {Event} event - adsManagerLoadedEvent\\n       */\\n      _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n        // Load could occur after a source change (race condition)\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Get the ads manager\\n        const settings = new google.ima.AdsRenderingSettings();\\n\\n        // Tell the SDK to save and restore content video state on our behalf\\n        settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n        settings.enablePreloading = true;\\n\\n        // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n        // so it can determine when to start the mid- and post-roll\\n        this.manager = event.getAdsManager(this.player, settings);\\n\\n        // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n        this.cuePoints = this.manager.getCuePoints();\\n\\n        // Add listeners to the required events\\n        // Advertisement error events\\n        this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n        // Advertisement regular events\\n        Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n          this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n        });\\n\\n        // Resolve our adsManager\\n        this.trigger('loaded');\\n      });\\n      _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n        // Add advertisement cue's within the time line if available\\n        if (!is.empty(this.cuePoints)) {\\n          this.cuePoints.forEach(cuePoint => {\\n            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n              const seekElement = this.player.elements.progress;\\n              if (is.element(seekElement)) {\\n                const cuePercentage = 100 / this.player.duration * cuePoint;\\n                const cue = createElement('span', {\\n                  class: this.player.config.classNames.cues\\n                });\\n                cue.style.left = `${cuePercentage.toString()}%`;\\n                seekElement.appendChild(cue);\\n              }\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n       * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n       * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n        // don't have ad object associated\\n        const ad = event.getAd();\\n        const adData = event.getAdData();\\n\\n        // Proxy event\\n        const dispatchEvent = type => {\\n          triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n        };\\n\\n        // Bubble the event\\n        dispatchEvent(event.type);\\n        switch (event.type) {\\n          case google.ima.AdEvent.Type.LOADED:\\n            // This is the first event sent for an ad - it is possible to determine whether the\\n            // ad is a video ad or an overlay\\n            this.trigger('loaded');\\n\\n            // Start countdown\\n            this.pollCountdown(true);\\n            if (!ad.isLinear()) {\\n              // Position AdDisplayContainer correctly for overlay\\n              ad.width = container.offsetWidth;\\n              ad.height = container.offsetHeight;\\n            }\\n\\n            // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n            break;\\n          case google.ima.AdEvent.Type.STARTED:\\n            // Set volume to match player\\n            this.manager.setVolume(this.player.volume);\\n            break;\\n          case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n            // All ads for the current videos are done. We can now request new advertisements\\n            // in case the video is re-played\\n\\n            // TODO: Example for what happens when a next video in a playlist would be loaded.\\n            // So here we load a new video when all ads are done.\\n            // Then we load new ads within a new adsManager. When the video\\n            // Is started - after - the ads are loaded, then we get ads.\\n            // You can also easily test cancelling and reloading by running\\n            // player.ads.cancel() and player.ads.play from the console I guess.\\n            // this.player.source = {\\n            //     type: 'video',\\n            //     title: 'View From A Blue Moon',\\n            //     sources: [{\\n            //         src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n            // 'video/mp4', }], poster:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n            // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n            // };\\n\\n            // TODO: So there is still this thing where a video should only be allowed to start\\n            // playing when the IMA SDK is ready or has failed\\n\\n            if (this.player.ended) {\\n              this.loadAds();\\n            } else {\\n              // The SDK won't allow new ads to be called without receiving a contentComplete()\\n              this.loader.contentComplete();\\n            }\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n            // This event indicates the ad has started - the video player can adjust the UI,\\n            // for example display a pause button and remaining time. Fired when content should\\n            // be paused. This usually happens right before an ad is about to cover the content\\n\\n            this.pauseContent();\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n            // This event indicates the ad has finished - the video player can perform\\n            // appropriate UI actions, such as removing the timer for remaining time detection.\\n            // Fired when content should be resumed. This usually happens when an ad finishes\\n            // or collapses\\n\\n            this.pollCountdown();\\n            this.resumeContent();\\n            break;\\n          case google.ima.AdEvent.Type.LOG:\\n            if (adData.adError) {\\n              this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n            }\\n            break;\\n        }\\n      });\\n      /**\\n       * Any ad error handling comes through here\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdError\\\", event => {\\n        this.cancel();\\n        this.player.debug.warn('Ads error', event);\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events. This ensures\\n       * the mid- and post-roll launch at the correct time. And\\n       * resize the advertisement when the player resizes\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        let time;\\n        this.player.on('canplay', () => {\\n          this.addCuePoints();\\n        });\\n        this.player.on('ended', () => {\\n          this.loader.contentComplete();\\n        });\\n        this.player.on('timeupdate', () => {\\n          time = this.player.currentTime;\\n        });\\n        this.player.on('seeked', () => {\\n          const seekedTime = this.player.currentTime;\\n          if (is.empty(this.cuePoints)) {\\n            return;\\n          }\\n          this.cuePoints.forEach((cuePoint, index) => {\\n            if (time < cuePoint && cuePoint < seekedTime) {\\n              this.manager.discardAdBreak();\\n              this.cuePoints.splice(index, 1);\\n            }\\n          });\\n        });\\n\\n        // Listen to the resizing of the window. And resize ad accordingly\\n        // TODO: eventually implement ResizeObserver\\n        window.addEventListener('resize', () => {\\n          if (this.manager) {\\n            this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n          }\\n        });\\n      });\\n      /**\\n       * Initialize the adsManager and start playing advertisements\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        if (!this.managerPromise) {\\n          this.resumeContent();\\n        }\\n\\n        // Play the requested advertisement whenever the adsManager is ready\\n        this.managerPromise.then(() => {\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n\\n          // Initialize the container. Must be done via a user action on mobile devices\\n          this.elements.displayContainer.initialize();\\n          try {\\n            if (!this.initialized) {\\n              // Initialize the ads manager. Ad rules playlist will start at this time\\n              this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n              // Call play to start showing the ad. Single video and overlay ads will\\n              // start at this time; the call will be ignored for ad rules\\n              this.manager.start();\\n            }\\n            this.initialized = true;\\n          } catch (adError) {\\n            // An error may be thrown if there was a problem with the\\n            // VAST response\\n            this.onAdError(adError);\\n          }\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Resume our video\\n       */\\n      _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n        // Hide the advertisement container\\n        this.elements.container.style.zIndex = '';\\n\\n        // Ad is stopped\\n        this.playing = false;\\n\\n        // Play video\\n        silencePromise(this.player.media.play());\\n      });\\n      /**\\n       * Pause our video\\n       */\\n      _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n        // Show the advertisement container\\n        this.elements.container.style.zIndex = 3;\\n\\n        // Ad is playing\\n        this.playing = true;\\n\\n        // Pause our video.\\n        this.player.media.pause();\\n      });\\n      /**\\n       * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n       * allowed to call new ads based on google policies, as they interpret this as an accidental\\n       * video requests. https://developers.google.com/interactive-\\n       * media-ads/docs/sdks/android/faq#8\\n       */\\n      _defineProperty$1(this, \\\"cancel\\\", () => {\\n        // Pause our video\\n        if (this.initialized) {\\n          this.resumeContent();\\n        }\\n\\n        // Tell our instance that we're done for now\\n        this.trigger('error');\\n\\n        // Re-create our adsManager\\n        this.loadAds();\\n      });\\n      /**\\n       * Re-create our adsManager\\n       */\\n      _defineProperty$1(this, \\\"loadAds\\\", () => {\\n        // Tell our adsManager to go bye bye\\n        this.managerPromise.then(() => {\\n          // Destroy our adsManager\\n          if (this.manager) {\\n            this.manager.destroy();\\n          }\\n\\n          // Re-set our adsManager promises\\n          this.managerPromise = new Promise(resolve => {\\n            this.on('loaded', resolve);\\n            this.player.debug.log(this.manager);\\n          });\\n          // Now that the manager has been destroyed set it to also be un-initialized\\n          this.initialized = false;\\n\\n          // Now request some new advertisements\\n          this.requestAds();\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Handles callbacks after an ad event was invoked\\n       * @param {String} event - Event type\\n       * @param args\\n       */\\n      _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n        const handlers = this.events[event];\\n        if (is.array(handlers)) {\\n          handlers.forEach(handler => {\\n            if (is.function(handler)) {\\n              handler.apply(this, args);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       * @return {Ads}\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        if (!is.array(this.events[event])) {\\n          this.events[event] = [];\\n        }\\n        this.events[event].push(callback);\\n        return this;\\n      });\\n      /**\\n       * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n       * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n       * advertisement is playing, or when a user action is required to start, then we clear the\\n       * timer on ad ready\\n       * @param {Number} time\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n        this.player.debug.log(`Safety timer invoked from: ${from}`);\\n        this.safetyTimer = setTimeout(() => {\\n          this.cancel();\\n          this.clearSafetyTimer('startSafetyTimer()');\\n        }, time);\\n      });\\n      /**\\n       * Clear our safety timer(s)\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n        if (!is.nullOrUndefined(this.safetyTimer)) {\\n          this.player.debug.log(`Safety timer cleared from: ${from}`);\\n          clearTimeout(this.safetyTimer);\\n          this.safetyTimer = null;\\n        }\\n      });\\n      this.player = player;\\n      this.config = player.config.ads;\\n      this.playing = false;\\n      this.initialized = false;\\n      this.elements = {\\n        container: null,\\n        displayContainer: null\\n      };\\n      this.manager = null;\\n      this.loader = null;\\n      this.cuePoints = null;\\n      this.events = {};\\n      this.safetyTimer = null;\\n      this.countdownTimer = null;\\n\\n      // Setup a promise to resolve when the IMA manager is ready\\n      this.managerPromise = new Promise((resolve, reject) => {\\n        // The ad is loaded and ready\\n        this.on('loaded', resolve);\\n\\n        // Ads failed\\n        this.on('error', reject);\\n      });\\n      this.load();\\n    }\\n    get enabled() {\\n      const {\\n        config\\n      } = this;\\n      return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n    }\\n    // Build the tag URL\\n    get tagUrl() {\\n      const {\\n        config\\n      } = this;\\n      if (is.url(config.tagUrl)) {\\n        return config.tagUrl;\\n      }\\n      const params = {\\n        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n        AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n        AV_URL: window.location.hostname,\\n        cb: Date.now(),\\n        AV_WIDTH: 640,\\n        AV_HEIGHT: 480,\\n        AV_CDIM2: config.publisherId\\n      };\\n      const base = 'https://go.aniview.com/api/adserver6/vast/';\\n      return `${base}?${buildUrlParams(params)}`;\\n    }\\n  }\\n\\n  /**\\n   * Returns a number whose value is limited to the given range.\\n   *\\n   * Example: limit the output of this computation to between 0 and 255\\n   * (x * 255).clamp(0, 255)\\n   *\\n   * @param {Number} input\\n   * @param {Number} min The lower boundary of the output range\\n   * @param {Number} max The upper boundary of the output range\\n   * @returns A number within the bounds of min and max\\n   * @type Number\\n   */\\n  function clamp(input = 0, min = 0, max = 255) {\\n    return Math.min(Math.max(input, min), max);\\n  }\\n\\n  // Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\n  const parseVtt = vttDataString => {\\n    const processedList = [];\\n    const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n    frames.forEach(frame => {\\n      const result = {};\\n      const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n      lines.forEach(line => {\\n        if (!is.number(result.startTime)) {\\n          // The line with start and end times on it is the first line of interest\\n          const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n          if (matchTimes) {\\n            result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n            result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n          }\\n        } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n          // If we already have the startTime, then we're definitely up to the text line(s)\\n          const lineSplit = line.trim().split('#xywh=');\\n          [result.text] = lineSplit;\\n\\n          // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n          if (lineSplit[1]) {\\n            [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n          }\\n        }\\n      });\\n      if (result.text) {\\n        processedList.push(result);\\n      }\\n    });\\n    return processedList;\\n  };\\n\\n  /**\\n   * Preview thumbnails for seek hover and scrubbing\\n   * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n   * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n   *\\n   * Notes:\\n   * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n   * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n   * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n   */\\n\\n  const fitRatio = (ratio, outer) => {\\n    const targetRatio = outer.width / outer.height;\\n    const result = {};\\n    if (ratio > targetRatio) {\\n      result.width = outer.width;\\n      result.height = 1 / ratio * outer.width;\\n    } else {\\n      result.height = outer.height;\\n      result.width = ratio * outer.height;\\n    }\\n    return result;\\n  };\\n  class PreviewThumbnails {\\n    /**\\n     * PreviewThumbnails constructor.\\n     * @param {Plyr} player\\n     * @return {PreviewThumbnails}\\n     */\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        // Toggle the regular seek tooltip\\n        if (this.player.elements.display.seekTooltip) {\\n          this.player.elements.display.seekTooltip.hidden = this.enabled;\\n        }\\n        if (!this.enabled) return;\\n        this.getThumbnails().then(() => {\\n          if (!this.enabled) {\\n            return;\\n          }\\n\\n          // Render DOM elements\\n          this.render();\\n\\n          // Check to see if thumb container size was specified manually in CSS\\n          this.determineContainerAutoSizing();\\n\\n          // Set up listeners\\n          this.listeners();\\n          this.loaded = true;\\n        });\\n      });\\n      // Download VTT files and parse them\\n      _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n        return new Promise(resolve => {\\n          const {\\n            src\\n          } = this.player.config.previewThumbnails;\\n          if (is.empty(src)) {\\n            throw new Error('Missing previewThumbnails.src config attribute');\\n          }\\n\\n          // Resolve promise\\n          const sortAndResolve = () => {\\n            // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n            this.thumbnails.sort((x, y) => x.height - y.height);\\n            this.player.debug.log('Preview thumbnails', this.thumbnails);\\n            resolve();\\n          };\\n\\n          // Via callback()\\n          if (is.function(src)) {\\n            src(thumbnails => {\\n              this.thumbnails = thumbnails;\\n              sortAndResolve();\\n            });\\n          }\\n          // VTT urls\\n          else {\\n            // If string, convert into single-element list\\n            const urls = is.string(src) ? [src] : src;\\n            // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n            const promises = urls.map(u => this.getThumbnail(u));\\n            // Resolve\\n            Promise.all(promises).then(sortAndResolve);\\n          }\\n        });\\n      });\\n      // Process individual VTT file\\n      _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n        return new Promise(resolve => {\\n          fetch(url).then(response => {\\n            const thumbnail = {\\n              frames: parseVtt(response),\\n              height: null,\\n              urlPrefix: ''\\n            };\\n\\n            // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n            // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n            // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n            if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n              thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n            }\\n\\n            // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n            const tempImage = new Image();\\n            tempImage.onload = () => {\\n              thumbnail.height = tempImage.naturalHeight;\\n              thumbnail.width = tempImage.naturalWidth;\\n              this.thumbnails.push(thumbnail);\\n              resolve();\\n            };\\n            tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n          });\\n        });\\n      });\\n      _defineProperty$1(this, \\\"startMove\\\", event => {\\n        if (!this.loaded) return;\\n        if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n        // Wait until media has a duration\\n        if (!this.player.media.duration) return;\\n        if (event.type === 'touchmove') {\\n          // Calculate seek hover position as approx video seconds\\n          this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n        } else {\\n          var _this$player$config$m, _this$player$config$m2;\\n          // Calculate seek hover position as approx video seconds\\n          const clientRect = this.player.elements.progress.getBoundingClientRect();\\n          const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n          this.seekTime = this.player.media.duration * (percentage / 100);\\n          if (this.seekTime < 0) {\\n            // The mousemove fires for 10+px out to the left\\n            this.seekTime = 0;\\n          }\\n          if (this.seekTime > this.player.media.duration - 1) {\\n            // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n            this.seekTime = this.player.media.duration - 1;\\n          }\\n          this.mousePosX = event.pageX;\\n\\n          // Set time text inside image container\\n          this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n          // Get marker point for time\\n          const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n            time: t\\n          }) => t === Math.round(this.seekTime));\\n\\n          // Append the point label to the tooltip\\n          if (point) {\\n            // this.elements.thumb.time.innerText.concat('\\\\n');\\n            this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n          }\\n        }\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      });\\n      _defineProperty$1(this, \\\"endMove\\\", () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n        // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n        if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n          this.mouseDown = true;\\n\\n          // Wait until media has a duration\\n          if (this.player.media.duration) {\\n            this.toggleScrubbingContainer(true);\\n            this.toggleThumbContainer(false, true);\\n\\n            // Download and show image\\n            this.showImageAtCurrentTime();\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n        this.mouseDown = false;\\n\\n        // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n        if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n          // The video was already seeked/loaded at the chosen time - hide immediately\\n          this.toggleScrubbingContainer(false);\\n        } else {\\n          // The video hasn't seeked yet. Wait for that\\n          once.call(this.player, this.player.media, 'timeupdate', () => {\\n            // Re-check mousedown - we might have already started scrubbing again\\n            if (!this.mouseDown) {\\n              this.toggleScrubbingContainer(false);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n        this.player.on('play', () => {\\n          this.toggleThumbContainer(false, true);\\n        });\\n        this.player.on('seeked', () => {\\n          this.toggleThumbContainer(false);\\n        });\\n        this.player.on('timeupdate', () => {\\n          this.lastTime = this.player.media.currentTime;\\n        });\\n      });\\n      /**\\n       * Create HTML elements for image containers\\n       */\\n      _defineProperty$1(this, \\\"render\\\", () => {\\n        // Create HTML element: plyr__preview-thumbnail-container\\n        this.elements.thumb.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.thumbContainer\\n        });\\n\\n        // Wrapper for the image for styling\\n        this.elements.thumb.imageContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.imageContainer\\n        });\\n        this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n        // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n        const timeContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.timeContainer\\n        });\\n        this.elements.thumb.time = createElement('span', {}, '00:00');\\n        timeContainer.appendChild(this.elements.thumb.time);\\n        this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n        // Inject the whole thumb\\n        if (is.element(this.player.elements.progress)) {\\n          this.player.elements.progress.appendChild(this.elements.thumb.container);\\n        }\\n\\n        // Create HTML element: plyr__preview-scrubbing-container\\n        this.elements.scrubbing.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n        });\\n        this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n      });\\n      _defineProperty$1(this, \\\"destroy\\\", () => {\\n        if (this.elements.thumb.container) {\\n          this.elements.thumb.container.remove();\\n        }\\n        if (this.elements.scrubbing.container) {\\n          this.elements.scrubbing.container.remove();\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n        if (this.mouseDown) {\\n          this.setScrubbingContainerSize();\\n        } else {\\n          this.setThumbContainerSizeAndPos();\\n        }\\n\\n        // Find the desired thumbnail index\\n        // TODO: Handle a video longer than the thumbs where thumbNum is null\\n        const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n        const hasThumb = thumbNum >= 0;\\n        let qualityIndex = 0;\\n\\n        // Show the thumb container if we're not scrubbing\\n        if (!this.mouseDown) {\\n          this.toggleThumbContainer(hasThumb);\\n        }\\n\\n        // No matching thumb found\\n        if (!hasThumb) {\\n          return;\\n        }\\n\\n        // Check to see if we've already downloaded higher quality versions of this image\\n        this.thumbnails.forEach((thumbnail, index) => {\\n          if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n            qualityIndex = index;\\n          }\\n        });\\n\\n        // Only proceed if either thumb num or thumbfilename has changed\\n        if (thumbNum !== this.showingThumb) {\\n          this.showingThumb = thumbNum;\\n          this.loadImage(qualityIndex);\\n        }\\n      });\\n      // Show the image that's currently specified in this.showingThumb\\n      _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n        const thumbNum = this.showingThumb;\\n        const thumbnail = this.thumbnails[qualityIndex];\\n        const {\\n          urlPrefix\\n        } = thumbnail;\\n        const frame = thumbnail.frames[thumbNum];\\n        const thumbFilename = thumbnail.frames[thumbNum].text;\\n        const thumbUrl = urlPrefix + thumbFilename;\\n        if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n          // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n          // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n          if (this.loadingImage && this.usingSprites) {\\n            this.loadingImage.onload = null;\\n          }\\n\\n          // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n          // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n          // images causes a flicker. Putting a new image over the top does not\\n          const previewImage = new Image();\\n          previewImage.src = thumbUrl;\\n          previewImage.dataset.index = thumbNum;\\n          previewImage.dataset.filename = thumbFilename;\\n          this.showingThumbFilename = thumbFilename;\\n          this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n          // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n          previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n          this.loadingImage = previewImage;\\n          this.removeOldImages(previewImage);\\n        } else {\\n          // Update the existing image\\n          this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n          this.currentImageElement.dataset.index = thumbNum;\\n          this.removeOldImages(this.currentImageElement);\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n        this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n        this.setImageSizeAndOffset(previewImage, frame);\\n        if (newImage) {\\n          this.currentImageContainer.appendChild(previewImage);\\n          this.currentImageElement = previewImage;\\n          if (!this.loadedImages.includes(thumbFilename)) {\\n            this.loadedImages.push(thumbFilename);\\n          }\\n        }\\n\\n        // Preload images before and after the current one\\n        // Show higher quality of the same frame\\n        // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n        this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n      });\\n      // Remove all preview images that aren't the designated current image\\n      _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n        // Get a list of all images, convert it from a DOM list to an array\\n        Array.from(this.currentImageContainer.children).forEach(image => {\\n          if (image.tagName.toLowerCase() !== 'img') {\\n            return;\\n          }\\n          const removeDelay = this.usingSprites ? 500 : 1000;\\n          if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n            // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n            // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n            // eslint-disable-next-line no-param-reassign\\n            image.dataset.deleting = true;\\n\\n            // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n            const {\\n              currentImageContainer\\n            } = this;\\n            setTimeout(() => {\\n              currentImageContainer.removeChild(image);\\n              this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n            }, removeDelay);\\n          }\\n        });\\n      });\\n      // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n      // This will only preload the lowest quality\\n      _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n        return new Promise(resolve => {\\n          setTimeout(() => {\\n            const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n            if (this.showingThumbFilename === oldThumbFilename) {\\n              // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n              let thumbnailsClone;\\n              if (forward) {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n              } else {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n              }\\n              let foundOne = false;\\n              thumbnailsClone.forEach(frame => {\\n                const newThumbFilename = frame.text;\\n                if (newThumbFilename !== oldThumbFilename) {\\n                  // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                  if (!this.loadedImages.includes(newThumbFilename)) {\\n                    foundOne = true;\\n                    this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                    const {\\n                      urlPrefix\\n                    } = this.thumbnails[0];\\n                    const thumbURL = urlPrefix + newThumbFilename;\\n                    const previewImage = new Image();\\n                    previewImage.src = thumbURL;\\n                    previewImage.onload = () => {\\n                      this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                      if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                      // We don't resolve until the thumb is loaded\\n                      resolve();\\n                    };\\n                  }\\n                }\\n              });\\n\\n              // If there are none to preload then we want to resolve immediately\\n              if (!foundOne) {\\n                resolve();\\n              }\\n            }\\n          }, 300);\\n        });\\n      });\\n      // If user has been hovering current image for half a second, look for a higher quality one\\n      _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n        if (currentQualityIndex < this.thumbnails.length - 1) {\\n          // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n          let previewImageHeight = previewImage.naturalHeight;\\n          if (this.usingSprites) {\\n            previewImageHeight = frame.h;\\n          }\\n          if (previewImageHeight < this.thumbContainerHeight) {\\n            // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n            setTimeout(() => {\\n              // Make sure the mouse hasn't already moved on and started hovering at another image\\n              if (this.showingThumbFilename === thumbFilename) {\\n                this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n                this.loadImage(currentQualityIndex + 1);\\n              }\\n            }, 300);\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n        this.elements.thumb.container.classList.toggle(className, toggle);\\n        if (!toggle && clearShowing) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n        this.elements.scrubbing.container.classList.toggle(className, toggle);\\n        if (!toggle) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n        if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n          // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n          this.sizeSpecifiedInCSS = true;\\n        }\\n      });\\n      // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n      _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n        const {\\n          imageContainer\\n        } = this.elements.thumb;\\n        if (!this.sizeSpecifiedInCSS) {\\n          const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n          imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n          const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n          const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n          imageContainer.style.height = `${thumbHeight}px`;\\n        }\\n        this.setThumbContainerPos();\\n      });\\n      _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n        const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n        const containerRect = this.player.elements.container.getBoundingClientRect();\\n        const {\\n          container\\n        } = this.elements.thumb;\\n        // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n        const min = containerRect.left - scrubberRect.left + 10;\\n        const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n        // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n        const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n        const clamped = clamp(position, min, max);\\n\\n        // Move the popover position\\n        container.style.left = `${clamped}px`;\\n\\n        // The arrow can follow the cursor\\n        container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n      });\\n      // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n      _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n        const {\\n          width,\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        this.elements.scrubbing.container.style.width = `${width}px`;\\n        this.elements.scrubbing.container.style.height = `${height}px`;\\n      });\\n      // Sprites need to be offset to the correct location\\n      _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n        if (!this.usingSprites) return;\\n\\n        // Find difference between height and preview container height\\n        const multiplier = this.thumbContainerHeight / frame.h;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.left = `-${frame.x * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.top = `-${frame.y * multiplier}px`;\\n      });\\n      this.player = player;\\n      this.thumbnails = [];\\n      this.loaded = false;\\n      this.lastMouseMoveTime = Date.now();\\n      this.mouseDown = false;\\n      this.loadedImages = [];\\n      this.elements = {\\n        thumb: {},\\n        scrubbing: {}\\n      };\\n      this.load();\\n    }\\n    get enabled() {\\n      return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n    }\\n    get currentImageContainer() {\\n      return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n    }\\n    get usingSprites() {\\n      return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n    }\\n    get thumbAspectRatio() {\\n      if (this.usingSprites) {\\n        return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n      }\\n      return this.thumbnails[0].width / this.thumbnails[0].height;\\n    }\\n    get thumbContainerHeight() {\\n      if (this.mouseDown) {\\n        const {\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        return height;\\n      }\\n\\n      // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n      if (this.sizeSpecifiedInCSS) {\\n        return this.elements.thumb.imageContainer.clientHeight;\\n      }\\n      return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n    }\\n    get currentImageElement() {\\n      return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n    }\\n    set currentImageElement(element) {\\n      if (this.mouseDown) {\\n        this.currentScrubbingImageElement = element;\\n      } else {\\n        this.currentThumbnailImageElement = element;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  const source = {\\n    // Add elements to HTML5 media (source, tracks, etc)\\n    insertElements(type, attributes) {\\n      if (is.string(attributes)) {\\n        insertElement(type, this.media, {\\n          src: attributes\\n        });\\n      } else if (is.array(attributes)) {\\n        attributes.forEach(attribute => {\\n          insertElement(type, this.media, attribute);\\n        });\\n      }\\n    },\\n    // Update source\\n    // Sources are not checked for support so be careful\\n    change(input) {\\n      if (!getDeep(input, 'sources.length')) {\\n        this.debug.warn('Invalid source format');\\n        return;\\n      }\\n\\n      // Cancel current network requests\\n      html5.cancelRequests.call(this);\\n\\n      // Destroy instance and re-setup\\n      this.destroy.call(this, () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const {\\n          sources,\\n          type\\n        } = input;\\n        const [{\\n          provider = providers.html5,\\n          src\\n        }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : {\\n          src\\n        };\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes)\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      }, true);\\n    }\\n  };\\n\\n  // Private properties\\n  // TODO: Use a WeakMap for private globals\\n  // const globals = new WeakMap();\\n\\n  // Plyr instance\\n  class Plyr {\\n    constructor(target, options) {\\n      /**\\n       * Play the media, or play the advertisement (if they are not blocked)\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        if (!is.function(this.media.play)) {\\n          return null;\\n        }\\n\\n        // Intecept play with ads\\n        if (this.ads && this.ads.enabled) {\\n          this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n        }\\n\\n        // Return the promise (for HTML5)\\n        return this.media.play();\\n      });\\n      /**\\n       * Pause the media\\n       */\\n      _defineProperty$1(this, \\\"pause\\\", () => {\\n        if (!this.playing || !is.function(this.media.pause)) {\\n          return null;\\n        }\\n        return this.media.pause();\\n      });\\n      /**\\n       * Toggle playback based on current status\\n       * @param {Boolean} input\\n       */\\n      _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n        // Toggle based on current state if nothing passed\\n        const toggle = is.boolean(input) ? input : !this.playing;\\n        if (toggle) {\\n          return this.play();\\n        }\\n        return this.pause();\\n      });\\n      /**\\n       * Stop playback\\n       */\\n      _defineProperty$1(this, \\\"stop\\\", () => {\\n        if (this.isHTML5) {\\n          this.pause();\\n          this.restart();\\n        } else if (is.function(this.media.stop)) {\\n          this.media.stop();\\n        }\\n      });\\n      /**\\n       * Restart playback\\n       */\\n      _defineProperty$1(this, \\\"restart\\\", () => {\\n        this.currentTime = 0;\\n      });\\n      /**\\n       * Rewind\\n       * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n        this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Fast forward\\n       * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n        this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Increase volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n        const volume = this.media.muted ? 0 : this.volume;\\n        this.volume = volume + (is.number(step) ? step : 0);\\n      });\\n      /**\\n       * Decrease volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n        this.increaseVolume(-step);\\n      });\\n      /**\\n       * Trigger the airplay dialog\\n       * TODO: update player with state, support, enabled\\n       */\\n      _defineProperty$1(this, \\\"airplay\\\", () => {\\n        // Show dialog if supported\\n        if (support.airplay) {\\n          this.media.webkitShowPlaybackTargetPicker();\\n        }\\n      });\\n      /**\\n       * Toggle the player controls\\n       * @param {Boolean} [toggle] - Whether to show the controls\\n       */\\n      _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n        // Don't toggle if missing UI support or if it's audio\\n        if (this.supported.ui && !this.isAudio) {\\n          // Get state before change\\n          const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n          // Negate the argument if not undefined since adding the class to hides the controls\\n          const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n          // Apply and get updated state\\n          const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n          // Close menu\\n          if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n            controls.toggleMenu.call(this, false);\\n          }\\n\\n          // Trigger event on change\\n          if (hiding !== isHidden) {\\n            const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n            triggerEvent.call(this, this.media, eventName);\\n          }\\n          return !hiding;\\n        }\\n        return false;\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        on.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Add event listeners once\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n        once.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Remove event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n        off(this.elements.container, event, callback);\\n      });\\n      /**\\n       * Destroy an instance\\n       * Event listeners are removed when elements are removed\\n       * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n       * @param {Function} callback - Callback for when destroy is complete\\n       * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n       */\\n      _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n        if (!this.ready) {\\n          return;\\n        }\\n        const done = () => {\\n          // Reset overflow (incase destroyed while in fullscreen)\\n          document.body.style.overflow = '';\\n\\n          // GC for embed\\n          this.embed = null;\\n\\n          // If it's a soft destroy, make minimal changes\\n          if (soft) {\\n            if (Object.keys(this.elements).length) {\\n              // Remove elements\\n              removeElement(this.elements.buttons.play);\\n              removeElement(this.elements.captions);\\n              removeElement(this.elements.controls);\\n              removeElement(this.elements.wrapper);\\n\\n              // Clear for GC\\n              this.elements.buttons.play = null;\\n              this.elements.captions = null;\\n              this.elements.controls = null;\\n              this.elements.wrapper = null;\\n            }\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback();\\n            }\\n          } else {\\n            // Unbind listeners\\n            unbindListeners.call(this);\\n\\n            // Cancel current network requests\\n            html5.cancelRequests.call(this);\\n\\n            // Replace the container with the original element provided\\n            replaceElement(this.elements.original, this.elements.container);\\n\\n            // Event\\n            triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback.call(this.elements.original);\\n            }\\n\\n            // Reset state\\n            this.ready = false;\\n\\n            // Clear for garbage collection\\n            setTimeout(() => {\\n              this.elements = null;\\n              this.media = null;\\n            }, 200);\\n          }\\n        };\\n\\n        // Stop playback\\n        this.stop();\\n\\n        // Clear timeouts\\n        clearTimeout(this.timers.loading);\\n        clearTimeout(this.timers.controls);\\n        clearTimeout(this.timers.resized);\\n\\n        // Provider specific stuff\\n        if (this.isHTML5) {\\n          // Restore native video controls\\n          ui.toggleNativeControls.call(this, true);\\n\\n          // Clean up\\n          done();\\n        } else if (this.isYouTube) {\\n          // Clear timers\\n          clearInterval(this.timers.buffering);\\n          clearInterval(this.timers.playing);\\n\\n          // Destroy YouTube API\\n          if (this.embed !== null && is.function(this.embed.destroy)) {\\n            this.embed.destroy();\\n          }\\n\\n          // Clean up\\n          done();\\n        } else if (this.isVimeo) {\\n          // Destroy Vimeo API\\n          // then clean up (wait, to prevent postmessage errors)\\n          if (this.embed !== null) {\\n            this.embed.unload().then(done);\\n          }\\n\\n          // Vimeo does not always return\\n          setTimeout(done, 200);\\n        }\\n      });\\n      /**\\n       * Check for support for a mime type (HTML5 only)\\n       * @param {String} type - Mime type\\n       */\\n      _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n      this.timers = {};\\n\\n      // State\\n      this.ready = false;\\n      this.loading = false;\\n      this.failed = false;\\n\\n      // Touch device\\n      this.touch = support.touch;\\n\\n      // Set the media element\\n      this.media = target;\\n\\n      // String selector passed\\n      if (is.string(this.media)) {\\n        this.media = document.querySelectorAll(this.media);\\n      }\\n\\n      // jQuery, NodeList or Array passed, use first element\\n      if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n        // eslint-disable-next-line\\n        this.media = this.media[0];\\n      }\\n\\n      // Set config\\n      this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })());\\n\\n      // Elements cache\\n      this.elements = {\\n        container: null,\\n        fullscreen: null,\\n        captions: null,\\n        buttons: {},\\n        display: {},\\n        progress: {},\\n        inputs: {},\\n        settings: {\\n          popup: null,\\n          menu: null,\\n          panels: {},\\n          buttons: {}\\n        }\\n      };\\n\\n      // Captions\\n      this.captions = {\\n        active: null,\\n        currentTrack: -1,\\n        meta: new WeakMap()\\n      };\\n\\n      // Fullscreen\\n      this.fullscreen = {\\n        active: false\\n      };\\n\\n      // Options\\n      this.options = {\\n        speed: [],\\n        quality: []\\n      };\\n\\n      // Debugging\\n      // TODO: move to globals\\n      this.debug = new Console(this.config.debug);\\n\\n      // Log config options and support\\n      this.debug.log('Config', this.config);\\n      this.debug.log('Support', support);\\n\\n      // We need an element to setup\\n      if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n        this.debug.error('Setup failed: no suitable element passed');\\n        return;\\n      }\\n\\n      // Bail if the element is initialized\\n      if (this.media.plyr) {\\n        this.debug.warn('Target already setup');\\n        return;\\n      }\\n\\n      // Bail if not enabled\\n      if (!this.config.enabled) {\\n        this.debug.error('Setup failed: disabled by config');\\n        return;\\n      }\\n\\n      // Bail if disabled or no basic support\\n      // You may want to disable certain UAs etc\\n      if (!support.check().api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n\\n      // Cache original element state for .destroy()\\n      const clone = this.media.cloneNode(true);\\n      clone.autoplay = false;\\n      this.elements.original = clone;\\n\\n      // Set media type based on tag or data attribute\\n      // Supported: video, audio, vimeo, youtube\\n      const _type = this.media.tagName.toLowerCase();\\n      // Embed properties\\n      let iframe = null;\\n      let url = null;\\n\\n      // Different setup based on type\\n      switch (_type) {\\n        case 'div':\\n          // Find the frame\\n          iframe = this.media.querySelector('iframe');\\n\\n          // <iframe> type\\n          if (is.element(iframe)) {\\n            // Detect provider\\n            url = parseUrl(iframe.getAttribute('src'));\\n            this.provider = getProviderByUrl(url.toString());\\n\\n            // Rework elements\\n            this.elements.container = this.media;\\n            this.media = iframe;\\n\\n            // Reset classname\\n            this.elements.container.className = '';\\n\\n            // Get attributes from URL and set config\\n            if (url.search.length) {\\n              const truthy = ['1', 'true'];\\n              if (truthy.includes(url.searchParams.get('autoplay'))) {\\n                this.config.autoplay = true;\\n              }\\n              if (truthy.includes(url.searchParams.get('loop'))) {\\n                this.config.loop.active = true;\\n              }\\n\\n              // TODO: replace fullscreen.iosNative with this playsinline config option\\n              // YouTube requires the playsinline in the URL\\n              if (this.isYouTube) {\\n                this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n                this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n              } else {\\n                this.config.playsinline = true;\\n              }\\n            }\\n          } else {\\n            // <div> with attributes\\n            this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n            // Remove attribute\\n            this.media.removeAttribute(this.config.attributes.embed.provider);\\n          }\\n\\n          // Unsupported or missing provider\\n          if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n            this.debug.error('Setup failed: Invalid provider');\\n            return;\\n          }\\n\\n          // Audio will come later for external providers\\n          this.type = types.video;\\n          break;\\n        case 'video':\\n        case 'audio':\\n          this.type = _type;\\n          this.provider = providers.html5;\\n\\n          // Get config from attributes\\n          if (this.media.hasAttribute('crossorigin')) {\\n            this.config.crossorigin = true;\\n          }\\n          if (this.media.hasAttribute('autoplay')) {\\n            this.config.autoplay = true;\\n          }\\n          if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n            this.config.playsinline = true;\\n          }\\n          if (this.media.hasAttribute('muted')) {\\n            this.config.muted = true;\\n          }\\n          if (this.media.hasAttribute('loop')) {\\n            this.config.loop.active = true;\\n          }\\n          break;\\n        default:\\n          this.debug.error('Setup failed: unsupported type');\\n          return;\\n      }\\n\\n      // Check for support again but with type\\n      this.supported = support.check(this.type, this.provider);\\n\\n      // If no support for even API, bail\\n      if (!this.supported.api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n      this.eventListeners = [];\\n\\n      // Create listeners\\n      this.listeners = new Listeners(this);\\n\\n      // Setup local storage for user settings\\n      this.storage = new Storage(this);\\n\\n      // Store reference\\n      this.media.plyr = this;\\n\\n      // Wrap media\\n      if (!is.element(this.elements.container)) {\\n        this.elements.container = createElement('div');\\n        wrap(this.media, this.elements.container);\\n      }\\n\\n      // Migrate custom properties from media to container (so they work 😉)\\n      ui.migrateStyles.call(this);\\n\\n      // Add style hook\\n      ui.addStyleHook.call(this);\\n\\n      // Setup media\\n      media.setup.call(this);\\n\\n      // Listen for events if debugging\\n      if (this.config.debug) {\\n        on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n          this.debug.log(`event: ${event.type}`);\\n        });\\n      }\\n\\n      // Setup fullscreen\\n      this.fullscreen = new Fullscreen(this);\\n\\n      // Setup interface\\n      // If embed but not fully supported, build interface now to avoid flash of controls\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        ui.build.call(this);\\n      }\\n\\n      // Container listeners\\n      this.listeners.container();\\n\\n      // Global listeners\\n      this.listeners.global();\\n\\n      // Setup ads if provided\\n      if (this.config.ads.enabled) {\\n        this.ads = new Ads(this);\\n      }\\n\\n      // Autoplay if required\\n      if (this.isHTML5 && this.config.autoplay) {\\n        this.once('canplay', () => silencePromise(this.play()));\\n      }\\n\\n      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n      this.lastSeekTime = 0;\\n\\n      // Setup preview thumbnails if enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n\\n    // ---------------------------------------\\n    // API\\n    // ---------------------------------------\\n\\n    /**\\n     * Types and provider helpers\\n     */\\n    get isHTML5() {\\n      return this.provider === providers.html5;\\n    }\\n    get isEmbed() {\\n      return this.isYouTube || this.isVimeo;\\n    }\\n    get isYouTube() {\\n      return this.provider === providers.youtube;\\n    }\\n    get isVimeo() {\\n      return this.provider === providers.vimeo;\\n    }\\n    get isVideo() {\\n      return this.type === types.video;\\n    }\\n    get isAudio() {\\n      return this.type === types.audio;\\n    }\\n    /**\\n     * Get playing state\\n     */\\n    get playing() {\\n      return Boolean(this.ready && !this.paused && !this.ended);\\n    }\\n\\n    /**\\n     * Get paused state\\n     */\\n    get paused() {\\n      return Boolean(this.media.paused);\\n    }\\n\\n    /**\\n     * Get stopped state\\n     */\\n    get stopped() {\\n      return Boolean(this.paused && this.currentTime === 0);\\n    }\\n\\n    /**\\n     * Get ended state\\n     */\\n    get ended() {\\n      return Boolean(this.media.ended);\\n    }\\n    /**\\n     * Seek to a time\\n     * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n     */\\n    set currentTime(input) {\\n      // Bail if media duration isn't available yet\\n      if (!this.duration) {\\n        return;\\n      }\\n\\n      // Validate input\\n      const inputIsValid = is.number(input) && input > 0;\\n\\n      // Set\\n      this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n      // Logging\\n      this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n    }\\n\\n    /**\\n     * Get current time\\n     */\\n    get currentTime() {\\n      return Number(this.media.currentTime);\\n    }\\n\\n    /**\\n     * Get buffered\\n     */\\n    get buffered() {\\n      const {\\n        buffered\\n      } = this.media;\\n\\n      // YouTube / Vimeo return a float between 0-1\\n      if (is.number(buffered)) {\\n        return buffered;\\n      }\\n\\n      // HTML5\\n      // TODO: Handle buffered chunks of the media\\n      // (i.e. seek to another section buffers only that section)\\n      if (buffered && buffered.length && this.duration > 0) {\\n        return buffered.end(0) / this.duration;\\n      }\\n      return 0;\\n    }\\n\\n    /**\\n     * Get seeking status\\n     */\\n    get seeking() {\\n      return Boolean(this.media.seeking);\\n    }\\n\\n    /**\\n     * Get the duration of the current media\\n     */\\n    get duration() {\\n      // Faux duration set via config\\n      const fauxDuration = parseFloat(this.config.duration);\\n      // Media duration can be NaN or Infinity before the media has loaded\\n      const realDuration = (this.media || {}).duration;\\n      const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n      // If config duration is funky, use regular duration\\n      return fauxDuration || duration;\\n    }\\n\\n    /**\\n     * Set the player volume\\n     * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n     */\\n    set volume(value) {\\n      let volume = value;\\n      const max = 1;\\n      const min = 0;\\n      if (is.string(volume)) {\\n        volume = Number(volume);\\n      }\\n\\n      // Load volume from storage if no value specified\\n      if (!is.number(volume)) {\\n        volume = this.storage.get('volume');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.number(volume)) {\\n        ({\\n          volume\\n        } = this.config);\\n      }\\n\\n      // Maximum is volumeMax\\n      if (volume > max) {\\n        volume = max;\\n      }\\n      // Minimum is volumeMin\\n      if (volume < min) {\\n        volume = min;\\n      }\\n\\n      // Update config\\n      this.config.volume = volume;\\n\\n      // Set the player volume\\n      this.media.volume = volume;\\n\\n      // If muted, and we're increasing volume manually, reset muted state\\n      if (!is.empty(value) && this.muted && volume > 0) {\\n        this.muted = false;\\n      }\\n    }\\n\\n    /**\\n     * Get the current player volume\\n     */\\n    get volume() {\\n      return Number(this.media.volume);\\n    }\\n    /**\\n     * Set muted state\\n     * @param {Boolean} mute\\n     */\\n    set muted(mute) {\\n      let toggle = mute;\\n\\n      // Load muted state from storage\\n      if (!is.boolean(toggle)) {\\n        toggle = this.storage.get('muted');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.boolean(toggle)) {\\n        toggle = this.config.muted;\\n      }\\n\\n      // Update config\\n      this.config.muted = toggle;\\n\\n      // Set mute on the player\\n      this.media.muted = toggle;\\n    }\\n\\n    /**\\n     * Get current muted state\\n     */\\n    get muted() {\\n      return Boolean(this.media.muted);\\n    }\\n\\n    /**\\n     * Check if the media has audio\\n     */\\n    get hasAudio() {\\n      // Assume yes for all non HTML5 (as we can't tell...)\\n      if (!this.isHTML5) {\\n        return true;\\n      }\\n      if (this.isAudio) {\\n        return true;\\n      }\\n\\n      // Get audio tracks\\n      return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n    }\\n\\n    /**\\n     * Set playback speed\\n     * @param {Number} input - the speed of playback (0.5-2.0)\\n     */\\n    set speed(input) {\\n      let speed = null;\\n      if (is.number(input)) {\\n        speed = input;\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.storage.get('speed');\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.config.speed.selected;\\n      }\\n\\n      // Clamp to min/max\\n      const {\\n        minimumSpeed: min,\\n        maximumSpeed: max\\n      } = this;\\n      speed = clamp(speed, min, max);\\n\\n      // Update config\\n      this.config.speed.selected = speed;\\n\\n      // Set media speed\\n      setTimeout(() => {\\n        if (this.media) {\\n          this.media.playbackRate = speed;\\n        }\\n      }, 0);\\n    }\\n\\n    /**\\n     * Get current playback speed\\n     */\\n    get speed() {\\n      return Number(this.media.playbackRate);\\n    }\\n\\n    /**\\n     * Get the minimum allowed speed\\n     */\\n    get minimumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.min(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 0.5;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 0.0625;\\n    }\\n\\n    /**\\n     * Get the maximum allowed speed\\n     */\\n    get maximumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.max(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 2;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 16;\\n    }\\n\\n    /**\\n     * Set playback quality\\n     * Currently HTML5 & YouTube only\\n     * @param {Number} input - Quality level\\n     */\\n    set quality(input) {\\n      const config = this.config.quality;\\n      const options = this.options.quality;\\n      if (!options.length) {\\n        return;\\n      }\\n      let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n      let updateStorage = true;\\n      if (!options.includes(quality)) {\\n        const value = closest(options, quality);\\n        this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n        quality = value;\\n\\n        // Don't update storage if quality is not supported\\n        updateStorage = false;\\n      }\\n\\n      // Update config\\n      config.selected = quality;\\n\\n      // Set quality\\n      this.media.quality = quality;\\n\\n      // Save to storage\\n      if (updateStorage) {\\n        this.storage.set({\\n          quality\\n        });\\n      }\\n    }\\n\\n    /**\\n     * Get current quality level\\n     */\\n    get quality() {\\n      return this.media.quality;\\n    }\\n\\n    /**\\n     * Toggle loop\\n     * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n     * @param {Boolean} input - Whether to loop or not\\n     */\\n    set loop(input) {\\n      const toggle = is.boolean(input) ? input : this.config.loop.active;\\n      this.config.loop.active = toggle;\\n      this.media.loop = toggle;\\n\\n      // Set default to be a true toggle\\n      /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n           switch (type) {\\n              case 'start':\\n                  if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                      this.config.loop.end = null;\\n                  }\\n                  this.config.loop.start = this.currentTime;\\n                  // this.config.loop.indicator.start = this.elements.display.played.value;\\n                  break;\\n               case 'end':\\n                  if (this.config.loop.start >= this.currentTime) {\\n                      return this;\\n                  }\\n                  this.config.loop.end = this.currentTime;\\n                  // this.config.loop.indicator.end = this.elements.display.played.value;\\n                  break;\\n               case 'all':\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = this.duration - 2;\\n                  this.config.loop.indicator.start = 0;\\n                  this.config.loop.indicator.end = 100;\\n                  break;\\n               case 'toggle':\\n                  if (this.config.loop.active) {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = null;\\n                  } else {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = this.duration - 2;\\n                  }\\n                  break;\\n               default:\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = null;\\n                  break;\\n          } */\\n    }\\n\\n    /**\\n     * Get current loop state\\n     */\\n    get loop() {\\n      return Boolean(this.media.loop);\\n    }\\n\\n    /**\\n     * Set new media source\\n     * @param {Object} input - The new source object (see docs)\\n     */\\n    set source(input) {\\n      source.change.call(this, input);\\n    }\\n\\n    /**\\n     * Get current source\\n     */\\n    get source() {\\n      return this.media.currentSrc;\\n    }\\n\\n    /**\\n     * Get a download URL (either source or custom)\\n     */\\n    get download() {\\n      const {\\n        download\\n      } = this.config.urls;\\n      return is.url(download) ? download : this.source;\\n    }\\n\\n    /**\\n     * Set the download URL\\n     */\\n    set download(input) {\\n      if (!is.url(input)) {\\n        return;\\n      }\\n      this.config.urls.download = input;\\n      controls.setDownloadUrl.call(this);\\n    }\\n\\n    /**\\n     * Set the poster image for a video\\n     * @param {String} input - the URL for the new poster image\\n     */\\n    set poster(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Poster can only be set for video');\\n        return;\\n      }\\n      ui.setPoster.call(this, input, false).catch(() => {});\\n    }\\n\\n    /**\\n     * Get the current poster image\\n     */\\n    get poster() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n    }\\n\\n    /**\\n     * Get the current aspect ratio in use\\n     */\\n    get ratio() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n      return is.array(ratio) ? ratio.join(':') : ratio;\\n    }\\n\\n    /**\\n     * Set video aspect ratio\\n     */\\n    set ratio(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Aspect ratio can only be set for video');\\n        return;\\n      }\\n      if (!is.string(input) || !validateAspectRatio(input)) {\\n        this.debug.error(`Invalid aspect ratio specified (${input})`);\\n        return;\\n      }\\n      this.config.ratio = reduceAspectRatio(input);\\n      setAspectRatio.call(this);\\n    }\\n\\n    /**\\n     * Set the autoplay state\\n     * @param {Boolean} input - Whether to autoplay or not\\n     */\\n    set autoplay(input) {\\n      this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n    }\\n\\n    /**\\n     * Get the current autoplay state\\n     */\\n    get autoplay() {\\n      return Boolean(this.config.autoplay);\\n    }\\n\\n    /**\\n     * Toggle captions\\n     * @param {Boolean} input - Whether to enable captions\\n     */\\n    toggleCaptions(input) {\\n      captions.toggle.call(this, input, false);\\n    }\\n\\n    /**\\n     * Set the caption track by index\\n     * @param {Number} input - Caption index\\n     */\\n    set currentTrack(input) {\\n      captions.set.call(this, input, false);\\n      captions.setup.call(this);\\n    }\\n\\n    /**\\n     * Get the current caption track index (-1 if disabled)\\n     */\\n    get currentTrack() {\\n      const {\\n        toggled,\\n        currentTrack\\n      } = this.captions;\\n      return toggled ? currentTrack : -1;\\n    }\\n\\n    /**\\n     * Set the wanted language for captions\\n     * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n     * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n     */\\n    set language(input) {\\n      captions.setLanguage.call(this, input, false);\\n    }\\n\\n    /**\\n     * Get the current track's language\\n     */\\n    get language() {\\n      return (captions.getCurrentTrack.call(this) || {}).language;\\n    }\\n\\n    /**\\n     * Toggle picture-in-picture playback on WebKit/MacOS\\n     * TODO: update player with state, support, enabled\\n     * TODO: detect outside changes\\n     */\\n    set pip(input) {\\n      // Bail if no support\\n      if (!support.pip) {\\n        return;\\n      }\\n\\n      // Toggle based on current state if not passed\\n      const toggle = is.boolean(input) ? input : !this.pip;\\n\\n      // Toggle based on current state\\n      // Safari\\n      if (is.function(this.media.webkitSetPresentationMode)) {\\n        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n      }\\n\\n      // Chrome\\n      if (is.function(this.media.requestPictureInPicture)) {\\n        if (!this.pip && toggle) {\\n          this.media.requestPictureInPicture();\\n        } else if (this.pip && !toggle) {\\n          document.exitPictureInPicture();\\n        }\\n      }\\n    }\\n\\n    /**\\n     * Get the current picture-in-picture state\\n     */\\n    get pip() {\\n      if (!support.pip) {\\n        return null;\\n      }\\n\\n      // Safari\\n      if (!is.empty(this.media.webkitPresentationMode)) {\\n        return this.media.webkitPresentationMode === pip.active;\\n      }\\n\\n      // Chrome\\n      return this.media === document.pictureInPictureElement;\\n    }\\n\\n    /**\\n     * Sets the preview thumbnails for the current source\\n     */\\n    setPreviewThumbnails(thumbnailSource) {\\n      if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n        this.previewThumbnails.destroy();\\n        this.previewThumbnails = null;\\n      }\\n      Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n      // Create new instance if it is still enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n    /**\\n     * Check for support\\n     * @param {String} type - Player type (audio/video)\\n     * @param {String} provider - Provider (html5/youtube/vimeo)\\n     */\\n    static supported(type, provider) {\\n      return support.check(type, provider);\\n    }\\n\\n    /**\\n     * Load an SVG sprite into the page\\n     * @param {String} url - URL for the SVG sprite\\n     * @param {String} [id] - Unique ID\\n     */\\n    static loadSprite(url, id) {\\n      return loadSprite(url, id);\\n    }\\n\\n    /**\\n     * Setup multiple instances\\n     * @param {*} selector\\n     * @param {Object} options\\n     */\\n    static setup(selector, options = {}) {\\n      let targets = null;\\n      if (is.string(selector)) {\\n        targets = Array.from(document.querySelectorAll(selector));\\n      } else if (is.nodeList(selector)) {\\n        targets = Array.from(selector);\\n      } else if (is.array(selector)) {\\n        targets = selector.filter(is.element);\\n      }\\n      if (is.empty(targets)) {\\n        return null;\\n      }\\n      return targets.map(t => new Plyr(t, options));\\n    }\\n  }\\n  Plyr.defaults = cloneDeep(defaults);\\n\\n  return Plyr;\\n\\n}));\\n//# sourceMappingURL=plyr.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}',\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',\\n    },\\n  },\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\n+{\"version\":3,\"sources\":[\"plyr.js\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"navigator\",\"global\",\"factory\",\"exports\",\"module\",\"define\",\"amd\",\"globalThis\",\"self\",\"Plyr\",\"this\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"arg\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"call\",\"TypeError\",\"String\",\"Number\",\"_toPrimitive\",\"_toPropertyKey\",\"Object\",\"defineProperty\",\"enumerable\",\"configurable\",\"writable\",\"_defineProperties\",\"e\",\"t\",\"n\",\"length\",\"r\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isString\",\"isArray\",\"Array\",\"isNodeList\",\"NodeList\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"isNaN\",\"string\",\"boolean\",\"Boolean\",\"function\",\"Function\",\"array\",\"nodeList\",\"element\",\"Element\",\"event\",\"Event\",\"empty\",\"round\",\"concat\",\"match\",\"Math\",\"max\",\"getDecimalPlaces\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"_classCallCheck\",\"document\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"prototype\",\"_createClass\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"set\",\"target\",\"i\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"u\",\"c\",\"getBoundingClientRect\",\"a\",\"width\",\"clientX\",\"left\",\"disabled\",\"preventDefault\",\"get\",\"bubbles\",\"dispatchEvent\",\"trigger\",\"type\",\"from\",\"querySelectorAll\",\"MutationObserver\",\"addedNodes\",\"includes\",\"matches\",\"observe\",\"body\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isFunction\",\"isEmpty\",\"weakMap\",\"WeakMap\",\"nodeType\",\"ownerDocument\",\"textNode\",\"Text\",\"keyboardEvent\",\"KeyboardEvent\",\"cue\",\"window\",\"TextTrackCue\",\"VTTCue\",\"track\",\"TextTrack\",\"kind\",\"promise\",\"Promise\",\"then\",\"url\",\"URL\",\"startsWith\",\"hostname\",\"_\",\"transitionEndEvent\",\"createElement\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"browser\",\"isIE\",\"documentMode\",\"isEdge\",\"test\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"getDeep\",\"path\",\"split\",\"reduce\",\"extend\",\"sources\",\"source\",\"shift\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"appendChild\",\"insertBefore\",\"setAttributes\",\"attributes\",\"entries\",\"setAttribute\",\"text\",\"innerText\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"replace\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"method\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"callback\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"detail\",\"CustomEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"indexOf\",\"closest\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"parse\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"format\",\"toString\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"slice\",\"toLowerCase\",\"toCamelCase\",\"toPascalCase\",\"getHTML\",\"innerHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"JSON\",\"storage\",\"setItem\",\"stringify\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"Error\",\"status\",\"open\",\"send\",\"error\",\"loadSprite\",\"prefix\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"location\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"join\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"current\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sort\",\"b\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"values\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"href\",\"urls\",\"isEmbed\",\"inject\",\"floor\",\"random\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"createDocumentFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"params\",\"URLSearchParams\",\"isYouTube\",\"protocol\",\"blob\",\"createObjectURL\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"has\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"providers\",\"types\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"head\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"el\",\"parentElement\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"attribute\",\"hasAttribute\",\"done\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"fn\",\"createCommonjsModule\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"doc\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathname\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"defaultPrevented\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"loadScript\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"found\",\"parseHash\",\"hashParam\",\"sidedock\",\"gesture\",\"$2\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"fragment\",\"firstChild\",\"stripHTML\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"setInterval\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"Ads\",\"google\",\"ima\",\"manager\",\"destroy\",\"displayContainer\",\"remove\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"getProviderByUrl\",\"search\",\"truthy\",\"searchParams\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.min.js\",\"sourcesContent\":[\"typeof navigator === \\\"object\\\" && (function (global, factory) {\\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\\n  typeof define === 'function' && define.amd ? define('Plyr', factory) :\\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());\\n})(this, (function () { 'use strict';\\n\\n  function _defineProperty$1(obj, key, value) {\\n    key = _toPropertyKey(key);\\n    if (key in obj) {\\n      Object.defineProperty(obj, key, {\\n        value: value,\\n        enumerable: true,\\n        configurable: true,\\n        writable: true\\n      });\\n    } else {\\n      obj[key] = value;\\n    }\\n    return obj;\\n  }\\n  function _toPrimitive(input, hint) {\\n    if (typeof input !== \\\"object\\\" || input === null) return input;\\n    var prim = input[Symbol.toPrimitive];\\n    if (prim !== undefined) {\\n      var res = prim.call(input, hint || \\\"default\\\");\\n      if (typeof res !== \\\"object\\\") return res;\\n      throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n    }\\n    return (hint === \\\"string\\\" ? String : Number)(input);\\n  }\\n  function _toPropertyKey(arg) {\\n    var key = _toPrimitive(arg, \\\"string\\\");\\n    return typeof key === \\\"symbol\\\" ? key : String(key);\\n  }\\n\\n  function _classCallCheck(e, t) {\\n    if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n  }\\n  function _defineProperties(e, t) {\\n    for (var n = 0; n < t.length; n++) {\\n      var r = t[n];\\n      r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n    }\\n  }\\n  function _createClass(e, t, n) {\\n    return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n  }\\n  function _defineProperty(e, t, n) {\\n    return t in e ? Object.defineProperty(e, t, {\\n      value: n,\\n      enumerable: !0,\\n      configurable: !0,\\n      writable: !0\\n    }) : e[t] = n, e;\\n  }\\n  function ownKeys(e, t) {\\n    var n = Object.keys(e);\\n    if (Object.getOwnPropertySymbols) {\\n      var r = Object.getOwnPropertySymbols(e);\\n      t && (r = r.filter(function (t) {\\n        return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n      })), n.push.apply(n, r);\\n    }\\n    return n;\\n  }\\n  function _objectSpread2(e) {\\n    for (var t = 1; t < arguments.length; t++) {\\n      var n = null != arguments[t] ? arguments[t] : {};\\n      t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n        _defineProperty(e, t, n[t]);\\n      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n      });\\n    }\\n    return e;\\n  }\\n  var defaults$1 = {\\n    addCSS: !0,\\n    thumbWidth: 15,\\n    watch: !0\\n  };\\n  function matches$1(e, t) {\\n    return function () {\\n      return Array.from(document.querySelectorAll(t)).includes(this);\\n    }.call(e, t);\\n  }\\n  function trigger(e, t) {\\n    if (e && t) {\\n      var n = new Event(t, {\\n        bubbles: !0\\n      });\\n      e.dispatchEvent(n);\\n    }\\n  }\\n  var getConstructor$1 = function (e) {\\n      return null != e ? e.constructor : null;\\n    },\\n    instanceOf$1 = function (e, t) {\\n      return !!(e && t && e instanceof t);\\n    },\\n    isNullOrUndefined$1 = function (e) {\\n      return null == e;\\n    },\\n    isObject$1 = function (e) {\\n      return getConstructor$1(e) === Object;\\n    },\\n    isNumber$1 = function (e) {\\n      return getConstructor$1(e) === Number && !Number.isNaN(e);\\n    },\\n    isString$1 = function (e) {\\n      return getConstructor$1(e) === String;\\n    },\\n    isBoolean$1 = function (e) {\\n      return getConstructor$1(e) === Boolean;\\n    },\\n    isFunction$1 = function (e) {\\n      return getConstructor$1(e) === Function;\\n    },\\n    isArray$1 = function (e) {\\n      return Array.isArray(e);\\n    },\\n    isNodeList$1 = function (e) {\\n      return instanceOf$1(e, NodeList);\\n    },\\n    isElement$1 = function (e) {\\n      return instanceOf$1(e, Element);\\n    },\\n    isEvent$1 = function (e) {\\n      return instanceOf$1(e, Event);\\n    },\\n    isEmpty$1 = function (e) {\\n      return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n    },\\n    is$1 = {\\n      nullOrUndefined: isNullOrUndefined$1,\\n      object: isObject$1,\\n      number: isNumber$1,\\n      string: isString$1,\\n      boolean: isBoolean$1,\\n      function: isFunction$1,\\n      array: isArray$1,\\n      nodeList: isNodeList$1,\\n      element: isElement$1,\\n      event: isEvent$1,\\n      empty: isEmpty$1\\n    };\\n  function getDecimalPlaces(e) {\\n    var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n    return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n  }\\n  function round(e, t) {\\n    if (1 > t) {\\n      var n = getDecimalPlaces(t);\\n      return parseFloat(e.toFixed(n));\\n    }\\n    return Math.round(e / t) * t;\\n  }\\n  var RangeTouch = function () {\\n    function e(t, n) {\\n      _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n    }\\n    return _createClass(e, [{\\n      key: \\\"init\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n      }\\n    }, {\\n      key: \\\"destroy\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n      }\\n    }, {\\n      key: \\\"listeners\\\",\\n      value: function (e) {\\n        var t = this,\\n          n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n        [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n          t.element[n](e, function (e) {\\n            return t.set(e);\\n          }, !1);\\n        });\\n      }\\n    }, {\\n      key: \\\"get\\\",\\n      value: function (t) {\\n        if (!e.enabled || !is$1.event(t)) return null;\\n        var n,\\n          r = t.target,\\n          i = t.changedTouches[0],\\n          o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n          s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n          u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n          c = r.getBoundingClientRect(),\\n          a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n        return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n      }\\n    }, {\\n      key: \\\"set\\\",\\n      value: function (t) {\\n        e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n      }\\n    }], [{\\n      key: \\\"setup\\\",\\n      value: function (t) {\\n        var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n          r = null;\\n        if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n        var i = _objectSpread2({}, defaults$1, {}, n);\\n        if (is$1.string(t) && i.watch) {\\n          var o = new MutationObserver(function (n) {\\n            Array.from(n).forEach(function (n) {\\n              Array.from(n.addedNodes).forEach(function (n) {\\n                is$1.element(n) && matches$1(n, t) && new e(n, i);\\n              });\\n            });\\n          });\\n          o.observe(document.body, {\\n            childList: !0,\\n            subtree: !0\\n          });\\n        }\\n        return r.map(function (t) {\\n          return new e(t, n);\\n        });\\n      }\\n    }, {\\n      key: \\\"enabled\\\",\\n      get: function () {\\n        return \\\"ontouchstart\\\" in document.documentElement;\\n      }\\n    }]), e;\\n  }();\\n\\n  // ==========================================================================\\n  // Type checking utils\\n  // ==========================================================================\\n\\n  const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\n  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\n  const isNullOrUndefined = input => input === null || typeof input === 'undefined';\\n  const isObject = input => getConstructor(input) === Object;\\n  const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\n  const isString = input => getConstructor(input) === String;\\n  const isBoolean = input => getConstructor(input) === Boolean;\\n  const isFunction = input => typeof input === 'function';\\n  const isArray = input => Array.isArray(input);\\n  const isWeakMap = input => instanceOf(input, WeakMap);\\n  const isNodeList = input => instanceOf(input, NodeList);\\n  const isTextNode = input => getConstructor(input) === Text;\\n  const isEvent = input => instanceOf(input, Event);\\n  const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\n  const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\n  const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\n  const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\n  const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\n  const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\n  const isUrl = input => {\\n    // Accept a URL object\\n    if (instanceOf(input, window.URL)) {\\n      return true;\\n    }\\n\\n    // Must be string from here\\n    if (!isString(input)) {\\n      return false;\\n    }\\n\\n    // Add the protocol if required\\n    let string = input;\\n    if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n      string = `http://${input}`;\\n    }\\n    try {\\n      return !isEmpty(new URL(string).hostname);\\n    } catch (_) {\\n      return false;\\n    }\\n  };\\n  var is = {\\n    nullOrUndefined: isNullOrUndefined,\\n    object: isObject,\\n    number: isNumber,\\n    string: isString,\\n    boolean: isBoolean,\\n    function: isFunction,\\n    array: isArray,\\n    weakMap: isWeakMap,\\n    nodeList: isNodeList,\\n    element: isElement,\\n    textNode: isTextNode,\\n    event: isEvent,\\n    keyboardEvent: isKeyboardEvent,\\n    cue: isCue,\\n    track: isTrack,\\n    promise: isPromise,\\n    url: isUrl,\\n    empty: isEmpty\\n  };\\n\\n  // ==========================================================================\\n  const transitionEndEvent = (() => {\\n    const element = document.createElement('span');\\n    const events = {\\n      WebkitTransition: 'webkitTransitionEnd',\\n      MozTransition: 'transitionend',\\n      OTransition: 'oTransitionEnd otransitionend',\\n      transition: 'transitionend'\\n    };\\n    const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n    return is.string(type) ? events[type] : false;\\n  })();\\n\\n  // Force repaint of element\\n  function repaint(element, delay) {\\n    setTimeout(() => {\\n      try {\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = true;\\n\\n        // eslint-disable-next-line no-unused-expressions\\n        element.offsetHeight;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = false;\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    }, delay);\\n  }\\n\\n  // ==========================================================================\\n  // Browser sniffing\\n  // Unfortunately, due to mixed support, UA sniffing is required\\n  // ==========================================================================\\n\\n  const isIE = Boolean(window.document.documentMode);\\n  const isEdge = /Edge/g.test(navigator.userAgent);\\n  const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\n  const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  // navigator.platform may be deprecated but this check is still required\\n  const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\n  const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  var browser = {\\n    isIE,\\n    isEdge,\\n    isWebKit,\\n    isIPhone,\\n    isIPadOS,\\n    isIos\\n  };\\n\\n  // ==========================================================================\\n\\n  // Clone nested objects\\n  function cloneDeep(object) {\\n    return JSON.parse(JSON.stringify(object));\\n  }\\n\\n  // Get a nested value in an object\\n  function getDeep(object, path) {\\n    return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n  }\\n\\n  // Deep extend destination object with N more objects\\n  function extend(target = {}, ...sources) {\\n    if (!sources.length) {\\n      return target;\\n    }\\n    const source = sources.shift();\\n    if (!is.object(source)) {\\n      return target;\\n    }\\n    Object.keys(source).forEach(key => {\\n      if (is.object(source[key])) {\\n        if (!Object.keys(target).includes(key)) {\\n          Object.assign(target, {\\n            [key]: {}\\n          });\\n        }\\n        extend(target[key], source[key]);\\n      } else {\\n        Object.assign(target, {\\n          [key]: source[key]\\n        });\\n      }\\n    });\\n    return extend(target, ...sources);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Wrap an element\\n  function wrap(elements, wrapper) {\\n    // Convert `elements` to an array, if necessary.\\n    const targets = elements.length ? elements : [elements];\\n\\n    // Loops backwards to prevent having to clone the wrapper on the\\n    // first element (see `child` below).\\n    Array.from(targets).reverse().forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n  }\\n\\n  // Set attributes\\n  function setAttributes(element, attributes) {\\n    if (!is.element(element) || is.empty(attributes)) return;\\n\\n    // Assume null and undefined attributes should be left out,\\n    // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n    Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n  }\\n\\n  // Create a DocumentFragment\\n  function createElement(type, attributes, text) {\\n    // Create a new <element>\\n    const element = document.createElement(type);\\n\\n    // Set all passed attributes\\n    if (is.object(attributes)) {\\n      setAttributes(element, attributes);\\n    }\\n\\n    // Add text node\\n    if (is.string(text)) {\\n      element.innerText = text;\\n    }\\n\\n    // Return built element\\n    return element;\\n  }\\n\\n  // Insert an element after another\\n  function insertAfter(element, target) {\\n    if (!is.element(element) || !is.element(target)) return;\\n    target.parentNode.insertBefore(element, target.nextSibling);\\n  }\\n\\n  // Insert a DocumentFragment\\n  function insertElement(type, parent, attributes, text) {\\n    if (!is.element(parent)) return;\\n    parent.appendChild(createElement(type, attributes, text));\\n  }\\n\\n  // Remove element(s)\\n  function removeElement(element) {\\n    if (is.nodeList(element) || is.array(element)) {\\n      Array.from(element).forEach(removeElement);\\n      return;\\n    }\\n    if (!is.element(element) || !is.element(element.parentNode)) {\\n      return;\\n    }\\n    element.parentNode.removeChild(element);\\n  }\\n\\n  // Remove all child elements\\n  function emptyElement(element) {\\n    if (!is.element(element)) return;\\n    let {\\n      length\\n    } = element.childNodes;\\n    while (length > 0) {\\n      element.removeChild(element.lastChild);\\n      length -= 1;\\n    }\\n  }\\n\\n  // Replace element\\n  function replaceElement(newChild, oldChild) {\\n    if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n    oldChild.parentNode.replaceChild(newChild, oldChild);\\n    return newChild;\\n  }\\n\\n  // Get an attribute object from a string selector\\n  function getAttributesFromSelector(sel, existingAttributes) {\\n    // For example:\\n    // '.test' to { class: 'test' }\\n    // '#test' to { id: 'test' }\\n    // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n    if (!is.string(sel) || is.empty(sel)) return {};\\n    const attributes = {};\\n    const existing = extend({}, existingAttributes);\\n    sel.split(',').forEach(s => {\\n      // Remove whitespace\\n      const selector = s.trim();\\n      const className = selector.replace('.', '');\\n      const stripped = selector.replace(/[[\\\\]]/g, '');\\n      // Get the parts and value\\n      const parts = stripped.split('=');\\n      const [key] = parts;\\n      const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n      // Get the first character\\n      const start = selector.charAt(0);\\n      switch (start) {\\n        case '.':\\n          // Add to existing classname\\n          if (is.string(existing.class)) {\\n            attributes.class = `${existing.class} ${className}`;\\n          } else {\\n            attributes.class = className;\\n          }\\n          break;\\n        case '#':\\n          // ID selector\\n          attributes.id = selector.replace('#', '');\\n          break;\\n        case '[':\\n          // Attribute selector\\n          attributes[key] = value;\\n          break;\\n      }\\n    });\\n    return extend(existing, attributes);\\n  }\\n\\n  // Toggle hidden\\n  function toggleHidden(element, hidden) {\\n    if (!is.element(element)) return;\\n    let hide = hidden;\\n    if (!is.boolean(hide)) {\\n      hide = !element.hidden;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    element.hidden = hide;\\n  }\\n\\n  // Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\n  function toggleClass(element, className, force) {\\n    if (is.nodeList(element)) {\\n      return Array.from(element).map(e => toggleClass(e, className, force));\\n    }\\n    if (is.element(element)) {\\n      let method = 'toggle';\\n      if (typeof force !== 'undefined') {\\n        method = force ? 'add' : 'remove';\\n      }\\n      element.classList[method](className);\\n      return element.classList.contains(className);\\n    }\\n    return false;\\n  }\\n\\n  // Has class name\\n  function hasClass(element, className) {\\n    return is.element(element) && element.classList.contains(className);\\n  }\\n\\n  // Element matches selector\\n  function matches(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n    function match() {\\n      return Array.from(document.querySelectorAll(selector)).includes(this);\\n    }\\n    const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n    return method.call(element, selector);\\n  }\\n\\n  // Closest ancestor element matching selector (also tests element itself)\\n  function closest$1(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n\\n    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n    function closestElement() {\\n      let el = this;\\n      do {\\n        if (matches.matches(el, selector)) return el;\\n        el = el.parentElement || el.parentNode;\\n      } while (el !== null && el.nodeType === 1);\\n      return null;\\n    }\\n    const method = prototype.closest || closestElement;\\n    return method.call(element, selector);\\n  }\\n\\n  // Find all elements\\n  function getElements(selector) {\\n    return this.elements.container.querySelectorAll(selector);\\n  }\\n\\n  // Find a single element\\n  function getElement(selector) {\\n    return this.elements.container.querySelector(selector);\\n  }\\n\\n  // Set focus and tab focus class\\n  function setFocus(element = null, focusVisible = false) {\\n    if (!is.element(element)) return;\\n\\n    // Set regular focus\\n    element.focus({\\n      preventScroll: true,\\n      focusVisible\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Default codecs for checking mimetype support\\n  const defaultCodecs = {\\n    'audio/ogg': 'vorbis',\\n    'audio/wav': '1',\\n    'video/webm': 'vp8, vorbis',\\n    'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n    'video/ogg': 'theora'\\n  };\\n\\n  // Check for feature support\\n  const support = {\\n    // Basic support\\n    audio: 'canPlayType' in document.createElement('audio'),\\n    video: 'canPlayType' in document.createElement('video'),\\n    // Check for support\\n    // Basic functionality vs full UI\\n    check(type, provider) {\\n      const api = support[type] || provider !== 'html5';\\n      const ui = api && support.rangeInput;\\n      return {\\n        api,\\n        ui\\n      };\\n    },\\n    // Picture-in-picture support\\n    // Safari & Chrome only currently\\n    pip: (() => {\\n      // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n      // It will throw the following error when trying to enter picture-in-picture\\n      // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n      if (browser.isIPhone) {\\n        return false;\\n      }\\n\\n      // Safari\\n      // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n      if (is.function(createElement('video').webkitSetPresentationMode)) {\\n        return true;\\n      }\\n\\n      // Chrome\\n      // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n      if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n        return true;\\n      }\\n      return false;\\n    })(),\\n    // Airplay support\\n    // Safari only currently\\n    airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n    // Inline playback support\\n    // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n    playsinline: 'playsInline' in document.createElement('video'),\\n    // Check for mime type support against a player instance\\n    // Credits: http://diveintohtml5.info/everything.html\\n    // Related: http://www.leanbackplayer.com/test/h5mt.html\\n    mime(input) {\\n      if (is.empty(input)) {\\n        return false;\\n      }\\n      const [mediaType] = input.split('/');\\n      let type = input;\\n\\n      // Verify we're using HTML5 and there's no media type mismatch\\n      if (!this.isHTML5 || mediaType !== this.type) {\\n        return false;\\n      }\\n\\n      // Add codec if required\\n      if (Object.keys(defaultCodecs).includes(type)) {\\n        type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n      }\\n      try {\\n        return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n      } catch (_) {\\n        return false;\\n      }\\n    },\\n    // Check for textTracks support\\n    textTracks: 'textTracks' in document.createElement('video'),\\n    // <input type=\\\"range\\\"> Sliders\\n    rangeInput: (() => {\\n      const range = document.createElement('input');\\n      range.type = 'range';\\n      return range.type === 'range';\\n    })(),\\n    // Touch\\n    // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n    touch: 'ontouchstart' in document.documentElement,\\n    // Detect transitions support\\n    transitions: transitionEndEvent !== false,\\n    // Reduced motion iOS & MacOS setting\\n    // https://webkit.org/blog/7551/responsive-design-for-motion/\\n    reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n  };\\n\\n  // ==========================================================================\\n\\n  // Check for passive event listener support\\n  // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n  // https://www.youtube.com/watch?v=NPM6172J22g\\n  const supportsPassiveListeners = (() => {\\n    // Test via a getter in the options object to see if the passive property is accessed\\n    let supported = false;\\n    try {\\n      const options = Object.defineProperty({}, 'passive', {\\n        get() {\\n          supported = true;\\n          return null;\\n        }\\n      });\\n      window.addEventListener('test', null, options);\\n      window.removeEventListener('test', null, options);\\n    } catch (_) {\\n      // Do nothing\\n    }\\n    return supported;\\n  })();\\n\\n  // Toggle event listener\\n  function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n    // Bail if no element, event, or callback\\n    if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n      return;\\n    }\\n\\n    // Allow multiple events\\n    const events = event.split(' ');\\n    // Build options\\n    // Default to just the capture boolean for browsers with no passive listener support\\n    let options = capture;\\n\\n    // If passive events listeners are supported\\n    if (supportsPassiveListeners) {\\n      options = {\\n        // Whether the listener can be passive (i.e. default never prevented)\\n        passive,\\n        // Whether the listener is a capturing listener or not\\n        capture\\n      };\\n    }\\n\\n    // If a single node is passed, bind the event listener\\n    events.forEach(type => {\\n      if (this && this.eventListeners && toggle) {\\n        // Cache event listener\\n        this.eventListeners.push({\\n          element,\\n          type,\\n          callback,\\n          options\\n        });\\n      }\\n      element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n    });\\n  }\\n\\n  // Bind event handler\\n  function on(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, true, passive, capture);\\n  }\\n\\n  // Unbind event handler\\n  function off(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, false, passive, capture);\\n  }\\n\\n  // Bind once-only event handler\\n  function once(element, events = '', callback, passive = true, capture = false) {\\n    const onceCallback = (...args) => {\\n      off(element, events, onceCallback, passive, capture);\\n      callback.apply(this, args);\\n    };\\n    toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n  }\\n\\n  // Trigger event\\n  function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n    // Bail if no element\\n    if (!is.element(element) || is.empty(type)) {\\n      return;\\n    }\\n\\n    // Create and dispatch the event\\n    const event = new CustomEvent(type, {\\n      bubbles,\\n      detail: {\\n        ...detail,\\n        plyr: this\\n      }\\n    });\\n\\n    // Dispatch the event\\n    element.dispatchEvent(event);\\n  }\\n\\n  // Unbind all cached event listeners\\n  function unbindListeners() {\\n    if (this && this.eventListeners) {\\n      this.eventListeners.forEach(item => {\\n        const {\\n          element,\\n          type,\\n          callback,\\n          options\\n        } = item;\\n        element.removeEventListener(type, callback, options);\\n      });\\n      this.eventListeners = [];\\n    }\\n  }\\n\\n  // Run method when / if player is ready\\n  function ready() {\\n    return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n  }\\n\\n  /**\\n   * Silence a Promise-like object.\\n   * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n   * play promise\\\" rejection error messages.\\n   * @param  {Object} value An object that may or may not be `Promise`-like.\\n   */\\n  function silencePromise(value) {\\n    if (is.promise(value)) {\\n      value.then(null, () => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Remove duplicates in an array\\n  function dedupe(array) {\\n    if (!is.array(array)) {\\n      return array;\\n    }\\n    return array.filter((item, index) => array.indexOf(item) === index);\\n  }\\n\\n  // Get the closest value in an array\\n  function closest(array, value) {\\n    if (!is.array(array) || !array.length) {\\n      return null;\\n    }\\n    return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Check support for a CSS declaration\\n  function supportsCSS(declaration) {\\n    if (!window || !window.CSS) {\\n      return false;\\n    }\\n    return window.CSS.supports(declaration);\\n  }\\n\\n  // Standard/common aspect ratios\\n  const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n    ...out,\\n    [x / y]: [x, y]\\n  }), {});\\n\\n  // Validate an aspect ratio\\n  function validateAspectRatio(input) {\\n    if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n      return false;\\n    }\\n    const ratio = is.array(input) ? input : input.split(':');\\n    return ratio.map(Number).every(is.number);\\n  }\\n\\n  // Reduce an aspect ratio to it's lowest form\\n  function reduceAspectRatio(ratio) {\\n    if (!is.array(ratio) || !ratio.every(is.number)) {\\n      return null;\\n    }\\n    const [width, height] = ratio;\\n    const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n    const divider = getDivider(width, height);\\n    return [width / divider, height / divider];\\n  }\\n\\n  // Calculate an aspect ratio\\n  function getAspectRatio(input) {\\n    const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n    // Try provided ratio\\n    let ratio = parse(input);\\n\\n    // Get from config\\n    if (ratio === null) {\\n      ratio = parse(this.config.ratio);\\n    }\\n\\n    // Get from embed\\n    if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n      ({\\n        ratio\\n      } = this.embed);\\n    }\\n\\n    // Get from HTML5 video\\n    if (ratio === null && this.isHTML5) {\\n      const {\\n        videoWidth,\\n        videoHeight\\n      } = this.media;\\n      ratio = [videoWidth, videoHeight];\\n    }\\n    return reduceAspectRatio(ratio);\\n  }\\n\\n  // Set aspect ratio for responsive container\\n  function setAspectRatio(input) {\\n    if (!this.isVideo) {\\n      return {};\\n    }\\n    const {\\n      wrapper\\n    } = this.elements;\\n    const ratio = getAspectRatio.call(this, input);\\n    if (!is.array(ratio)) {\\n      return {};\\n    }\\n    const [x, y] = reduceAspectRatio(ratio);\\n    const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n    const padding = 100 / x * y;\\n    if (useNative) {\\n      wrapper.style.aspectRatio = `${x}/${y}`;\\n    } else {\\n      wrapper.style.paddingBottom = `${padding}%`;\\n    }\\n\\n    // For Vimeo we have an extra <div> to hide the standard controls and UI\\n    if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n      const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n      const offset = (height - padding) / (height / 50);\\n      if (this.fullscreen.active) {\\n        wrapper.style.paddingBottom = null;\\n      } else {\\n        this.media.style.transform = `translateY(-${offset}%)`;\\n      }\\n    } else if (this.isHTML5) {\\n      wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n    }\\n    return {\\n      padding,\\n      ratio\\n    };\\n  }\\n\\n  // Round an aspect ratio to closest standard ratio\\n  function roundAspectRatio(x, y, tolerance = 0.05) {\\n    const ratio = x / y;\\n    const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n    // Check match is within tolerance\\n    if (Math.abs(closestRatio - ratio) <= tolerance) {\\n      return standardRatios[closestRatio];\\n    }\\n\\n    // No match\\n    return [x, y];\\n  }\\n\\n  // Get the size of the viewport\\n  // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\n  function getViewportSize() {\\n    const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n    const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n    return [width, height];\\n  }\\n\\n  // ==========================================================================\\n  const html5 = {\\n    getSources() {\\n      if (!this.isHTML5) {\\n        return [];\\n      }\\n      const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n      // Filter out unsupported sources (if type is specified)\\n      return sources.filter(source => {\\n        const type = source.getAttribute('type');\\n        if (is.empty(type)) {\\n          return true;\\n        }\\n        return support.mime.call(this, type);\\n      });\\n    },\\n    // Get quality levels\\n    getQualityOptions() {\\n      // Whether we're forcing all options (e.g. for streaming)\\n      if (this.config.quality.forced) {\\n        return this.config.quality.options;\\n      }\\n\\n      // Get sizes from <source> elements\\n      return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n    },\\n    setup() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n      const player = this;\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set aspect ratio if fixed\\n      if (!is.empty(this.config.ratio)) {\\n        setAspectRatio.call(player);\\n      }\\n\\n      // Quality\\n      Object.defineProperty(player.media, 'quality', {\\n        get() {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n          // Return size, if match is found\\n          return source && Number(source.getAttribute('size'));\\n        },\\n        set(input) {\\n          if (player.quality === input) {\\n            return;\\n          }\\n\\n          // If we're using an external handler...\\n          if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n            player.config.quality.onChange(input);\\n          } else {\\n            // Get sources\\n            const sources = html5.getSources.call(player);\\n            // Get first match for requested size\\n            const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n            // No matching source found\\n            if (!source) {\\n              return;\\n            }\\n\\n            // Get current state\\n            const {\\n              currentTime,\\n              paused,\\n              preload,\\n              readyState,\\n              playbackRate\\n            } = player.media;\\n\\n            // Set new source\\n            player.media.src = source.getAttribute('src');\\n\\n            // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n            if (preload !== 'none' || readyState) {\\n              // Restore time\\n              player.once('loadedmetadata', () => {\\n                player.speed = playbackRate;\\n                player.currentTime = currentTime;\\n\\n                // Resume playing\\n                if (!paused) {\\n                  silencePromise(player.play());\\n                }\\n              });\\n\\n              // Load new source\\n              player.media.load();\\n            }\\n          }\\n\\n          // Trigger change event\\n          triggerEvent.call(player, player.media, 'qualitychange', false, {\\n            quality: input\\n          });\\n        }\\n      });\\n    },\\n    // Cancel current network requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    cancelRequests() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n\\n      // Remove child sources\\n      removeElement(html5.getSources.call(this));\\n\\n      // Set blank video src attribute\\n      // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n      // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n      this.media.setAttribute('src', this.config.blankVideo);\\n\\n      // Load the new empty source\\n      // This will cancel existing requests\\n      // See https://github.com/sampotts/plyr/issues/174\\n      this.media.load();\\n\\n      // Debugging\\n      this.debug.log('Cancelled network requests');\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Generate a random ID\\n  function generateId(prefix) {\\n    return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n  }\\n\\n  // Format string\\n  function format(input, ...args) {\\n    if (is.empty(input)) return input;\\n    return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n  }\\n\\n  // Get percentage\\n  function getPercentage(current, max) {\\n    if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n      return 0;\\n    }\\n    return (current / max * 100).toFixed(2);\\n  }\\n\\n  // Replace all occurrences of a string in a string\\n  const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n  // Convert to title case\\n  const toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n  // Convert string to pascalCase\\n  function toPascalCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert kebab case\\n    string = replaceAll(string, '-', ' ');\\n\\n    // Convert snake case\\n    string = replaceAll(string, '_', ' ');\\n\\n    // Convert to title case\\n    string = toTitleCase(string);\\n\\n    // Convert to pascal case\\n    return replaceAll(string, ' ', '');\\n  }\\n\\n  // Convert string to pascalCase\\n  function toCamelCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert to pascal case\\n    string = toPascalCase(string);\\n\\n    // Convert first character to lowercase\\n    return string.charAt(0).toLowerCase() + string.slice(1);\\n  }\\n\\n  // Remove HTML from a string\\n  function stripHTML(source) {\\n    const fragment = document.createDocumentFragment();\\n    const element = document.createElement('div');\\n    fragment.appendChild(element);\\n    element.innerHTML = source;\\n    return fragment.firstChild.innerText;\\n  }\\n\\n  // Like outerHTML, but also works for DocumentFragment\\n  function getHTML(element) {\\n    const wrapper = document.createElement('div');\\n    wrapper.appendChild(element);\\n    return wrapper.innerHTML;\\n  }\\n\\n  // ==========================================================================\\n\\n  // Skip i18n for abbreviations and brand names\\n  const resources = {\\n    pip: 'PIP',\\n    airplay: 'AirPlay',\\n    html5: 'HTML5',\\n    vimeo: 'Vimeo',\\n    youtube: 'YouTube'\\n  };\\n  const i18n = {\\n    get(key = '', config = {}) {\\n      if (is.empty(key) || is.empty(config)) {\\n        return '';\\n      }\\n      let string = getDeep(config.i18n, key);\\n      if (is.empty(string)) {\\n        if (Object.keys(resources).includes(key)) {\\n          return resources[key];\\n        }\\n        return '';\\n      }\\n      const replace = {\\n        '{seektime}': config.seekTime,\\n        '{title}': config.title\\n      };\\n      Object.entries(replace).forEach(([k, v]) => {\\n        string = replaceAll(string, k, v);\\n      });\\n      return string;\\n    }\\n  };\\n\\n  class Storage {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"get\\\", key => {\\n        if (!Storage.supported || !this.enabled) {\\n          return null;\\n        }\\n        const store = window.localStorage.getItem(this.key);\\n        if (is.empty(store)) {\\n          return null;\\n        }\\n        const json = JSON.parse(store);\\n        return is.string(key) && key.length ? json[key] : json;\\n      });\\n      _defineProperty$1(this, \\\"set\\\", object => {\\n        // Bail if we don't have localStorage support or it's disabled\\n        if (!Storage.supported || !this.enabled) {\\n          return;\\n        }\\n\\n        // Can only store objectst\\n        if (!is.object(object)) {\\n          return;\\n        }\\n\\n        // Get current storage\\n        let storage = this.get();\\n\\n        // Default to empty object\\n        if (is.empty(storage)) {\\n          storage = {};\\n        }\\n\\n        // Update the working copy of the values\\n        extend(storage, object);\\n\\n        // Update storage\\n        try {\\n          window.localStorage.setItem(this.key, JSON.stringify(storage));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      });\\n      this.enabled = player.config.storage.enabled;\\n      this.key = player.config.storage.key;\\n    }\\n\\n    // Check for actual support (see if we can use it)\\n    static get supported() {\\n      try {\\n        if (!('localStorage' in window)) {\\n          return false;\\n        }\\n        const test = '___test';\\n\\n        // Try to use it (it might be disabled, e.g. user is in private mode)\\n        // see: https://github.com/sampotts/plyr/issues/131\\n        window.localStorage.setItem(test, test);\\n        window.localStorage.removeItem(test);\\n        return true;\\n      } catch (_) {\\n        return false;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Fetch wrapper\\n  // Using XHR to avoid issues with older browsers\\n  // ==========================================================================\\n\\n  function fetch(url, responseType = 'text') {\\n    return new Promise((resolve, reject) => {\\n      try {\\n        const request = new XMLHttpRequest();\\n\\n        // Check for CORS support\\n        if (!('withCredentials' in request)) {\\n          return;\\n        }\\n        request.addEventListener('load', () => {\\n          if (responseType === 'text') {\\n            try {\\n              resolve(JSON.parse(request.responseText));\\n            } catch (_) {\\n              resolve(request.responseText);\\n            }\\n          } else {\\n            resolve(request.response);\\n          }\\n        });\\n        request.addEventListener('error', () => {\\n          throw new Error(request.status);\\n        });\\n        request.open('GET', url, true);\\n\\n        // Set the required response type\\n        request.responseType = responseType;\\n        request.send();\\n      } catch (error) {\\n        reject(error);\\n      }\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Load an external SVG sprite\\n  function loadSprite(url, id) {\\n    if (!is.string(url)) {\\n      return;\\n    }\\n    const prefix = 'cache';\\n    const hasId = is.string(id);\\n    let isCached = false;\\n    const exists = () => document.getElementById(id) !== null;\\n    const update = (container, data) => {\\n      // eslint-disable-next-line no-param-reassign\\n      container.innerHTML = data;\\n\\n      // Check again incase of race condition\\n      if (hasId && exists()) {\\n        return;\\n      }\\n\\n      // Inject the SVG to the body\\n      document.body.insertAdjacentElement('afterbegin', container);\\n    };\\n\\n    // Only load once if ID set\\n    if (!hasId || !exists()) {\\n      const useStorage = Storage.supported;\\n      // Create container\\n      const container = document.createElement('div');\\n      container.setAttribute('hidden', '');\\n      if (hasId) {\\n        container.setAttribute('id', id);\\n      }\\n\\n      // Check in cache\\n      if (useStorage) {\\n        const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n        isCached = cached !== null;\\n        if (isCached) {\\n          const data = JSON.parse(cached);\\n          update(container, data.content);\\n        }\\n      }\\n\\n      // Get the sprite\\n      fetch(url).then(result => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n              content: result\\n            }));\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n        update(container, result);\\n      }).catch(() => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Time helpers\\n  const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\n  const getMinutes = value => Math.trunc(value / 60 % 60, 10);\\n  const getSeconds = value => Math.trunc(value % 60, 10);\\n\\n  // Format time to UI friendly string\\n  function formatTime(time = 0, displayHours = false, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return formatTime(undefined, displayHours, inverted);\\n    }\\n\\n    // Format time component to add leading zero\\n    const format = value => `0${value}`.slice(-2);\\n    // Breakdown to hours, mins, secs\\n    let hours = getHours(time);\\n    const mins = getMinutes(time);\\n    const secs = getSeconds(time);\\n\\n    // Do we need to display hours?\\n    if (displayHours || hours > 0) {\\n      hours = `${hours}:`;\\n    } else {\\n      hours = '';\\n    }\\n\\n    // Render\\n    return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n  }\\n\\n  // ==========================================================================\\n\\n  // TODO: Don't export a massive object - break down and create class\\n  const controls = {\\n    // Get icon URL\\n    getIconUrl() {\\n      const url = new URL(this.config.iconUrl, window.location);\\n      const host = window.location.host ? window.location.host : window.top.location.host;\\n      const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n      return {\\n        url: this.config.iconUrl,\\n        cors\\n      };\\n    },\\n    // Find the UI controls\\n    findElements() {\\n      try {\\n        this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n        // Buttons\\n        this.elements.buttons = {\\n          play: getElements.call(this, this.config.selectors.buttons.play),\\n          pause: getElement.call(this, this.config.selectors.buttons.pause),\\n          restart: getElement.call(this, this.config.selectors.buttons.restart),\\n          rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n          fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n          mute: getElement.call(this, this.config.selectors.buttons.mute),\\n          pip: getElement.call(this, this.config.selectors.buttons.pip),\\n          airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n          settings: getElement.call(this, this.config.selectors.buttons.settings),\\n          captions: getElement.call(this, this.config.selectors.buttons.captions),\\n          fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n        };\\n\\n        // Progress\\n        this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n        // Inputs\\n        this.elements.inputs = {\\n          seek: getElement.call(this, this.config.selectors.inputs.seek),\\n          volume: getElement.call(this, this.config.selectors.inputs.volume)\\n        };\\n\\n        // Display\\n        this.elements.display = {\\n          buffer: getElement.call(this, this.config.selectors.display.buffer),\\n          currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n          duration: getElement.call(this, this.config.selectors.display.duration)\\n        };\\n\\n        // Seek tooltip\\n        if (is.element(this.elements.progress)) {\\n          this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n        }\\n        return true;\\n      } catch (error) {\\n        // Log it\\n        this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n        // Restore native video controls\\n        this.toggleNativeControls(true);\\n        return false;\\n      }\\n    },\\n    // Create <svg> icon\\n    createIcon(type, attributes) {\\n      const namespace = 'http://www.w3.org/2000/svg';\\n      const iconUrl = controls.getIconUrl.call(this);\\n      const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n      // Create <svg>\\n      const icon = document.createElementNS(namespace, 'svg');\\n      setAttributes(icon, extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false'\\n      }));\\n\\n      // Create the <use> to reference sprite\\n      const use = document.createElementNS(namespace, 'use');\\n      const path = `${iconPath}-${type}`;\\n\\n      // Set `href` attributes\\n      // https://github.com/sampotts/plyr/issues/460\\n      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n      if ('href' in use) {\\n        use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n      }\\n\\n      // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n      // Add <use> to <svg>\\n      icon.appendChild(use);\\n      return icon;\\n    },\\n    // Create hidden text label\\n    createLabel(key, attr = {}) {\\n      const text = i18n.get(key, this.config);\\n      const attributes = {\\n        ...attr,\\n        class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n      };\\n      return createElement('span', attributes, text);\\n    },\\n    // Create a badge\\n    createBadge(text) {\\n      if (is.empty(text)) {\\n        return null;\\n      }\\n      const badge = createElement('span', {\\n        class: this.config.classNames.menu.value\\n      });\\n      badge.appendChild(createElement('span', {\\n        class: this.config.classNames.menu.badge\\n      }, text));\\n      return badge;\\n    },\\n    // Create a <button>\\n    createButton(buttonType, attr) {\\n      const attributes = extend({}, attr);\\n      let type = toCamelCase(buttonType);\\n      const props = {\\n        element: 'button',\\n        toggle: false,\\n        label: null,\\n        icon: null,\\n        labelPressed: null,\\n        iconPressed: null\\n      };\\n      ['element', 'icon', 'label'].forEach(key => {\\n        if (Object.keys(attributes).includes(key)) {\\n          props[key] = attributes[key];\\n          delete attributes[key];\\n        }\\n      });\\n\\n      // Default to 'button' type to prevent form submission\\n      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n        attributes.type = 'button';\\n      }\\n\\n      // Set class name\\n      if (Object.keys(attributes).includes('class')) {\\n        if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n          extend(attributes, {\\n            class: `${attributes.class} ${this.config.classNames.control}`\\n          });\\n        }\\n      } else {\\n        attributes.class = this.config.classNames.control;\\n      }\\n\\n      // Large play button\\n      switch (buttonType) {\\n        case 'play':\\n          props.toggle = true;\\n          props.label = 'play';\\n          props.labelPressed = 'pause';\\n          props.icon = 'play';\\n          props.iconPressed = 'pause';\\n          break;\\n        case 'mute':\\n          props.toggle = true;\\n          props.label = 'mute';\\n          props.labelPressed = 'unmute';\\n          props.icon = 'volume';\\n          props.iconPressed = 'muted';\\n          break;\\n        case 'captions':\\n          props.toggle = true;\\n          props.label = 'enableCaptions';\\n          props.labelPressed = 'disableCaptions';\\n          props.icon = 'captions-off';\\n          props.iconPressed = 'captions-on';\\n          break;\\n        case 'fullscreen':\\n          props.toggle = true;\\n          props.label = 'enterFullscreen';\\n          props.labelPressed = 'exitFullscreen';\\n          props.icon = 'enter-fullscreen';\\n          props.iconPressed = 'exit-fullscreen';\\n          break;\\n        case 'play-large':\\n          attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n          type = 'play';\\n          props.label = 'play';\\n          props.icon = 'play';\\n          break;\\n        default:\\n          if (is.empty(props.label)) {\\n            props.label = type;\\n          }\\n          if (is.empty(props.icon)) {\\n            props.icon = buttonType;\\n          }\\n      }\\n      const button = createElement(props.element);\\n\\n      // Setup toggle icon and labels\\n      if (props.toggle) {\\n        // Icon\\n        button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed'\\n        }));\\n        button.appendChild(controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed'\\n        }));\\n\\n        // Label/Tooltip\\n        button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed'\\n        }));\\n        button.appendChild(controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed'\\n        }));\\n      } else {\\n        button.appendChild(controls.createIcon.call(this, props.icon));\\n        button.appendChild(controls.createLabel.call(this, props.label));\\n      }\\n\\n      // Merge and set attributes\\n      extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n      setAttributes(button, attributes);\\n\\n      // We have multiple play buttons\\n      if (type === 'play') {\\n        if (!is.array(this.elements.buttons[type])) {\\n          this.elements.buttons[type] = [];\\n        }\\n        this.elements.buttons[type].push(button);\\n      } else {\\n        this.elements.buttons[type] = button;\\n      }\\n      return button;\\n    },\\n    // Create an <input type='range'>\\n    createRange(type, attributes) {\\n      // Seek input\\n      const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n        type: 'range',\\n        min: 0,\\n        max: 100,\\n        step: 0.01,\\n        value: 0,\\n        autocomplete: 'off',\\n        // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n        role: 'slider',\\n        'aria-label': i18n.get(type, this.config),\\n        'aria-valuemin': 0,\\n        'aria-valuemax': 100,\\n        'aria-valuenow': 0\\n      }, attributes));\\n      this.elements.inputs[type] = input;\\n\\n      // Set the fill for webkit now\\n      controls.updateRangeFill.call(this, input);\\n\\n      // Improve support on touch devices\\n      RangeTouch.setup(input);\\n      return input;\\n    },\\n    // Create a <progress>\\n    createProgress(type, attributes) {\\n      const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n        min: 0,\\n        max: 100,\\n        value: 0,\\n        role: 'progressbar',\\n        'aria-hidden': true\\n      }, attributes));\\n\\n      // Create the label inside\\n      if (type !== 'volume') {\\n        progress.appendChild(createElement('span', null, '0'));\\n        const suffixKey = {\\n          played: 'played',\\n          buffer: 'buffered'\\n        }[type];\\n        const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n        progress.innerText = `% ${suffix.toLowerCase()}`;\\n      }\\n      this.elements.display[type] = progress;\\n      return progress;\\n    },\\n    // Create time display\\n    createTime(type, attrs) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n      const container = createElement('div', extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer'\\n      }), '00:00');\\n\\n      // Reference for updates\\n      this.elements.display[type] = container;\\n      return container;\\n    },\\n    // Bind keyboard shortcuts for a menu item\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    bindMenuItemShortcuts(menuItem, type) {\\n      // Navigate through menus via arrow keys and space\\n      on.call(this, menuItem, 'keydown keyup', event => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n              target = menuItem.nextElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      }, false);\\n\\n      // Enter will fire a `click` event but we still need to manage focus\\n      // So we bind to keyup which fires after and set focus here\\n      on.call(this, menuItem, 'keyup', event => {\\n        if (event.key !== 'Return') return;\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      });\\n    },\\n    // Create a settings menu item\\n    createMenuItem({\\n      value,\\n      list,\\n      type,\\n      title,\\n      badge = null,\\n      checked = false\\n    }) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n      const menuItem = createElement('button', extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value\\n      }));\\n      const flex = createElement('span');\\n\\n      // We have to set as HTML incase of special characters\\n      flex.innerHTML = title;\\n      if (is.element(badge)) {\\n        flex.appendChild(badge);\\n      }\\n      menuItem.appendChild(flex);\\n\\n      // Replicate radio button behavior\\n      Object.defineProperty(menuItem, 'checked', {\\n        enumerable: true,\\n        get() {\\n          return menuItem.getAttribute('aria-checked') === 'true';\\n        },\\n        set(check) {\\n          // Ensure exclusivity\\n          if (check) {\\n            Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n          }\\n          menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n        }\\n      });\\n      this.listeners.bind(menuItem, 'click keyup', event => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n        event.preventDefault();\\n        event.stopPropagation();\\n        menuItem.checked = true;\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n        }\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      }, type, false);\\n      controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n      list.appendChild(menuItem);\\n    },\\n    // Format a time for display\\n    formatTime(time = 0, inverted = false) {\\n      // Bail if the value isn't a number\\n      if (!is.number(time)) {\\n        return time;\\n      }\\n\\n      // Always display hours if duration is over an hour\\n      const forceHours = getHours(this.duration) > 0;\\n      return formatTime(time, forceHours, inverted);\\n    },\\n    // Update the displayed time\\n    updateTimeDisplay(target = null, time = 0, inverted = false) {\\n      // Bail if there's no element to display or the value isn't a number\\n      if (!is.element(target) || !is.number(time)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line no-param-reassign\\n      target.innerText = controls.formatTime(time, inverted);\\n    },\\n    // Update volume UI and storage\\n    updateVolume() {\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Update range\\n      if (is.element(this.elements.inputs.volume)) {\\n        controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n      }\\n\\n      // Update mute state\\n      if (is.element(this.elements.buttons.mute)) {\\n        this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n      }\\n    },\\n    // Update seek value and lower fill\\n    setRange(target, value = 0) {\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line\\n      target.value = value;\\n\\n      // Webkit range fill\\n      controls.updateRangeFill.call(this, target);\\n    },\\n    // Update <progress> elements\\n    updateProgress(event) {\\n      if (!this.supported.ui || !is.event(event)) {\\n        return;\\n      }\\n      let value = 0;\\n      const setProgress = (target, input) => {\\n        const val = is.number(input) ? input : 0;\\n        const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n        // Update value and label\\n        if (is.element(progress)) {\\n          progress.value = val;\\n\\n          // Update text label inside\\n          const label = progress.getElementsByTagName('span')[0];\\n          if (is.element(label)) {\\n            label.childNodes[0].nodeValue = val;\\n          }\\n        }\\n      };\\n      if (event) {\\n        switch (event.type) {\\n          // Video playing\\n          case 'timeupdate':\\n          case 'seeking':\\n          case 'seeked':\\n            value = getPercentage(this.currentTime, this.duration);\\n\\n            // Set seek range value only if it's a 'natural' time event\\n            if (event.type === 'timeupdate') {\\n              controls.setRange.call(this, this.elements.inputs.seek, value);\\n            }\\n            break;\\n\\n          // Check buffer status\\n          case 'playing':\\n          case 'progress':\\n            setProgress(this.elements.display.buffer, this.buffered * 100);\\n            break;\\n        }\\n      }\\n    },\\n    // Webkit polyfill for lower fill range\\n    updateRangeFill(target) {\\n      // Get range from event if event passed\\n      const range = is.event(target) ? target.target : target;\\n\\n      // Needs to be a valid <input type='range'>\\n      if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n        return;\\n      }\\n\\n      // Set aria values for https://github.com/sampotts/plyr/issues/905\\n      if (matches(range, this.config.selectors.inputs.seek)) {\\n        range.setAttribute('aria-valuenow', this.currentTime);\\n        const currentTime = controls.formatTime(this.currentTime);\\n        const duration = controls.formatTime(this.duration);\\n        const format = i18n.get('seekLabel', this.config);\\n        range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n      } else if (matches(range, this.config.selectors.inputs.volume)) {\\n        const percent = range.value * 100;\\n        range.setAttribute('aria-valuenow', percent);\\n        range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n      } else {\\n        range.setAttribute('aria-valuenow', range.value);\\n      }\\n\\n      // WebKit only\\n      if (!browser.isWebKit && !browser.isIPadOS) {\\n        return;\\n      }\\n\\n      // Set CSS custom property\\n      range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n    },\\n    // Update hover tooltip for seeking\\n    updateSeekTooltip(event) {\\n      var _this$config$markers, _this$config$markers$;\\n      // Bail if setting not true\\n      if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n        return;\\n      }\\n      const tipElement = this.elements.display.seekTooltip;\\n      const visible = `${this.config.classNames.tooltip}--visible`;\\n      const toggle = show => toggleClass(tipElement, visible, show);\\n\\n      // Hide on touch\\n      if (this.touch) {\\n        toggle(false);\\n        return;\\n      }\\n\\n      // Determine percentage, if already visible\\n      let percent = 0;\\n      const clientRect = this.elements.progress.getBoundingClientRect();\\n      if (is.event(event)) {\\n        percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n      } else if (hasClass(tipElement, visible)) {\\n        percent = parseFloat(tipElement.style.left, 10);\\n      } else {\\n        return;\\n      }\\n\\n      // Set bounds\\n      if (percent < 0) {\\n        percent = 0;\\n      } else if (percent > 100) {\\n        percent = 100;\\n      }\\n      const time = this.duration / 100 * percent;\\n\\n      // Display the time a click would seek to\\n      tipElement.innerText = controls.formatTime(time);\\n\\n      // Get marker point for time\\n      const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n        time: t\\n      }) => t === Math.round(time));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n\\n      // Set position\\n      tipElement.style.left = `${percent}%`;\\n\\n      // Show/hide the tooltip\\n      // If the event is a moues in/out and percentage is inside bounds\\n      if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n        toggle(event.type === 'mouseenter');\\n      }\\n    },\\n    // Handle time change event\\n    timeUpdate(event) {\\n      // Only invert if only one time element is displayed and used for both duration and currentTime\\n      const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n      // Duration\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n      // Ignore updates while seeking\\n      if (event && event.type === 'timeupdate' && this.media.seeking) {\\n        return;\\n      }\\n\\n      // Playing progress\\n      controls.updateProgress.call(this, event);\\n    },\\n    // Show the duration on metadataloaded or durationchange events\\n    durationUpdate() {\\n      // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n      if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n        return;\\n      }\\n\\n      // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n      // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n      // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n      // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n      if (this.duration >= 2 ** 32) {\\n        toggleHidden(this.elements.display.currentTime, true);\\n        toggleHidden(this.elements.progress, true);\\n        return;\\n      }\\n\\n      // Update ARIA values\\n      if (is.element(this.elements.inputs.seek)) {\\n        this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n      }\\n\\n      // If there's a spot to display duration\\n      const hasDuration = is.element(this.elements.display.duration);\\n\\n      // If there's only one time display, display duration there\\n      if (!hasDuration && this.config.displayDuration && this.paused) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n      }\\n\\n      // If there's a duration element, update content\\n      if (hasDuration) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n      }\\n      if (this.config.markers.enabled) {\\n        controls.setMarkers.call(this);\\n      }\\n\\n      // Update the tooltip (if visible)\\n      controls.updateSeekTooltip.call(this);\\n    },\\n    // Hide/show a tab\\n    toggleMenuButton(setting, toggle) {\\n      toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n    },\\n    // Update the selected setting\\n    updateSetting(setting, container, input) {\\n      const pane = this.elements.settings.panels[setting];\\n      let value = null;\\n      let list = container;\\n      if (setting === 'captions') {\\n        value = this.currentTrack;\\n      } else {\\n        value = !is.empty(input) ? input : this[setting];\\n\\n        // Get default\\n        if (is.empty(value)) {\\n          value = this.config[setting].default;\\n        }\\n\\n        // Unsupported value\\n        if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n          this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n          return;\\n        }\\n\\n        // Disabled value\\n        if (!this.config[setting].options.includes(value)) {\\n          this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n          return;\\n        }\\n      }\\n\\n      // Get the list if we need to\\n      if (!is.element(list)) {\\n        list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n      }\\n\\n      // If there's no list it means it's not been rendered...\\n      if (!is.element(list)) {\\n        return;\\n      }\\n\\n      // Update the label\\n      const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n      label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n      // Find the radio option and check it\\n      const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n      if (is.element(target)) {\\n        target.checked = true;\\n      }\\n    },\\n    // Translate a value into a nice label\\n    getLabel(setting, value) {\\n      switch (setting) {\\n        case 'speed':\\n          return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n        case 'quality':\\n          if (is.number(value)) {\\n            const label = i18n.get(`qualityLabel.${value}`, this.config);\\n            if (!label.length) {\\n              return `${value}p`;\\n            }\\n            return label;\\n          }\\n          return toTitleCase(value);\\n        case 'captions':\\n          return captions.getLabel.call(this);\\n        default:\\n          return null;\\n      }\\n    },\\n    // Set the quality menu\\n    setQualityMenu(options) {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.quality)) {\\n        return;\\n      }\\n      const type = 'quality';\\n      const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Set options if passed and filter based on uniqueness and config\\n      if (is.array(options)) {\\n        this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n      }\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Get the badge HTML for HD, 4K etc\\n      const getBadge = quality => {\\n        const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n        if (!label.length) {\\n          return null;\\n        }\\n        return controls.createBadge.call(this, label);\\n      };\\n\\n      // Sort options by the config and then render options\\n      this.options.quality.sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      }).forEach(quality => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set the looping options\\n    /* setLoopMenu() {\\n          // Menu required\\n          if (!is.element(this.elements.settings.panels.loop)) {\\n              return;\\n          }\\n           const options = ['start', 'end', 'all', 'reset'];\\n          const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n           // Show the pane and tab\\n          toggleHidden(this.elements.settings.buttons.loop, false);\\n          toggleHidden(this.elements.settings.panels.loop, false);\\n           // Toggle the pane and tab\\n          const toggle = !is.empty(this.loop.options);\\n          controls.toggleMenuButton.call(this, 'loop', toggle);\\n           // Empty the menu\\n          emptyElement(list);\\n           options.forEach(option => {\\n              const item = createElement('li');\\n               const button = createElement(\\n                  'button',\\n                  extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                      type: 'button',\\n                      class: this.config.classNames.control,\\n                      'data-plyr-loop-action': option,\\n                  }),\\n                  i18n.get(option, this.config)\\n              );\\n               if (['start', 'end'].includes(option)) {\\n                  const badge = controls.createBadge.call(this, '00:00');\\n                  button.appendChild(badge);\\n              }\\n               item.appendChild(button);\\n              list.appendChild(item);\\n          });\\n      }, */\\n\\n    // Get current selected caption language\\n    // TODO: rework this to user the getter in the API?\\n\\n    // Set a list of available captions languages\\n    setCaptionsMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.captions)) {\\n        return;\\n      }\\n\\n      // TODO: Captions or language? Currently it's mixed\\n      const type = 'captions';\\n      const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n      const tracks = captions.getTracks.call(this);\\n      const toggle = Boolean(tracks.length);\\n\\n      // Toggle the pane and tab\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If there's no captions, bail\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Generate options data\\n      const options = tracks.map((track, value) => ({\\n        value,\\n        checked: this.captions.toggled && this.currentTrack === value,\\n        title: captions.getLabel.call(this, track),\\n        badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n        list,\\n        type: 'language'\\n      }));\\n\\n      // Add the \\\"Disabled\\\" option to turn off captions\\n      options.unshift({\\n        value: -1,\\n        checked: !this.captions.toggled,\\n        title: i18n.get('disabled', this.config),\\n        list,\\n        type: 'language'\\n      });\\n\\n      // Generate options\\n      options.forEach(controls.createMenuItem.bind(this));\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set a list of available captions languages\\n    setSpeedMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.speed)) {\\n        return;\\n      }\\n      const type = 'speed';\\n      const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Filter out invalid speeds\\n      this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Create items\\n      this.options.speed.forEach(speed => {\\n        controls.createMenuItem.call(this, {\\n          value: speed,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'speed', speed)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Check if we need to hide/show the settings menu\\n    checkMenu() {\\n      const {\\n        buttons\\n      } = this.elements.settings;\\n      const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n      toggleHidden(this.elements.settings.menu, !visible);\\n    },\\n    // Focus the first menu item in a given (or visible) menu\\n    focusFirstMenuItem(pane, focusVisible = false) {\\n      if (this.elements.settings.popup.hidden) {\\n        return;\\n      }\\n      let target = pane;\\n      if (!is.element(target)) {\\n        target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n      }\\n      const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n      setFocus.call(this, firstItem, focusVisible);\\n    },\\n    // Show/hide menu\\n    toggleMenu(input) {\\n      const {\\n        popup\\n      } = this.elements.settings;\\n      const button = this.elements.buttons.settings;\\n\\n      // Menu and button are required\\n      if (!is.element(popup) || !is.element(button)) {\\n        return;\\n      }\\n\\n      // True toggle by default\\n      const {\\n        hidden\\n      } = popup;\\n      let show = hidden;\\n      if (is.boolean(input)) {\\n        show = input;\\n      } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n        show = false;\\n      } else if (is.event(input)) {\\n        // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n        // Element in the shadowDOM. The path, if available, is complete.\\n        const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n        const isMenuItem = popup.contains(target);\\n\\n        // If the click was inside the menu or if the click\\n        // wasn't the button or menu item and we're trying to\\n        // show the menu (a doc click shouldn't show the menu)\\n        if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n          return;\\n        }\\n      }\\n\\n      // Set button attributes\\n      button.setAttribute('aria-expanded', show);\\n\\n      // Show the actual popup\\n      toggleHidden(popup, !show);\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n      // Focus the first item if key interaction\\n      if (show && is.keyboardEvent(input)) {\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      } else if (!show && !hidden) {\\n        // If closing, re-focus the button\\n        setFocus.call(this, button, is.keyboardEvent(input));\\n      }\\n    },\\n    // Get the natural size of a menu panel\\n    getMenuSize(tab) {\\n      const clone = tab.cloneNode(true);\\n      clone.style.position = 'absolute';\\n      clone.style.opacity = 0;\\n      clone.removeAttribute('hidden');\\n\\n      // Append to parent so we get the \\\"real\\\" size\\n      tab.parentNode.appendChild(clone);\\n\\n      // Get the sizes before we remove\\n      const width = clone.scrollWidth;\\n      const height = clone.scrollHeight;\\n\\n      // Remove from the DOM\\n      removeElement(clone);\\n      return {\\n        width,\\n        height\\n      };\\n    },\\n    // Show a panel in the menu\\n    showMenuPanel(type = '', focusVisible = false) {\\n      const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n      // Nothing to show, bail\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // Hide all other panels\\n      const container = target.parentNode;\\n      const current = Array.from(container.children).find(node => !node.hidden);\\n\\n      // If we can do fancy animations, we'll animate the height/width\\n      if (support.transitions && !support.reducedMotion) {\\n        // Set the current width as a base\\n        container.style.width = `${current.scrollWidth}px`;\\n        container.style.height = `${current.scrollHeight}px`;\\n\\n        // Get potential sizes\\n        const size = controls.getMenuSize.call(this, target);\\n\\n        // Restore auto height/width\\n        const restore = event => {\\n          // We're only bothered about height and width on the container\\n          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n            return;\\n          }\\n\\n          // Revert back to auto\\n          container.style.width = '';\\n          container.style.height = '';\\n\\n          // Only listen once\\n          off.call(this, container, transitionEndEvent, restore);\\n        };\\n\\n        // Listen for the transition finishing and restore auto height/width\\n        on.call(this, container, transitionEndEvent, restore);\\n\\n        // Set dimensions to target\\n        container.style.width = `${size.width}px`;\\n        container.style.height = `${size.height}px`;\\n      }\\n\\n      // Set attributes on current tab\\n      toggleHidden(current, true);\\n\\n      // Set attributes on target\\n      toggleHidden(target, false);\\n\\n      // Focus the first item\\n      controls.focusFirstMenuItem.call(this, target, focusVisible);\\n    },\\n    // Set the download URL\\n    setDownloadUrl() {\\n      const button = this.elements.buttons.download;\\n\\n      // Bail if no button\\n      if (!is.element(button)) {\\n        return;\\n      }\\n\\n      // Set attribute\\n      button.setAttribute('href', this.download);\\n    },\\n    // Build the default HTML\\n    create(data) {\\n      const {\\n        bindMenuItemShortcuts,\\n        createButton,\\n        createProgress,\\n        createRange,\\n        createTime,\\n        setQualityMenu,\\n        setSpeedMenu,\\n        showMenuPanel\\n      } = controls;\\n      this.elements.controls = null;\\n\\n      // Larger overlaid play button\\n      if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n        this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n      }\\n\\n      // Create the container\\n      const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n      this.elements.controls = container;\\n\\n      // Default item attributes\\n      const defaultAttributes = {\\n        class: 'plyr__controls__item'\\n      };\\n\\n      // Loop through controls in order\\n      dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n        // Restart button\\n        if (control === 'restart') {\\n          container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n        }\\n\\n        // Rewind button\\n        if (control === 'rewind') {\\n          container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n        }\\n\\n        // Play/Pause button\\n        if (control === 'play') {\\n          container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n        }\\n\\n        // Fast forward button\\n        if (control === 'fast-forward') {\\n          container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n        }\\n\\n        // Progress\\n        if (control === 'progress') {\\n          const progressContainer = createElement('div', {\\n            class: `${defaultAttributes.class} plyr__progress__container`\\n          });\\n          const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n          // Seek range slider\\n          progress.appendChild(createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`\\n          }));\\n\\n          // Buffer progress\\n          progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n          // TODO: Add loop display indicator\\n\\n          // Seek tooltip\\n          if (this.config.tooltips.seek) {\\n            const tooltip = createElement('span', {\\n              class: this.config.classNames.tooltip\\n            }, '00:00');\\n            progress.appendChild(tooltip);\\n            this.elements.display.seekTooltip = tooltip;\\n          }\\n          this.elements.progress = progress;\\n          progressContainer.appendChild(this.elements.progress);\\n          container.appendChild(progressContainer);\\n        }\\n\\n        // Media current time display\\n        if (control === 'current-time') {\\n          container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n        }\\n\\n        // Media duration display\\n        if (control === 'duration') {\\n          container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n        }\\n\\n        // Volume controls\\n        if (control === 'mute' || control === 'volume') {\\n          let {\\n            volume\\n          } = this.elements;\\n\\n          // Create the volume container if needed\\n          if (!is.element(volume) || !container.contains(volume)) {\\n            volume = createElement('div', extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim()\\n            }));\\n            this.elements.volume = volume;\\n            container.appendChild(volume);\\n          }\\n\\n          // Toggle mute button\\n          if (control === 'mute') {\\n            volume.appendChild(createButton.call(this, 'mute'));\\n          }\\n\\n          // Volume range control\\n          // Ignored on iOS as it's handled globally\\n          // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n          if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n            // Set the attributes\\n            const attributes = {\\n              max: 1,\\n              step: 0.05,\\n              value: this.config.volume\\n            };\\n\\n            // Create the volume range slider\\n            volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n              id: `plyr-volume-${data.id}`\\n            })));\\n          }\\n        }\\n\\n        // Toggle captions button\\n        if (control === 'captions') {\\n          container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n        }\\n\\n        // Settings button / menu\\n        if (control === 'settings' && !is.empty(this.config.settings)) {\\n          const wrapper = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: ''\\n          }));\\n          wrapper.appendChild(createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false\\n          }));\\n          const popup = createElement('div', {\\n            class: 'plyr__menu__container',\\n            id: `plyr-settings-${data.id}`,\\n            hidden: ''\\n          });\\n          const inner = createElement('div');\\n          const home = createElement('div', {\\n            id: `plyr-settings-${data.id}-home`\\n          });\\n\\n          // Create the menu\\n          const menu = createElement('div', {\\n            role: 'menu'\\n          });\\n          home.appendChild(menu);\\n          inner.appendChild(home);\\n          this.elements.settings.panels.home = home;\\n\\n          // Build the menu items\\n          this.config.settings.forEach(type => {\\n            // TODO: bundle this with the createMenuItem helper and bindings\\n            const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: ''\\n            }));\\n\\n            // Bind menu shortcuts for keyboard users\\n            bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n            // Show menu on click\\n            on.call(this, menuItem, 'click', () => {\\n              showMenuPanel.call(this, type, false);\\n            });\\n            const flex = createElement('span', null, i18n.get(type, this.config));\\n            const value = createElement('span', {\\n              class: this.config.classNames.menu.value\\n            });\\n\\n            // Speed contains HTML entities\\n            value.innerHTML = data[type];\\n            flex.appendChild(value);\\n            menuItem.appendChild(flex);\\n            menu.appendChild(menuItem);\\n\\n            // Build the panes\\n            const pane = createElement('div', {\\n              id: `plyr-settings-${data.id}-${type}`,\\n              hidden: ''\\n            });\\n\\n            // Back button\\n            const backButton = createElement('button', {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n            });\\n\\n            // Visible label\\n            backButton.appendChild(createElement('span', {\\n              'aria-hidden': true\\n            }, i18n.get(type, this.config)));\\n\\n            // Screen reader label\\n            backButton.appendChild(createElement('span', {\\n              class: this.config.classNames.hidden\\n            }, i18n.get('menuBack', this.config)));\\n\\n            // Go back via keyboard\\n            on.call(this, pane, 'keydown', event => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            }, false);\\n\\n            // Go back via button click\\n            on.call(this, backButton, 'click', () => {\\n              showMenuPanel.call(this, 'home', false);\\n            });\\n\\n            // Add to pane\\n            pane.appendChild(backButton);\\n\\n            // Menu\\n            pane.appendChild(createElement('div', {\\n              role: 'menu'\\n            }));\\n            inner.appendChild(pane);\\n            this.elements.settings.buttons[type] = menuItem;\\n            this.elements.settings.panels[type] = pane;\\n          });\\n          popup.appendChild(inner);\\n          wrapper.appendChild(popup);\\n          container.appendChild(wrapper);\\n          this.elements.settings.popup = popup;\\n          this.elements.settings.menu = wrapper;\\n        }\\n\\n        // Picture in picture button\\n        if (control === 'pip' && support.pip) {\\n          container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n        }\\n\\n        // Airplay button\\n        if (control === 'airplay' && support.airplay) {\\n          container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n        }\\n\\n        // Download button\\n        if (control === 'download') {\\n          const attributes = extend({}, defaultAttributes, {\\n            element: 'a',\\n            href: this.download,\\n            target: '_blank'\\n          });\\n\\n          // Set download attribute for HTML5 only\\n          if (this.isHTML5) {\\n            attributes.download = '';\\n          }\\n          const {\\n            download\\n          } = this.config.urls;\\n          if (!is.url(download) && this.isEmbed) {\\n            extend(attributes, {\\n              icon: `logo-${this.provider}`,\\n              label: this.provider\\n            });\\n          }\\n          container.appendChild(createButton.call(this, 'download', attributes));\\n        }\\n\\n        // Toggle fullscreen button\\n        if (control === 'fullscreen') {\\n          container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n        }\\n      });\\n\\n      // Set available quality levels\\n      if (this.isHTML5) {\\n        setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n      }\\n      setSpeedMenu.call(this);\\n      return container;\\n    },\\n    // Insert controls\\n    inject() {\\n      // Sprite\\n      if (this.config.loadSprite) {\\n        const icon = controls.getIconUrl.call(this);\\n\\n        // Only load external sprite using AJAX\\n        if (icon.cors) {\\n          loadSprite(icon.url, 'sprite-plyr');\\n        }\\n      }\\n\\n      // Create a unique ID\\n      this.id = Math.floor(Math.random() * 10000);\\n\\n      // Null by default\\n      let container = null;\\n      this.elements.controls = null;\\n\\n      // Set template properties\\n      const props = {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        title: this.config.title\\n      };\\n      let update = true;\\n\\n      // If function, run it and use output\\n      if (is.function(this.config.controls)) {\\n        this.config.controls = this.config.controls.call(this, props);\\n      }\\n\\n      // Convert falsy controls to empty array (primarily for empty strings)\\n      if (!this.config.controls) {\\n        this.config.controls = [];\\n      }\\n      if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n        // HTMLElement or Non-empty string passed as the option\\n        container = this.config.controls;\\n      } else {\\n        // Create controls\\n        container = controls.create.call(this, {\\n          id: this.id,\\n          seektime: this.config.seekTime,\\n          speed: this.speed,\\n          quality: this.quality,\\n          captions: captions.getLabel.call(this)\\n          // TODO: Looping\\n          // loop: 'None',\\n        });\\n\\n        update = false;\\n      }\\n\\n      // Replace props with their value\\n      const replace = input => {\\n        let result = input;\\n        Object.entries(props).forEach(([key, value]) => {\\n          result = replaceAll(result, `{${key}}`, value);\\n        });\\n        return result;\\n      };\\n\\n      // Update markup\\n      if (update) {\\n        if (is.string(this.config.controls)) {\\n          container = replace(container);\\n        }\\n      }\\n\\n      // Controls container\\n      let target;\\n\\n      // Inject to custom location\\n      if (is.string(this.config.selectors.controls.container)) {\\n        target = document.querySelector(this.config.selectors.controls.container);\\n      }\\n\\n      // Inject into the container by default\\n      if (!is.element(target)) {\\n        target = this.elements.container;\\n      }\\n\\n      // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n      const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n      target[insertMethod]('afterbegin', container);\\n\\n      // Find the elements if need be\\n      if (!is.element(this.elements.controls)) {\\n        controls.findElements.call(this);\\n      }\\n\\n      // Add pressed property to buttons\\n      if (!is.empty(this.elements.buttons)) {\\n        const addProperty = button => {\\n          const className = this.config.classNames.controlPressed;\\n          button.setAttribute('aria-pressed', 'false');\\n          Object.defineProperty(button, 'pressed', {\\n            configurable: true,\\n            enumerable: true,\\n            get() {\\n              return hasClass(button, className);\\n            },\\n            set(pressed = false) {\\n              toggleClass(button, className, pressed);\\n              button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n            }\\n          });\\n        };\\n\\n        // Toggle classname when pressed property is set\\n        Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n      }\\n\\n      // Edge sometimes doesn't finish the paint so force a repaint\\n      if (browser.isEdge) {\\n        repaint(target);\\n      }\\n\\n      // Setup tooltips\\n      if (this.config.tooltips.controls) {\\n        const {\\n          classNames,\\n          selectors\\n        } = this.config;\\n        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n        const labels = getElements.call(this, selector);\\n        Array.from(labels).forEach(label => {\\n          toggleClass(label, this.config.classNames.hidden, false);\\n          toggleClass(label, this.config.classNames.tooltip, true);\\n        });\\n      }\\n    },\\n    // Set media metadata\\n    setMediaMetadata() {\\n      try {\\n        if ('mediaSession' in navigator) {\\n          navigator.mediaSession.metadata = new window.MediaMetadata({\\n            title: this.config.mediaMetadata.title,\\n            artist: this.config.mediaMetadata.artist,\\n            album: this.config.mediaMetadata.album,\\n            artwork: this.config.mediaMetadata.artwork\\n          });\\n        }\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    },\\n    // Add markers\\n    setMarkers() {\\n      var _this$config$markers2, _this$config$markers3;\\n      if (!this.duration || this.elements.markers) return;\\n\\n      // Get valid points\\n      const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n        time\\n      }) => time > 0 && time < this.duration);\\n      if (!(points !== null && points !== void 0 && points.length)) return;\\n      const containerFragment = document.createDocumentFragment();\\n      const pointsFragment = document.createDocumentFragment();\\n      let tipElement = null;\\n      const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n      const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n      // Inject markers to progress container\\n      points.forEach(point => {\\n        const markerElement = createElement('span', {\\n          class: this.config.classNames.marker\\n        }, '');\\n        const left = `${point.time / this.duration * 100}%`;\\n        if (tipElement) {\\n          // Show on hover\\n          markerElement.addEventListener('mouseenter', () => {\\n            if (point.label) return;\\n            tipElement.style.left = left;\\n            tipElement.innerHTML = point.label;\\n            toggleTip(true);\\n          });\\n\\n          // Hide on leave\\n          markerElement.addEventListener('mouseleave', () => {\\n            toggleTip(false);\\n          });\\n        }\\n        markerElement.addEventListener('click', () => {\\n          this.currentTime = point.time;\\n        });\\n        markerElement.style.left = left;\\n        pointsFragment.appendChild(markerElement);\\n      });\\n      containerFragment.appendChild(pointsFragment);\\n\\n      // Inject a tooltip if needed\\n      if (!this.config.tooltips.seek) {\\n        tipElement = createElement('span', {\\n          class: this.config.classNames.tooltip\\n        }, '');\\n        containerFragment.appendChild(tipElement);\\n      }\\n      this.elements.markers = {\\n        points: pointsFragment,\\n        tip: tipElement\\n      };\\n      this.elements.progress.appendChild(containerFragment);\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  /**\\n   * Parse a string to a URL object\\n   * @param {String} input - the URL to be parsed\\n   * @param {Boolean} safe - failsafe parsing\\n   */\\n  function parseUrl(input, safe = true) {\\n    let url = input;\\n    if (safe) {\\n      const parser = document.createElement('a');\\n      parser.href = url;\\n      url = parser.href;\\n    }\\n    try {\\n      return new URL(url);\\n    } catch (_) {\\n      return null;\\n    }\\n  }\\n\\n  // Convert object to URLSearchParams\\n  function buildUrlParams(input) {\\n    const params = new URLSearchParams();\\n    if (is.object(input)) {\\n      Object.entries(input).forEach(([key, value]) => {\\n        params.set(key, value);\\n      });\\n    }\\n    return params;\\n  }\\n\\n  // ==========================================================================\\n  const captions = {\\n    // Setup captions\\n    setup() {\\n      // Requires UI support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Only Vimeo and HTML5 video supported at this point\\n      if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n        // Clear menu and hide\\n        if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n          controls.setCaptionsMenu.call(this);\\n        }\\n        return;\\n      }\\n\\n      // Inject the container\\n      if (!is.element(this.elements.captions)) {\\n        this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n        this.elements.captions.setAttribute('dir', 'auto');\\n        insertAfter(this.elements.captions, this.elements.wrapper);\\n      }\\n\\n      // Fix IE captions if CORS is used\\n      // Fetch captions and inject as blobs instead (data URIs not supported!)\\n      if (browser.isIE && window.URL) {\\n        const elements = this.media.querySelectorAll('track');\\n        Array.from(elements).forEach(track => {\\n          const src = track.getAttribute('src');\\n          const url = parseUrl(src);\\n          if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n            fetch(src, 'blob').then(blob => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            }).catch(() => {\\n              removeElement(track);\\n            });\\n          }\\n        });\\n      }\\n\\n      // Get and set initial data\\n      // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n      // * languages: Array of user's browser languages.\\n      // * language:  The language preferred by user settings or config\\n      // * active:    The state preferred by user settings or config\\n      // * toggled:   The real captions state\\n\\n      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n      const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n      let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n      // Use first browser language when language is 'auto'\\n      if (language === 'auto') {\\n        [language] = languages;\\n      }\\n      let active = this.storage.get('captions');\\n      if (!is.boolean(active)) {\\n        ({\\n          active\\n        } = this.config.captions);\\n      }\\n      Object.assign(this.captions, {\\n        toggled: false,\\n        active,\\n        language,\\n        languages\\n      });\\n\\n      // Watch changes to textTracks and update captions menu\\n      if (this.isHTML5) {\\n        const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n        on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n      }\\n\\n      // Update available languages in list next tick (the event must not be triggered before the listeners)\\n      setTimeout(captions.update.bind(this), 0);\\n    },\\n    // Update available language options in settings based on tracks\\n    update() {\\n      const tracks = captions.getTracks.call(this, true);\\n      // Get the wanted language\\n      const {\\n        active,\\n        language,\\n        meta,\\n        currentTrackNode\\n      } = this.captions;\\n      const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n      // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n      if (this.isHTML5 && this.isVideo) {\\n        tracks.filter(track => !meta.get(track)).forEach(track => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing'\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n      }\\n\\n      // Update language first time it matches, or if the previous matching track was removed\\n      if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n        captions.setLanguage.call(this, language);\\n        captions.toggle.call(this, active && languageExists);\\n      }\\n\\n      // Enable or disable captions based on track length\\n      if (this.elements) {\\n        toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n      }\\n\\n      // Update available languages in list\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n    },\\n    // Toggle captions display\\n    // Used internally for the toggleCaptions method, with the passive option forced to false\\n    toggle(input, passive = true) {\\n      // If there's no full support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      const {\\n        toggled\\n      } = this.captions; // Current state\\n      const activeClass = this.config.classNames.captions.active;\\n      // Get the next state\\n      // If the method is called without parameter, toggle based on current value\\n      const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n      // Update state and trigger event\\n      if (active !== toggled) {\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.active = active;\\n          this.storage.set({\\n            captions: active\\n          });\\n        }\\n\\n        // Force language if the call isn't passive and there is no matching language to toggle to\\n        if (!this.language && active && !passive) {\\n          const tracks = captions.getTracks.call(this);\\n          const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n          // Override user preferences to avoid switching languages if a matching track is added\\n          this.captions.language = track.language;\\n\\n          // Set caption, but don't store in localStorage as user preference\\n          captions.set.call(this, tracks.indexOf(track));\\n          return;\\n        }\\n\\n        // Toggle button if it's enabled\\n        if (this.elements.buttons.captions) {\\n          this.elements.buttons.captions.pressed = active;\\n        }\\n\\n        // Add class hook\\n        toggleClass(this.elements.container, activeClass, active);\\n        this.captions.toggled = active;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // Trigger event (not used internally)\\n        triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n      }\\n\\n      // Wait for the call stack to clear before setting mode='hidden'\\n      // on the active track - forcing the browser to download it\\n      setTimeout(() => {\\n        if (active && this.captions.toggled) {\\n          this.captions.currentTrackNode.mode = 'hidden';\\n        }\\n      });\\n    },\\n    // Set captions by track index\\n    // Used internally for the currentTrack setter with the passive option forced to false\\n    set(index, passive = true) {\\n      const tracks = captions.getTracks.call(this);\\n\\n      // Disable captions if setting to -1\\n      if (index === -1) {\\n        captions.toggle.call(this, false, passive);\\n        return;\\n      }\\n      if (!is.number(index)) {\\n        this.debug.warn('Invalid caption argument', index);\\n        return;\\n      }\\n      if (!(index in tracks)) {\\n        this.debug.warn('Track not found', index);\\n        return;\\n      }\\n      if (this.captions.currentTrack !== index) {\\n        this.captions.currentTrack = index;\\n        const track = tracks[index];\\n        const {\\n          language\\n        } = track || {};\\n\\n        // Store reference to node for invalidation on remove\\n        this.captions.currentTrackNode = track;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.language = language;\\n          this.storage.set({\\n            language\\n          });\\n        }\\n\\n        // Handle Vimeo captions\\n        if (this.isVimeo) {\\n          this.embed.enableTextTrack(language);\\n        }\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'languagechange');\\n      }\\n\\n      // Show captions\\n      captions.toggle.call(this, true, passive);\\n      if (this.isHTML5 && this.isVideo) {\\n        // If we change the active track while a cue is already displayed we need to update it\\n        captions.updateCues.call(this);\\n      }\\n    },\\n    // Set captions by language\\n    // Used internally for the language setter with the passive option forced to false\\n    setLanguage(input, passive = true) {\\n      if (!is.string(input)) {\\n        this.debug.warn('Invalid language argument', input);\\n        return;\\n      }\\n      // Normalize\\n      const language = input.toLowerCase();\\n      this.captions.language = language;\\n\\n      // Set currentTrack\\n      const tracks = captions.getTracks.call(this);\\n      const track = captions.findTrack.call(this, [language]);\\n      captions.set.call(this, tracks.indexOf(track), passive);\\n    },\\n    // Get current valid caption tracks\\n    // If update is false it will also ignore tracks without metadata\\n    // This is used to \\\"freeze\\\" the language options when captions.update is false\\n    getTracks(update = false) {\\n      // Handle media or textTracks missing or null\\n      const tracks = Array.from((this.media || {}).textTracks || []);\\n      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n      return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n    },\\n    // Match tracks based on languages and get the first\\n    findTrack(languages, force = false) {\\n      const tracks = captions.getTracks.call(this);\\n      const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n      let track;\\n      languages.every(language => {\\n        track = sorted.find(t => t.language === language);\\n        return !track; // Break iteration if there is a match\\n      });\\n\\n      // If no match is found but is required, get first\\n      return track || (force ? sorted[0] : undefined);\\n    },\\n    // Get the current track\\n    getCurrentTrack() {\\n      return captions.getTracks.call(this)[this.currentTrack];\\n    },\\n    // Get UI label for track\\n    getLabel(track) {\\n      let currentTrack = track;\\n      if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n        currentTrack = captions.getCurrentTrack.call(this);\\n      }\\n      if (is.track(currentTrack)) {\\n        if (!is.empty(currentTrack.label)) {\\n          return currentTrack.label;\\n        }\\n        if (!is.empty(currentTrack.language)) {\\n          return track.language.toUpperCase();\\n        }\\n        return i18n.get('enabled', this.config);\\n      }\\n      return i18n.get('disabled', this.config);\\n    },\\n    // Update captions using current track's active cues\\n    // Also optional array argument in case there isn't any track (ex: vimeo)\\n    updateCues(input) {\\n      // Requires UI\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      if (!is.element(this.elements.captions)) {\\n        this.debug.warn('No captions element to render to');\\n        return;\\n      }\\n\\n      // Only accept array or empty input\\n      if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n        this.debug.warn('updateCues: Invalid input', input);\\n        return;\\n      }\\n      let cues = input;\\n\\n      // Get cues from track\\n      if (!cues) {\\n        const track = captions.getCurrentTrack.call(this);\\n        cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n      }\\n\\n      // Set new caption text\\n      const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n      const changed = content !== this.elements.captions.innerHTML;\\n      if (changed) {\\n        // Empty the container and create a new child element\\n        emptyElement(this.elements.captions);\\n        const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n        caption.innerHTML = content;\\n        this.elements.captions.appendChild(caption);\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'cuechange');\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr default config\\n  // ==========================================================================\\n\\n  const defaults = {\\n    // Disable\\n    enabled: true,\\n    // Custom media title\\n    title: '',\\n    // Logging to console\\n    debug: false,\\n    // Auto play (if supported)\\n    autoplay: false,\\n    // Only allow one media playing at once (vimeo only)\\n    autopause: true,\\n    // Allow inline playback on iOS\\n    playsinline: true,\\n    // Default time to skip when rewind/fast forward\\n    seekTime: 10,\\n    // Default volume\\n    volume: 1,\\n    muted: false,\\n    // Pass a custom duration\\n    duration: null,\\n    // Display the media duration on load in the current time position\\n    // If you have opted to display both duration and currentTime, this is ignored\\n    displayDuration: true,\\n    // Invert the current time to be a countdown\\n    invertTime: true,\\n    // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n    toggleInvert: true,\\n    // Force an aspect ratio\\n    // The format must be `'w:h'` (e.g. `'16:9'`)\\n    ratio: null,\\n    // Click video container to play/pause\\n    clickToPlay: true,\\n    // Auto hide the controls\\n    hideControls: true,\\n    // Reset to start when playback ended\\n    resetOnEnd: false,\\n    // Disable the standard context menu\\n    disableContextMenu: true,\\n    // Sprite (for icons)\\n    loadSprite: true,\\n    iconPrefix: 'plyr',\\n    iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n    // Blank video (used to prevent errors on source change)\\n    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n    // Quality default\\n    quality: {\\n      default: 576,\\n      // The options to display in the UI, if available for the source media\\n      options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n      forced: false,\\n      onChange: null\\n    },\\n    // Set loops\\n    loop: {\\n      active: false\\n      // start: null,\\n      // end: null,\\n    },\\n\\n    // Speed default and options to display\\n    speed: {\\n      selected: 1,\\n      // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n      options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n    },\\n    // Keyboard shortcut settings\\n    keyboard: {\\n      focused: true,\\n      global: false\\n    },\\n    // Display tooltips\\n    tooltips: {\\n      controls: false,\\n      seek: true\\n    },\\n    // Captions settings\\n    captions: {\\n      active: false,\\n      language: 'auto',\\n      // Listen to new tracks added after Plyr is initialized.\\n      // This is needed for streaming captions, but may result in unselectable options\\n      update: false\\n    },\\n    // Fullscreen settings\\n    fullscreen: {\\n      enabled: true,\\n      // Allow fullscreen?\\n      fallback: true,\\n      // Fallback using full viewport/window\\n      iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n      // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n      // Non-ancestors of the player element will be ignored\\n      // container: null, // defaults to the player element\\n    },\\n\\n    // Local storage\\n    storage: {\\n      enabled: true,\\n      key: 'plyr'\\n    },\\n    // Default controls\\n    controls: ['play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress', 'current-time',\\n    // 'duration',\\n    'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n    // 'download',\\n    'fullscreen'],\\n    settings: ['captions', 'quality', 'speed'],\\n    // Localisation\\n    i18n: {\\n      restart: 'Restart',\\n      rewind: 'Rewind {seektime}s',\\n      play: 'Play',\\n      pause: 'Pause',\\n      fastForward: 'Forward {seektime}s',\\n      seek: 'Seek',\\n      seekLabel: '{currentTime} of {duration}',\\n      played: 'Played',\\n      buffered: 'Buffered',\\n      currentTime: 'Current time',\\n      duration: 'Duration',\\n      volume: 'Volume',\\n      mute: 'Mute',\\n      unmute: 'Unmute',\\n      enableCaptions: 'Enable captions',\\n      disableCaptions: 'Disable captions',\\n      download: 'Download',\\n      enterFullscreen: 'Enter fullscreen',\\n      exitFullscreen: 'Exit fullscreen',\\n      frameTitle: 'Player for {title}',\\n      captions: 'Captions',\\n      settings: 'Settings',\\n      pip: 'PIP',\\n      menuBack: 'Go back to previous menu',\\n      speed: 'Speed',\\n      normal: 'Normal',\\n      quality: 'Quality',\\n      loop: 'Loop',\\n      start: 'Start',\\n      end: 'End',\\n      all: 'All',\\n      reset: 'Reset',\\n      disabled: 'Disabled',\\n      enabled: 'Enabled',\\n      advertisement: 'Ad',\\n      qualityBadge: {\\n        2160: '4K',\\n        1440: 'HD',\\n        1080: 'HD',\\n        720: 'HD',\\n        576: 'SD',\\n        480: 'SD'\\n      }\\n    },\\n    // URLs\\n    urls: null,\\n    // Custom control listeners\\n    listeners: {\\n      seek: null,\\n      play: null,\\n      pause: null,\\n      restart: null,\\n      rewind: null,\\n      fastForward: null,\\n      mute: null,\\n      volume: null,\\n      captions: null,\\n      download: null,\\n      fullscreen: null,\\n      pip: null,\\n      airplay: null,\\n      speed: null,\\n      quality: null,\\n      loop: null,\\n      language: null\\n    },\\n    // Events to watch and bubble\\n    events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n    // Custom events\\n    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n    // YouTube\\n    'statechange',\\n    // Quality\\n    'qualitychange',\\n    // Ads\\n    'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n    // Selectors\\n    // Change these to match your template if using custom HTML\\n    selectors: {\\n      editable: 'input, textarea, select, [contenteditable]',\\n      container: '.plyr',\\n      controls: {\\n        container: null,\\n        wrapper: '.plyr__controls'\\n      },\\n      labels: '[data-plyr]',\\n      buttons: {\\n        play: '[data-plyr=\\\"play\\\"]',\\n        pause: '[data-plyr=\\\"pause\\\"]',\\n        restart: '[data-plyr=\\\"restart\\\"]',\\n        rewind: '[data-plyr=\\\"rewind\\\"]',\\n        fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n        mute: '[data-plyr=\\\"mute\\\"]',\\n        captions: '[data-plyr=\\\"captions\\\"]',\\n        download: '[data-plyr=\\\"download\\\"]',\\n        fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n        pip: '[data-plyr=\\\"pip\\\"]',\\n        airplay: '[data-plyr=\\\"airplay\\\"]',\\n        settings: '[data-plyr=\\\"settings\\\"]',\\n        loop: '[data-plyr=\\\"loop\\\"]'\\n      },\\n      inputs: {\\n        seek: '[data-plyr=\\\"seek\\\"]',\\n        volume: '[data-plyr=\\\"volume\\\"]',\\n        speed: '[data-plyr=\\\"speed\\\"]',\\n        language: '[data-plyr=\\\"language\\\"]',\\n        quality: '[data-plyr=\\\"quality\\\"]'\\n      },\\n      display: {\\n        currentTime: '.plyr__time--current',\\n        duration: '.plyr__time--duration',\\n        buffer: '.plyr__progress__buffer',\\n        loop: '.plyr__progress__loop',\\n        // Used later\\n        volume: '.plyr__volume--display'\\n      },\\n      progress: '.plyr__progress',\\n      captions: '.plyr__captions',\\n      caption: '.plyr__caption'\\n    },\\n    // Class hooks added to the player in different states\\n    classNames: {\\n      type: 'plyr--{0}',\\n      provider: 'plyr--{0}',\\n      video: 'plyr__video-wrapper',\\n      embed: 'plyr__video-embed',\\n      videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n      embedContainer: 'plyr__video-embed__container',\\n      poster: 'plyr__poster',\\n      posterEnabled: 'plyr__poster-enabled',\\n      ads: 'plyr__ads',\\n      control: 'plyr__control',\\n      controlPressed: 'plyr__control--pressed',\\n      playing: 'plyr--playing',\\n      paused: 'plyr--paused',\\n      stopped: 'plyr--stopped',\\n      loading: 'plyr--loading',\\n      hover: 'plyr--hover',\\n      tooltip: 'plyr__tooltip',\\n      cues: 'plyr__cues',\\n      marker: 'plyr__progress__marker',\\n      hidden: 'plyr__sr-only',\\n      hideControls: 'plyr--hide-controls',\\n      isTouch: 'plyr--is-touch',\\n      uiSupported: 'plyr--full-ui',\\n      noTransition: 'plyr--no-transition',\\n      display: {\\n        time: 'plyr__time'\\n      },\\n      menu: {\\n        value: 'plyr__menu__value',\\n        badge: 'plyr__badge',\\n        open: 'plyr--menu-open'\\n      },\\n      captions: {\\n        enabled: 'plyr--captions-enabled',\\n        active: 'plyr--captions-active'\\n      },\\n      fullscreen: {\\n        enabled: 'plyr--fullscreen-enabled',\\n        fallback: 'plyr--fullscreen-fallback'\\n      },\\n      pip: {\\n        supported: 'plyr--pip-supported',\\n        active: 'plyr--pip-active'\\n      },\\n      airplay: {\\n        supported: 'plyr--airplay-supported',\\n        active: 'plyr--airplay-active'\\n      },\\n      previewThumbnails: {\\n        // Tooltip thumbs\\n        thumbContainer: 'plyr__preview-thumb',\\n        thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n        imageContainer: 'plyr__preview-thumb__image-container',\\n        timeContainer: 'plyr__preview-thumb__time-container',\\n        // Scrubbing\\n        scrubbingContainer: 'plyr__preview-scrubbing',\\n        scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n      }\\n    },\\n    // Embed attributes\\n    attributes: {\\n      embed: {\\n        provider: 'data-plyr-provider',\\n        id: 'data-plyr-embed-id',\\n        hash: 'data-plyr-embed-hash'\\n      }\\n    },\\n    // Advertisements plugin\\n    // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n    ads: {\\n      enabled: false,\\n      publisherId: '',\\n      tagUrl: ''\\n    },\\n    // Preview Thumbnails plugin\\n    previewThumbnails: {\\n      enabled: false,\\n      src: ''\\n    },\\n    // Vimeo plugin\\n    vimeo: {\\n      byline: false,\\n      portrait: false,\\n      title: false,\\n      speed: true,\\n      transparent: false,\\n      // Custom settings from Plyr\\n      customControls: true,\\n      referrerPolicy: null,\\n      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n      // Whether the owner of the video has a Pro or Business account\\n      // (which allows us to properly hide controls without CSS hacks, etc)\\n      premium: false\\n    },\\n    // YouTube plugin\\n    youtube: {\\n      rel: 0,\\n      // No related vids\\n      showinfo: 0,\\n      // Hide info\\n      iv_load_policy: 3,\\n      // Hide annotations\\n      modestbranding: 1,\\n      // Hide logos as much as possible (they still show one in the corner when paused)\\n      // Custom settings from Plyr\\n      customControls: true,\\n      noCookie: false // Whether to use an alternative version of YouTube without cookies\\n    },\\n\\n    // Media Metadata\\n    mediaMetadata: {\\n      title: '',\\n      artist: '',\\n      album: '',\\n      artwork: []\\n    },\\n    // Markers\\n    markers: {\\n      enabled: false,\\n      points: []\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr states\\n  // ==========================================================================\\n\\n  const pip = {\\n    active: 'picture-in-picture',\\n    inactive: 'inline'\\n  };\\n\\n  // ==========================================================================\\n  // Plyr supported types and providers\\n  // ==========================================================================\\n\\n  const providers = {\\n    html5: 'html5',\\n    youtube: 'youtube',\\n    vimeo: 'vimeo'\\n  };\\n  const types = {\\n    audio: 'audio',\\n    video: 'video'\\n  };\\n\\n  /**\\n   * Get provider by URL\\n   * @param {String} url\\n   */\\n  function getProviderByUrl(url) {\\n    // YouTube\\n    if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n      return providers.youtube;\\n    }\\n\\n    // Vimeo\\n    if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n      return providers.vimeo;\\n    }\\n    return null;\\n  }\\n\\n  // ==========================================================================\\n  // Console wrapper\\n  // ==========================================================================\\n\\n  const noop = () => {};\\n  class Console {\\n    constructor(enabled = false) {\\n      this.enabled = window.console && enabled;\\n      if (this.enabled) {\\n        this.log('Debugging enabled');\\n      }\\n    }\\n    get log() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n    }\\n    get warn() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n    }\\n    get error() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n    }\\n  }\\n\\n  class Fullscreen {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"onChange\\\", () => {\\n        if (!this.supported) return;\\n\\n        // Update toggle button\\n        const button = this.player.elements.buttons.fullscreen;\\n        if (is.element(button)) {\\n          button.pressed = this.active;\\n        }\\n\\n        // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n        const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n        // Trigger an event\\n        triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n      });\\n      _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n        // Store or restore scroll position\\n        if (toggle) {\\n          this.scrollPosition = {\\n            x: window.scrollX ?? 0,\\n            y: window.scrollY ?? 0\\n          };\\n        } else {\\n          window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n        }\\n\\n        // Toggle scroll\\n        document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n        // Toggle class hook\\n        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n        // Force full viewport on iPhone X+\\n        if (browser.isIos) {\\n          let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n          const property = 'viewport-fit=cover';\\n\\n          // Inject the viewport meta if required\\n          if (!viewport) {\\n            viewport = document.createElement('meta');\\n            viewport.setAttribute('name', 'viewport');\\n          }\\n\\n          // Check if the property already exists\\n          const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n          if (toggle) {\\n            this.cleanupViewport = !hasProperty;\\n            if (!hasProperty) viewport.content += `,${property}`;\\n          } else if (this.cleanupViewport) {\\n            viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n          }\\n        }\\n\\n        // Toggle button and fire events\\n        this.onChange();\\n      });\\n      // Trap focus inside container\\n      _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n        // Bail if iOS/iPadOS, not active, not the tab key\\n        if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n        // Get the current focused element\\n        const focused = document.activeElement;\\n        const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n        const [first] = focusable;\\n        const last = focusable[focusable.length - 1];\\n        if (focused === last && !event.shiftKey) {\\n          // Move focus to first element that can be tabbed if Shift isn't used\\n          first.focus();\\n          event.preventDefault();\\n        } else if (focused === first && event.shiftKey) {\\n          // Move focus to last element that can be tabbed if Shift is used\\n          last.focus();\\n          event.preventDefault();\\n        }\\n      });\\n      // Update UI\\n      _defineProperty$1(this, \\\"update\\\", () => {\\n        if (this.supported) {\\n          let mode;\\n          if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n          this.player.debug.log(`${mode} fullscreen enabled`);\\n        } else {\\n          this.player.debug.log('Fullscreen not supported and fallback disabled');\\n        }\\n\\n        // Add styling hook to show button\\n        toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n      });\\n      // Make an element fullscreen\\n      _defineProperty$1(this, \\\"enter\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen doesn't need the request step\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.requestFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(true);\\n        } else if (!this.prefix) {\\n          this.target.requestFullscreen({\\n            navigationUI: 'hide'\\n          });\\n        } else if (!is.empty(this.prefix)) {\\n          this.target[`${this.prefix}Request${this.property}`]();\\n        }\\n      });\\n      // Bail from fullscreen\\n      _defineProperty$1(this, \\\"exit\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.exitFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n          silencePromise(this.player.play());\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(false);\\n        } else if (!this.prefix) {\\n          (document.cancelFullScreen || document.exitFullscreen).call(document);\\n        } else if (!is.empty(this.prefix)) {\\n          const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n          document[`${this.prefix}${action}${this.property}`]();\\n        }\\n      });\\n      // Toggle state\\n      _defineProperty$1(this, \\\"toggle\\\", () => {\\n        if (!this.active) this.enter();else this.exit();\\n      });\\n      // Keep reference to parent\\n      this.player = player;\\n\\n      // Get prefix\\n      this.prefix = Fullscreen.prefix;\\n      this.property = Fullscreen.property;\\n\\n      // Scroll position\\n      this.scrollPosition = {\\n        x: 0,\\n        y: 0\\n      };\\n\\n      // Force the use of 'full window/browser' rather than fullscreen\\n      this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n      // Get the fullscreen element\\n      // Checks container is an ancestor, defaults to null\\n      this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n      // Register event listeners\\n      // Handle event (incase user presses escape etc)\\n      on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      });\\n\\n      // Fullscreen toggle on double click\\n      on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n        // Ignore double click in controls\\n        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n          return;\\n        }\\n        this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n      });\\n\\n      // Tap focus when in fullscreen\\n      on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n      // Update the UI\\n      this.update();\\n    }\\n\\n    // Determine if native supported\\n    static get nativeSupported() {\\n      return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n    }\\n\\n    // If we're actually using native\\n    get useNative() {\\n      return Fullscreen.nativeSupported && !this.forceFallback;\\n    }\\n\\n    // Get the prefix for handlers\\n    static get prefix() {\\n      // No prefix\\n      if (is.function(document.exitFullscreen)) return '';\\n\\n      // Check for fullscreen support by vendor prefix\\n      let value = '';\\n      const prefixes = ['webkit', 'moz', 'ms'];\\n      prefixes.some(pre => {\\n        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n          value = pre;\\n          return true;\\n        }\\n        return false;\\n      });\\n      return value;\\n    }\\n    static get property() {\\n      return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n    }\\n\\n    // Determine if fullscreen is supported\\n    get supported() {\\n      return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n    }\\n\\n    // Get active state\\n    get active() {\\n      if (!this.supported) return false;\\n\\n      // Fallback using classname\\n      if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n      }\\n      const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n      return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n    }\\n\\n    // Get target element\\n    get target() {\\n      return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Load image avoiding xhr/fetch CORS issues\\n  // Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n  // By default it checks if it is at least 1px, but you can add a second argument to change this\\n  // ==========================================================================\\n\\n  function loadImage(src, minWidth = 1) {\\n    return new Promise((resolve, reject) => {\\n      const image = new Image();\\n      const handler = () => {\\n        delete image.onload;\\n        delete image.onerror;\\n        (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n      };\\n      Object.assign(image, {\\n        onload: handler,\\n        onerror: handler,\\n        src\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n  const ui = {\\n    addStyleHook() {\\n      toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n      toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n    },\\n    // Toggle native HTML5 media controls\\n    toggleNativeControls(toggle = false) {\\n      if (toggle && this.isHTML5) {\\n        this.media.setAttribute('controls', '');\\n      } else {\\n        this.media.removeAttribute('controls');\\n      }\\n    },\\n    // Setup the UI\\n    build() {\\n      // Re-attach media element listeners\\n      // TODO: Use event bubbling?\\n      this.listeners.media();\\n\\n      // Don't setup interface if no support\\n      if (!this.supported.ui) {\\n        this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n        // Restore native controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Bail\\n        return;\\n      }\\n\\n      // Inject custom controls if not present\\n      if (!is.element(this.elements.controls)) {\\n        // Inject custom controls\\n        controls.inject.call(this);\\n\\n        // Re-attach control listeners\\n        this.listeners.controls();\\n      }\\n\\n      // Remove native controls\\n      ui.toggleNativeControls.call(this);\\n\\n      // Setup captions for HTML5\\n      if (this.isHTML5) {\\n        captions.setup.call(this);\\n      }\\n\\n      // Reset volume\\n      this.volume = null;\\n\\n      // Reset mute state\\n      this.muted = null;\\n\\n      // Reset loop state\\n      this.loop = null;\\n\\n      // Reset quality setting\\n      this.quality = null;\\n\\n      // Reset speed\\n      this.speed = null;\\n\\n      // Reset volume display\\n      controls.updateVolume.call(this);\\n\\n      // Reset time display\\n      controls.timeUpdate.call(this);\\n\\n      // Reset duration display\\n      controls.durationUpdate.call(this);\\n\\n      // Update the UI\\n      ui.checkPlaying.call(this);\\n\\n      // Check for picture-in-picture support\\n      toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n      // Check for airplay support\\n      toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n      // Add touch class\\n      toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n      // Ready for API calls\\n      this.ready = true;\\n\\n      // Ready event at end of execution stack\\n      setTimeout(() => {\\n        triggerEvent.call(this, this.media, 'ready');\\n      }, 0);\\n\\n      // Set the title\\n      ui.setTitle.call(this);\\n\\n      // Assure the poster image is set, if the property was added before the element was created\\n      if (this.poster) {\\n        ui.setPoster.call(this, this.poster, false).catch(() => {});\\n      }\\n\\n      // Manually set the duration if user has overridden it.\\n      // The event listeners for it doesn't get called if preload is disabled (#701)\\n      if (this.config.duration) {\\n        controls.durationUpdate.call(this);\\n      }\\n\\n      // Media metadata\\n      if (this.config.mediaMetadata) {\\n        controls.setMediaMetadata.call(this);\\n      }\\n    },\\n    // Setup aria attribute for play and iframe title\\n    setTitle() {\\n      // Find the current text\\n      let label = i18n.get('play', this.config);\\n\\n      // If there's a media title set, use that for the label\\n      if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n        label += `, ${this.config.title}`;\\n      }\\n\\n      // If there's a play button, set label\\n      Array.from(this.elements.buttons.play || []).forEach(button => {\\n        button.setAttribute('aria-label', label);\\n      });\\n\\n      // Set iframe title\\n      // https://github.com/sampotts/plyr/issues/124\\n      if (this.isEmbed) {\\n        const iframe = getElement.call(this, 'iframe');\\n        if (!is.element(iframe)) {\\n          return;\\n        }\\n\\n        // Default to media type\\n        const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n        const format = i18n.get('frameTitle', this.config);\\n        iframe.setAttribute('title', format.replace('{title}', title));\\n      }\\n    },\\n    // Toggle poster\\n    togglePoster(enable) {\\n      toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n    },\\n    // Set the poster image (async)\\n    // Used internally for the poster setter, with the passive option forced to false\\n    setPoster(poster, passive = true) {\\n      // Don't override if call is passive\\n      if (passive && this.poster) {\\n        return Promise.reject(new Error('Poster already set'));\\n      }\\n\\n      // Set property synchronously to respect the call order\\n      this.media.setAttribute('data-poster', poster);\\n\\n      // Show the poster\\n      this.elements.poster.removeAttribute('hidden');\\n\\n      // Wait until ui is ready\\n      return ready.call(this)\\n      // Load image\\n      .then(() => loadImage(poster)).catch(error => {\\n        // Hide poster on error unless it's been set by another call\\n        if (poster === this.poster) {\\n          ui.togglePoster.call(this, false);\\n        }\\n        // Rethrow\\n        throw error;\\n      }).then(() => {\\n        // Prevent race conditions\\n        if (poster !== this.poster) {\\n          throw new Error('setPoster cancelled by later call to setPoster');\\n        }\\n      }).then(() => {\\n        Object.assign(this.elements.poster.style, {\\n          backgroundImage: `url('${poster}')`,\\n          // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n          backgroundSize: ''\\n        });\\n        ui.togglePoster.call(this, true);\\n        return poster;\\n      });\\n    },\\n    // Check playing state\\n    checkPlaying(event) {\\n      // Class hooks\\n      toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n      toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n      // Set state\\n      Array.from(this.elements.buttons.play || []).forEach(target => {\\n        Object.assign(target, {\\n          pressed: this.playing\\n        });\\n        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n      });\\n\\n      // Only update controls on non timeupdate events\\n      if (is.event(event) && event.type === 'timeupdate') {\\n        return;\\n      }\\n\\n      // Toggle controls\\n      ui.toggleControls.call(this);\\n    },\\n    // Check if media is loading\\n    checkLoading(event) {\\n      this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n      // Clear timer\\n      clearTimeout(this.timers.loading);\\n\\n      // Timer to prevent flicker when seeking\\n      this.timers.loading = setTimeout(() => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      }, this.loading ? 250 : 0);\\n    },\\n    // Toggle controls based on state and `force` argument\\n    toggleControls(force) {\\n      const {\\n        controls: controlsElement\\n      } = this.elements;\\n      if (controlsElement && this.config.hideControls) {\\n        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n        const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n        // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n        this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n      }\\n    },\\n    // Migrate any custom properties from the media to the parent\\n    migrateStyles() {\\n      // Loop through values (as they are the keys when the object is spread 🤔)\\n      Object.values({\\n        ...this.media.style\\n      })\\n      // We're only fussed about Plyr specific properties\\n      .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n      // Remove attribute if empty\\n      if (is.empty(this.media.style)) {\\n        this.media.removeAttribute('style');\\n      }\\n    }\\n  };\\n\\n  class Listeners {\\n    constructor(_player) {\\n      // Device is touch enabled\\n      _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        player.touch = true;\\n\\n        // Add touch class\\n        toggleClass(elements.container, player.config.classNames.isTouch, true);\\n      });\\n      // Global window & document listeners\\n      _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n        const {\\n          player\\n        } = this;\\n\\n        // Keyboard shortcuts\\n        if (player.config.keyboard.global) {\\n          toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n        }\\n\\n        // Click anywhere closes menu\\n        toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n        // Detect touch by events\\n        once.call(player, document.body, 'touchstart', this.firstTouch);\\n      });\\n      // Container listeners\\n      _defineProperty$1(this, \\\"container\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          config,\\n          elements,\\n          timers\\n        } = player;\\n\\n        // Keyboard shortcuts\\n        if (!config.keyboard.global && config.keyboard.focused) {\\n          on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n        }\\n\\n        // Toggle controls on mouse events and entering fullscreen\\n        on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n          const {\\n            controls: controlsElement\\n          } = elements;\\n\\n          // Remove button states for fullscreen\\n          if (controlsElement && event.type === 'enterfullscreen') {\\n            controlsElement.pressed = false;\\n            controlsElement.hover = false;\\n          }\\n\\n          // Show, then hide after a timeout unless another control event occurs\\n          const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n          let delay = 0;\\n          if (show) {\\n            ui.toggleControls.call(player, true);\\n            // Use longer timeout for touch devices\\n            delay = player.touch ? 3000 : 2000;\\n          }\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Set new timer to prevent flicker when seeking\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Set a gutter for Vimeo\\n        const setGutter = () => {\\n          if (!player.isVimeo || player.config.vimeo.premium) {\\n            return;\\n          }\\n          const target = elements.wrapper;\\n          const {\\n            active\\n          } = player.fullscreen;\\n          const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n          const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n          // If not active, remove styles\\n          if (!active) {\\n            if (useNativeAspectRatio) {\\n              target.style.width = null;\\n              target.style.height = null;\\n            } else {\\n              target.style.maxWidth = null;\\n              target.style.margin = null;\\n            }\\n            return;\\n          }\\n\\n          // Determine which dimension will overflow and constrain view\\n          const [viewportWidth, viewportHeight] = getViewportSize();\\n          const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n          if (useNativeAspectRatio) {\\n            target.style.width = overflow ? 'auto' : '100%';\\n            target.style.height = overflow ? '100%' : 'auto';\\n          } else {\\n            target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n            target.style.margin = overflow ? '0 auto' : null;\\n          }\\n        };\\n\\n        // Handle resizing\\n        const resized = () => {\\n          clearTimeout(timers.resized);\\n          timers.resized = setTimeout(setGutter, 50);\\n        };\\n        on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n          const {\\n            target\\n          } = player.fullscreen;\\n\\n          // Ignore events not from target\\n          if (target !== elements.container) {\\n            return;\\n          }\\n\\n          // If it's not an embed and no ratio specified\\n          if (!player.isEmbed && is.empty(player.config.ratio)) {\\n            return;\\n          }\\n\\n          // Set Vimeo gutter\\n          setGutter();\\n\\n          // Watch for resizes\\n          const method = event.type === 'enterfullscreen' ? on : off;\\n          method.call(player, window, 'resize', resized);\\n        });\\n      });\\n      // Listen for media events\\n      _defineProperty$1(this, \\\"media\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n\\n        // Time change on media\\n        on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n        // Display duration\\n        on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n        // Handle the media finishing\\n        on.call(player, player.media, 'ended', () => {\\n          // Show poster on end\\n          if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n            // Restart\\n            player.restart();\\n\\n            // Call pause otherwise IE11 will start playing the video again\\n            player.pause();\\n          }\\n        });\\n\\n        // Check for buffer progress\\n        on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n        // Handle volume changes\\n        on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n        // Handle play/pause\\n        on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n        // Loading state\\n        on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n        // Click video\\n        if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n          // Re-fetch the wrapper\\n          const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n          // Bail if there's no wrapper (this should never happen)\\n          if (!is.element(wrapper)) {\\n            return;\\n          }\\n\\n          // On click play, pause or restart\\n          on.call(player, elements.container, 'click', event => {\\n            const targets = [elements.container, wrapper];\\n\\n            // Ignore if click if not container or in video wrapper\\n            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n              return;\\n            }\\n\\n            // Touch devices will just show controls (if hidden)\\n            if (player.touch && player.config.hideControls) {\\n              return;\\n            }\\n            if (player.ended) {\\n              this.proxy(event, player.restart, 'restart');\\n              this.proxy(event, () => {\\n                silencePromise(player.play());\\n              }, 'play');\\n            } else {\\n              this.proxy(event, () => {\\n                silencePromise(player.togglePlay());\\n              }, 'play');\\n            }\\n          });\\n        }\\n\\n        // Disable right click\\n        if (player.supported.ui && player.config.disableContextMenu) {\\n          on.call(player, elements.wrapper, 'contextmenu', event => {\\n            event.preventDefault();\\n          }, false);\\n        }\\n\\n        // Volume change\\n        on.call(player, player.media, 'volumechange', () => {\\n          // Save to storage\\n          player.storage.set({\\n            volume: player.volume,\\n            muted: player.muted\\n          });\\n        });\\n\\n        // Speed change\\n        on.call(player, player.media, 'ratechange', () => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'speed');\\n\\n          // Save to storage\\n          player.storage.set({\\n            speed: player.speed\\n          });\\n        });\\n\\n        // Quality change\\n        on.call(player, player.media, 'qualitychange', event => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n        });\\n\\n        // Update download link when ready and if quality changes\\n        on.call(player, player.media, 'ready qualitychange', () => {\\n          controls.setDownloadUrl.call(player);\\n        });\\n\\n        // Proxy events to container\\n        // Bubble up key events for Edge\\n        const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n        on.call(player, player.media, proxyEvents, event => {\\n          let {\\n            detail = {}\\n          } = event;\\n\\n          // Get error details from media\\n          if (event.type === 'error') {\\n            detail = player.media.error;\\n          }\\n          triggerEvent.call(player, elements.container, event.type, true, detail);\\n        });\\n      });\\n      // Run default and custom handlers\\n      _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        let returned = true;\\n\\n        // Execute custom handler\\n        if (hasCustomHandler) {\\n          returned = customHandler.call(player, event);\\n        }\\n\\n        // Only call default handler if not prevented in custom handler\\n        if (returned !== false && is.function(defaultHandler)) {\\n          defaultHandler.call(player, event);\\n        }\\n      });\\n      // Trigger custom and default handlers\\n      _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n      });\\n      // Listen for control events\\n      _defineProperty$1(this, \\\"controls\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        // IE doesn't support input event, so we fallback to change\\n        const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n        // Play/pause toggle\\n        if (elements.buttons.play) {\\n          Array.from(elements.buttons.play).forEach(button => {\\n            this.bind(button, 'click', () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          });\\n        }\\n\\n        // Pause\\n        this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n        // Rewind\\n        this.bind(elements.buttons.rewind, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n          player.lastSeekTime = Date.now();\\n          player.rewind();\\n        }, 'rewind');\\n\\n        // Rewind\\n        this.bind(elements.buttons.fastForward, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n          player.lastSeekTime = Date.now();\\n          player.forward();\\n        }, 'fastForward');\\n\\n        // Mute toggle\\n        this.bind(elements.buttons.mute, 'click', () => {\\n          player.muted = !player.muted;\\n        }, 'mute');\\n\\n        // Captions toggle\\n        this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n        // Download\\n        this.bind(elements.buttons.download, 'click', () => {\\n          triggerEvent.call(player, player.media, 'download');\\n        }, 'download');\\n\\n        // Fullscreen toggle\\n        this.bind(elements.buttons.fullscreen, 'click', () => {\\n          player.fullscreen.toggle();\\n        }, 'fullscreen');\\n\\n        // Picture-in-Picture\\n        this.bind(elements.buttons.pip, 'click', () => {\\n          player.pip = 'toggle';\\n        }, 'pip');\\n\\n        // Airplay\\n        this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n        // Settings menu - click toggle\\n        this.bind(elements.buttons.settings, 'click', event => {\\n          // Prevent the document click listener closing the menu\\n          event.stopPropagation();\\n          event.preventDefault();\\n          controls.toggleMenu.call(player, event);\\n        }, null, false); // Can't be passive as we're preventing default\\n\\n        // Settings menu - keyboard toggle\\n        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n        this.bind(elements.buttons.settings, 'keyup', event => {\\n          if (![' ', 'Enter'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Because return triggers a click anyway, all we need to do is set focus\\n          if (event.key === 'Enter') {\\n            controls.focusFirstMenuItem.call(player, null, true);\\n            return;\\n          }\\n\\n          // Prevent scroll\\n          event.preventDefault();\\n\\n          // Prevent playing video (Firefox)\\n          event.stopPropagation();\\n\\n          // Toggle menu\\n          controls.toggleMenu.call(player, event);\\n        }, null, false // Can't be passive as we're preventing default\\n        );\\n\\n        // Escape closes menu\\n        this.bind(elements.settings.menu, 'keydown', event => {\\n          if (event.key === 'Escape') {\\n            controls.toggleMenu.call(player, event);\\n          }\\n        });\\n\\n        // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n        this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n          const rect = elements.progress.getBoundingClientRect();\\n          const percent = 100 / rect.width * (event.pageX - rect.left);\\n          event.currentTarget.setAttribute('seek-value', percent);\\n        });\\n\\n        // Pause while seeking\\n        this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n          const seek = event.currentTarget;\\n          const attribute = 'play-on-seeked';\\n          if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Record seek time so we can prevent hiding controls for a few seconds after seek\\n          player.lastSeekTime = Date.now();\\n\\n          // Was playing before?\\n          const play = seek.hasAttribute(attribute);\\n          // Done seeking\\n          const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n          // If we're done seeking and it was playing, resume playback\\n          if (play && done) {\\n            seek.removeAttribute(attribute);\\n            silencePromise(player.play());\\n          } else if (!done && player.playing) {\\n            seek.setAttribute(attribute, '');\\n            player.pause();\\n          }\\n        });\\n\\n        // Fix range inputs on iOS\\n        // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n        // it takes over further interactions on the page. This is a hack\\n        if (browser.isIos) {\\n          const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n          Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n        }\\n\\n        // Seek\\n        this.bind(elements.inputs.seek, inputEvent, event => {\\n          const seek = event.currentTarget;\\n          // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n          let seekTo = seek.getAttribute('seek-value');\\n          if (is.empty(seekTo)) {\\n            seekTo = seek.value;\\n          }\\n          seek.removeAttribute('seek-value');\\n          player.currentTime = seekTo / seek.max * player.duration;\\n        }, 'seek');\\n\\n        // Seek tooltip\\n        this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n        // Preview thumbnails plugin\\n        // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n        this.bind(elements.progress, 'mousemove touchmove', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startMove(event);\\n          }\\n        });\\n\\n        // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n        this.bind(elements.progress, 'mouseleave touchend click', () => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endMove(false, true);\\n          }\\n        });\\n\\n        // Show scrubbing preview\\n        this.bind(elements.progress, 'mousedown touchstart', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startScrubbing(event);\\n          }\\n        });\\n        this.bind(elements.progress, 'mouseup touchend', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endScrubbing(event);\\n          }\\n        });\\n\\n        // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n        if (browser.isWebKit) {\\n          Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n            this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n          });\\n        }\\n\\n        // Current time invert\\n        // Only if one time element is used for both currentTime and duration\\n        if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n          this.bind(elements.display.currentTime, 'click', () => {\\n            // Do nothing if we're at the start\\n            if (player.currentTime === 0) {\\n              return;\\n            }\\n            player.config.invertTime = !player.config.invertTime;\\n            controls.timeUpdate.call(player);\\n          });\\n        }\\n\\n        // Volume\\n        this.bind(elements.inputs.volume, inputEvent, event => {\\n          player.volume = event.target.value;\\n        }, 'volume');\\n\\n        // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n          elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n        });\\n\\n        // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n        if (elements.fullscreen) {\\n          Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n            this.bind(child, 'mouseenter mouseleave', event => {\\n              if (elements.controls) {\\n                elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n              }\\n            });\\n          });\\n        }\\n\\n        // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n          elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n        });\\n\\n        // Show controls when they receive focus (e.g., when using keyboard tab key)\\n        this.bind(elements.controls, 'focusin', () => {\\n          const {\\n            config,\\n            timers\\n          } = player;\\n\\n          // Skip transition to prevent focus from scrolling the parent element\\n          toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n          // Toggle\\n          ui.toggleControls.call(player, true);\\n\\n          // Restore transition\\n          setTimeout(() => {\\n            toggleClass(elements.controls, config.classNames.noTransition, false);\\n          }, 0);\\n\\n          // Delay a little more for mouse users\\n          const delay = this.touch ? 3000 : 4000;\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Hide again after delay\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Mouse wheel for volume\\n        this.bind(elements.inputs.volume, 'wheel', event => {\\n          // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n          // Other browsers on OS X will be inverted until support improves\\n          const inverted = event.webkitDirectionInvertedFromDevice;\\n          // Get delta from event. Invert if `inverted` is true\\n          const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n          // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n          const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n          // Change the volume by 2%\\n          player.increaseVolume(direction / 50);\\n\\n          // Don't break page scrolling at max and min\\n          const {\\n            volume\\n          } = player.media;\\n          if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n            event.preventDefault();\\n          }\\n        }, 'volume', false);\\n      });\\n      this.player = _player;\\n      this.lastKey = null;\\n      this.focusTimer = null;\\n      this.lastKeyDown = null;\\n      this.handleKey = this.handleKey.bind(this);\\n      this.toggleMenu = this.toggleMenu.bind(this);\\n      this.firstTouch = this.firstTouch.bind(this);\\n    }\\n\\n    // Handle key presses\\n    handleKey(event) {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      const {\\n        key,\\n        type,\\n        altKey,\\n        ctrlKey,\\n        metaKey,\\n        shiftKey\\n      } = event;\\n      const pressed = type === 'keydown';\\n      const repeat = pressed && key === this.lastKey;\\n\\n      // Bail if a modifier key is set\\n      if (altKey || ctrlKey || metaKey || shiftKey) {\\n        return;\\n      }\\n\\n      // If the event is bubbled from the media element\\n      // Firefox doesn't get the key for whatever reason\\n      if (!key) {\\n        return;\\n      }\\n\\n      // Seek by increment\\n      const seekByIncrement = increment => {\\n        // Divide the max duration into 10th's and times by the number value\\n        player.currentTime = player.duration / 10 * increment;\\n      };\\n\\n      // Handle the key on keydown\\n      // Reset on keyup\\n      if (pressed) {\\n        // Check focused element\\n        // and if the focused element is not editable (e.g. text input)\\n        // and any that accept key input http://webaim.org/techniques/keyboard/\\n        const focused = document.activeElement;\\n        if (is.element(focused)) {\\n          const {\\n            editable\\n          } = player.config.selectors;\\n          const {\\n            seek\\n          } = elements.inputs;\\n          if (focused !== seek && matches(focused, editable)) {\\n            return;\\n          }\\n          if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n            return;\\n          }\\n        }\\n\\n        // Which keys should we prevent default\\n        const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n        // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n        if (preventDefault.includes(key)) {\\n          event.preventDefault();\\n          event.stopPropagation();\\n        }\\n        switch (key) {\\n          case '0':\\n          case '1':\\n          case '2':\\n          case '3':\\n          case '4':\\n          case '5':\\n          case '6':\\n          case '7':\\n          case '8':\\n          case '9':\\n            if (!repeat) {\\n              seekByIncrement(parseInt(key, 10));\\n            }\\n            break;\\n          case ' ':\\n          case 'k':\\n            if (!repeat) {\\n              silencePromise(player.togglePlay());\\n            }\\n            break;\\n          case 'ArrowUp':\\n            player.increaseVolume(0.1);\\n            break;\\n          case 'ArrowDown':\\n            player.decreaseVolume(0.1);\\n            break;\\n          case 'm':\\n            if (!repeat) {\\n              player.muted = !player.muted;\\n            }\\n            break;\\n          case 'ArrowRight':\\n            player.forward();\\n            break;\\n          case 'ArrowLeft':\\n            player.rewind();\\n            break;\\n          case 'f':\\n            player.fullscreen.toggle();\\n            break;\\n          case 'c':\\n            if (!repeat) {\\n              player.toggleCaptions();\\n            }\\n            break;\\n          case 'l':\\n            player.loop = !player.loop;\\n            break;\\n        }\\n\\n        // Escape is handle natively when in full screen\\n        // So we only need to worry about non native\\n        if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n          player.fullscreen.toggle();\\n        }\\n\\n        // Store last key for next cycle\\n        this.lastKey = key;\\n      } else {\\n        this.lastKey = null;\\n      }\\n    }\\n\\n    // Toggle menu\\n    toggleMenu(event) {\\n      controls.toggleMenu.call(this.player, event);\\n    }\\n  }\\n\\n  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\n  function createCommonjsModule(fn, module) {\\n  \\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n  }\\n\\n  var loadjs_umd = createCommonjsModule(function (module, exports) {\\n    (function (root, factory) {\\n      {\\n        module.exports = factory();\\n      }\\n    })(commonjsGlobal, function () {\\n      /**\\n       * Global dependencies.\\n       * @global {Object} document - DOM\\n       */\\n\\n      var devnull = function () {},\\n        bundleIdCache = {},\\n        bundleResultCache = {},\\n        bundleCallbackQueue = {};\\n\\n      /**\\n       * Subscribe to bundle load event.\\n       * @param {string[]} bundleIds - Bundle ids\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function subscribe(bundleIds, callbackFn) {\\n        // listify\\n        bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n        var depsNotFound = [],\\n          i = bundleIds.length,\\n          numWaiting = i,\\n          fn,\\n          bundleId,\\n          r,\\n          q;\\n\\n        // define callback function\\n        fn = function (bundleId, pathsNotFound) {\\n          if (pathsNotFound.length) depsNotFound.push(bundleId);\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(depsNotFound);\\n        };\\n\\n        // register callback\\n        while (i--) {\\n          bundleId = bundleIds[i];\\n\\n          // execute callback if in result cache\\n          r = bundleResultCache[bundleId];\\n          if (r) {\\n            fn(bundleId, r);\\n            continue;\\n          }\\n\\n          // add to callback queue\\n          q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n          q.push(fn);\\n        }\\n      }\\n\\n      /**\\n       * Publish bundle load event.\\n       * @param {string} bundleId - Bundle id\\n       * @param {string[]} pathsNotFound - List of files not found\\n       */\\n      function publish(bundleId, pathsNotFound) {\\n        // exit if id isn't defined\\n        if (!bundleId) return;\\n        var q = bundleCallbackQueue[bundleId];\\n\\n        // cache result\\n        bundleResultCache[bundleId] = pathsNotFound;\\n\\n        // exit if queue is empty\\n        if (!q) return;\\n\\n        // empty callback queue\\n        while (q.length) {\\n          q[0](bundleId, pathsNotFound);\\n          q.splice(0, 1);\\n        }\\n      }\\n\\n      /**\\n       * Execute callbacks.\\n       * @param {Object or Function} args - The callback args\\n       * @param {string[]} depsNotFound - List of dependencies not found\\n       */\\n      function executeCallbacks(args, depsNotFound) {\\n        // accept function as argument\\n        if (args.call) args = {\\n          success: args\\n        };\\n\\n        // success and error callbacks\\n        if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n      }\\n\\n      /**\\n       * Load individual file.\\n       * @param {string} path - The file path\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFile(path, callbackFn, args, numTries) {\\n        var doc = document,\\n          async = args.async,\\n          maxTries = (args.numRetries || 0) + 1,\\n          beforeCallbackFn = args.before || devnull,\\n          pathname = path.replace(/[\\\\?|#].*$/, ''),\\n          pathStripped = path.replace(/^(css|img)!/, ''),\\n          isLegacyIECss,\\n          e;\\n        numTries = numTries || 0;\\n        if (/(^css!|\\\\.css$)/.test(pathname)) {\\n          // css\\n          e = doc.createElement('link');\\n          e.rel = 'stylesheet';\\n          e.href = pathStripped;\\n\\n          // tag IE9+\\n          isLegacyIECss = 'hideFocus' in e;\\n\\n          // use preload in IE Edge (to detect load errors)\\n          if (isLegacyIECss && e.relList) {\\n            isLegacyIECss = 0;\\n            e.rel = 'preload';\\n            e.as = 'style';\\n          }\\n        } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n          // image\\n          e = doc.createElement('img');\\n          e.src = pathStripped;\\n        } else {\\n          // javascript\\n          e = doc.createElement('script');\\n          e.src = path;\\n          e.async = async === undefined ? true : async;\\n        }\\n        e.onload = e.onerror = e.onbeforeload = function (ev) {\\n          var result = ev.type[0];\\n\\n          // treat empty stylesheets as failures to get around lack of onerror\\n          // support in IE9-11\\n          if (isLegacyIECss) {\\n            try {\\n              if (!e.sheet.cssText.length) result = 'e';\\n            } catch (x) {\\n              // sheets objects created from load errors don't allow access to\\n              // `cssText` (unless error is Code:18 SecurityError)\\n              if (x.code != 18) result = 'e';\\n            }\\n          }\\n\\n          // handle retries in case of load failure\\n          if (result == 'e') {\\n            // increment counter\\n            numTries += 1;\\n\\n            // exit function and try again\\n            if (numTries < maxTries) {\\n              return loadFile(path, callbackFn, args, numTries);\\n            }\\n          } else if (e.rel == 'preload' && e.as == 'style') {\\n            // activate preloaded stylesheets\\n            return e.rel = 'stylesheet'; // jshint ignore:line\\n          }\\n\\n          // execute callback\\n          callbackFn(path, result, ev.defaultPrevented);\\n        };\\n\\n        // add to document (unless callback returns `false`)\\n        if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n      }\\n\\n      /**\\n       * Load multiple files.\\n       * @param {string[]} paths - The file paths\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFiles(paths, callbackFn, args) {\\n        // listify paths\\n        paths = paths.push ? paths : [paths];\\n        var numWaiting = paths.length,\\n          x = numWaiting,\\n          pathsNotFound = [],\\n          fn,\\n          i;\\n\\n        // define callback function\\n        fn = function (path, result, defaultPrevented) {\\n          // handle error\\n          if (result == 'e') pathsNotFound.push(path);\\n\\n          // handle beforeload event. If defaultPrevented then that means the load\\n          // will be blocked (ex. Ghostery/ABP on Safari)\\n          if (result == 'b') {\\n            if (defaultPrevented) pathsNotFound.push(path);else return;\\n          }\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(pathsNotFound);\\n        };\\n\\n        // load scripts\\n        for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n      }\\n\\n      /**\\n       * Initiate script load and register bundle.\\n       * @param {(string|string[])} paths - The file paths\\n       * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n       *   callback or (3) object literal with success/error arguments, numRetries,\\n       *   etc.\\n       * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n       *   literal with success/error arguments, numRetries, etc.\\n       */\\n      function loadjs(paths, arg1, arg2) {\\n        var bundleId, args;\\n\\n        // bundleId (if string)\\n        if (arg1 && arg1.trim) bundleId = arg1;\\n\\n        // args (default is {})\\n        args = (bundleId ? arg2 : arg1) || {};\\n\\n        // throw error if bundle is already defined\\n        if (bundleId) {\\n          if (bundleId in bundleIdCache) {\\n            throw \\\"LoadJS\\\";\\n          } else {\\n            bundleIdCache[bundleId] = true;\\n          }\\n        }\\n        function loadFn(resolve, reject) {\\n          loadFiles(paths, function (pathsNotFound) {\\n            // execute callbacks\\n            executeCallbacks(args, pathsNotFound);\\n\\n            // resolve Promise\\n            if (resolve) {\\n              executeCallbacks({\\n                success: resolve,\\n                error: reject\\n              }, pathsNotFound);\\n            }\\n\\n            // publish bundle load event\\n            publish(bundleId, pathsNotFound);\\n          }, args);\\n        }\\n        if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n      }\\n\\n      /**\\n       * Execute callbacks when dependencies have been satisfied.\\n       * @param {(string|string[])} deps - List of bundle ids\\n       * @param {Object} args - success/error arguments\\n       */\\n      loadjs.ready = function ready(deps, args) {\\n        // subscribe to bundle load event\\n        subscribe(deps, function (depsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, depsNotFound);\\n        });\\n        return loadjs;\\n      };\\n\\n      /**\\n       * Manually satisfy bundle dependencies.\\n       * @param {string} bundleId - The bundle id\\n       */\\n      loadjs.done = function done(bundleId) {\\n        publish(bundleId, []);\\n      };\\n\\n      /**\\n       * Reset loadjs dependencies statuses\\n       */\\n      loadjs.reset = function reset() {\\n        bundleIdCache = {};\\n        bundleResultCache = {};\\n        bundleCallbackQueue = {};\\n      };\\n\\n      /**\\n       * Determine if bundle has already been defined\\n       * @param String} bundleId - The bundle id\\n       */\\n      loadjs.isDefined = function isDefined(bundleId) {\\n        return bundleId in bundleIdCache;\\n      };\\n\\n      // export\\n      return loadjs;\\n    });\\n  });\\n\\n  // ==========================================================================\\n  function loadScript(url) {\\n    return new Promise((resolve, reject) => {\\n      loadjs_umd(url, {\\n        success: resolve,\\n        error: reject\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Parse Vimeo ID from URL\\n  function parseId$1(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    if (is.number(Number(url))) {\\n      return url;\\n    }\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Try to extract a hash for private videos from the URL\\n  function parseHash(url) {\\n    /* This regex matches a hexadecimal hash if given in any of these forms:\\n     *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n     *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n     *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n     *  - video/{id}/{hash}\\n     * If matched, the hash is available in capture group 4\\n     */\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n    const found = url.match(regex);\\n    return found && found.length === 5 ? found[4] : null;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState$1(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  const vimeo = {\\n    setup() {\\n      const player = this;\\n\\n      // Add embed class for responsive\\n      toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set intial ratio\\n      setAspectRatio.call(player);\\n\\n      // Load the SDK if not already\\n      if (!is.object(window.Vimeo)) {\\n        loadScript(player.config.urls.vimeo.sdk).then(() => {\\n          vimeo.ready.call(player);\\n        }).catch(error => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n      } else {\\n        vimeo.ready.call(player);\\n      }\\n    },\\n    // API Ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.vimeo;\\n      const {\\n        premium,\\n        referrerPolicy,\\n        ...frameParams\\n      } = config;\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n      let hash = '';\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(player.config.attributes.embed.id);\\n        // hash can also be set as attribute on the <div>\\n        hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n      } else {\\n        hash = parseHash(source);\\n      }\\n      const hashParam = hash ? {\\n        h: hash\\n      } : {};\\n\\n      // If the owner has a pro or premium account then we can hide controls etc\\n      if (premium) {\\n        Object.assign(frameParams, {\\n          controls: false,\\n          sidedock: false\\n        });\\n      }\\n\\n      // Get Vimeo params for the iframe\\n      const params = buildUrlParams({\\n        loop: player.config.loop.active,\\n        autoplay: player.autoplay,\\n        muted: player.muted,\\n        gesture: 'media',\\n        playsinline: player.config.playsinline,\\n        // hash has to be added to iframe-URL\\n        ...hashParam,\\n        ...frameParams\\n      });\\n      const id = parseId$1(source);\\n      // Build an iframe\\n      const iframe = createElement('iframe');\\n      const src = format(player.config.urls.vimeo.iframe, id, params);\\n      iframe.setAttribute('src', src);\\n      iframe.setAttribute('allowfullscreen', '');\\n      iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n      // Set the referrer policy if required\\n      if (!is.empty(referrerPolicy)) {\\n        iframe.setAttribute('referrerPolicy', referrerPolicy);\\n      }\\n\\n      // Inject the package\\n      if (premium || !config.customControls) {\\n        iframe.setAttribute('data-poster', player.poster);\\n        player.media = replaceElement(iframe, player.media);\\n      } else {\\n        const wrapper = createElement('div', {\\n          class: player.config.classNames.embedContainer,\\n          'data-poster': player.poster\\n        });\\n        wrapper.appendChild(iframe);\\n        player.media = replaceElement(wrapper, player.media);\\n      }\\n\\n      // Get poster image\\n      if (!config.customControls) {\\n        fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n          if (is.empty(response) || !response.thumbnail_url) {\\n            return;\\n          }\\n\\n          // Set and show poster\\n          ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n        });\\n      }\\n\\n      // Setup instance\\n      // https://github.com/vimeo/player.js\\n      player.embed = new window.Vimeo.Player(iframe, {\\n        autopause: player.config.autopause,\\n        muted: player.muted\\n      });\\n      player.media.paused = true;\\n      player.media.currentTime = 0;\\n\\n      // Disable native text track rendering\\n      if (player.supported.ui) {\\n        player.embed.disableTextTrack();\\n      }\\n\\n      // Create a faux HTML5 API using the Vimeo API\\n      player.media.play = () => {\\n        assurePlaybackState$1.call(player, true);\\n        return player.embed.play();\\n      };\\n      player.media.pause = () => {\\n        assurePlaybackState$1.call(player, false);\\n        return player.embed.pause();\\n      };\\n      player.media.stop = () => {\\n        player.pause();\\n        player.currentTime = 0;\\n      };\\n\\n      // Seeking\\n      let {\\n        currentTime\\n      } = player.media;\\n      Object.defineProperty(player.media, 'currentTime', {\\n        get() {\\n          return currentTime;\\n        },\\n        set(time) {\\n          // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n          // Get current paused state and volume etc\\n          const {\\n            embed,\\n            media,\\n            paused,\\n            volume\\n          } = player;\\n          const restorePause = paused && !embed.hasPlayed;\\n\\n          // Set seeking state and trigger event\\n          media.seeking = true;\\n          triggerEvent.call(player, media, 'seeking');\\n\\n          // If paused, mute until seek is complete\\n          Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n            // Do nothing\\n          });\\n        }\\n      });\\n\\n      // Playback speed\\n      let speed = player.config.speed.selected;\\n      Object.defineProperty(player.media, 'playbackRate', {\\n        get() {\\n          return speed;\\n        },\\n        set(input) {\\n          player.embed.setPlaybackRate(input).then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          }).catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n        }\\n      });\\n\\n      // Volume\\n      let {\\n        volume\\n      } = player.config;\\n      Object.defineProperty(player.media, 'volume', {\\n        get() {\\n          return volume;\\n        },\\n        set(input) {\\n          player.embed.setVolume(input).then(() => {\\n            volume = input;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Muted\\n      let {\\n        muted\\n      } = player.config;\\n      Object.defineProperty(player.media, 'muted', {\\n        get() {\\n          return muted;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : false;\\n          player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n            muted = toggle;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Loop\\n      let {\\n        loop\\n      } = player.config;\\n      Object.defineProperty(player.media, 'loop', {\\n        get() {\\n          return loop;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : player.config.loop.active;\\n          player.embed.setLoop(toggle).then(() => {\\n            loop = toggle;\\n          });\\n        }\\n      });\\n\\n      // Source\\n      let currentSrc;\\n      player.embed.getVideoUrl().then(value => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      }).catch(error => {\\n        this.debug.warn(error);\\n      });\\n      Object.defineProperty(player.media, 'currentSrc', {\\n        get() {\\n          return currentSrc;\\n        }\\n      });\\n\\n      // Ended\\n      Object.defineProperty(player.media, 'ended', {\\n        get() {\\n          return player.currentTime === player.duration;\\n        }\\n      });\\n\\n      // Set aspect ratio based on video size\\n      Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n        const [width, height] = dimensions;\\n        player.embed.ratio = roundAspectRatio(width, height);\\n        setAspectRatio.call(this);\\n      });\\n\\n      // Set autopause\\n      player.embed.setAutopause(player.config.autopause).then(state => {\\n        player.config.autopause = state;\\n      });\\n\\n      // Get title\\n      player.embed.getVideoTitle().then(title => {\\n        player.config.title = title;\\n        ui.setTitle.call(this);\\n      });\\n\\n      // Get current time\\n      player.embed.getCurrentTime().then(value => {\\n        currentTime = value;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n\\n      // Get duration\\n      player.embed.getDuration().then(value => {\\n        player.media.duration = value;\\n        triggerEvent.call(player, player.media, 'durationchange');\\n      });\\n\\n      // Get captions\\n      player.embed.getTextTracks().then(tracks => {\\n        player.media.textTracks = tracks;\\n        captions.setup.call(player);\\n      });\\n      player.embed.on('cuechange', ({\\n        cues = []\\n      }) => {\\n        const strippedCues = cues.map(cue => stripHTML(cue.text));\\n        captions.updateCues.call(player, strippedCues);\\n      });\\n      player.embed.on('loaded', () => {\\n        // Assure state and events are updated on autoplay\\n        player.embed.getPaused().then(paused => {\\n          assurePlaybackState$1.call(player, !paused);\\n          if (!paused) {\\n            triggerEvent.call(player, player.media, 'playing');\\n          }\\n        });\\n        if (is.element(player.embed.element) && player.supported.ui) {\\n          const frame = player.embed.element;\\n\\n          // Fix keyboard focus issues\\n          // https://github.com/sampotts/plyr/issues/317\\n          frame.setAttribute('tabindex', -1);\\n        }\\n      });\\n      player.embed.on('bufferstart', () => {\\n        triggerEvent.call(player, player.media, 'waiting');\\n      });\\n      player.embed.on('bufferend', () => {\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('play', () => {\\n        assurePlaybackState$1.call(player, true);\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('pause', () => {\\n        assurePlaybackState$1.call(player, false);\\n      });\\n      player.embed.on('timeupdate', data => {\\n        player.media.seeking = false;\\n        currentTime = data.seconds;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n      player.embed.on('progress', data => {\\n        player.media.buffered = data.percent;\\n        triggerEvent.call(player, player.media, 'progress');\\n\\n        // Check all loaded\\n        if (parseInt(data.percent, 10) === 1) {\\n          triggerEvent.call(player, player.media, 'canplaythrough');\\n        }\\n\\n        // Get duration as if we do it before load, it gives an incorrect value\\n        // https://github.com/sampotts/plyr/issues/891\\n        player.embed.getDuration().then(value => {\\n          if (value !== player.media.duration) {\\n            player.media.duration = value;\\n            triggerEvent.call(player, player.media, 'durationchange');\\n          }\\n        });\\n      });\\n      player.embed.on('seeked', () => {\\n        player.media.seeking = false;\\n        triggerEvent.call(player, player.media, 'seeked');\\n      });\\n      player.embed.on('ended', () => {\\n        player.media.paused = true;\\n        triggerEvent.call(player, player.media, 'ended');\\n      });\\n      player.embed.on('error', detail => {\\n        player.media.error = detail;\\n        triggerEvent.call(player, player.media, 'error');\\n      });\\n\\n      // Rebuild UI\\n      if (config.customControls) {\\n        setTimeout(() => ui.build.call(player), 0);\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Parse YouTube ID from URL\\n  function parseId(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  function getHost(config) {\\n    if (config.noCookie) {\\n      return 'https://www.youtube-nocookie.com';\\n    }\\n    if (window.location.protocol === 'http:') {\\n      return 'http://www.youtube.com';\\n    }\\n\\n    // Use YouTube's default\\n    return undefined;\\n  }\\n  const youtube = {\\n    setup() {\\n      // Add embed class for responsive\\n      toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n      // Setup API\\n      if (is.object(window.YT) && is.function(window.YT.Player)) {\\n        youtube.ready.call(this);\\n      } else {\\n        // Reference current global callback\\n        const callback = window.onYouTubeIframeAPIReady;\\n\\n        // Set callback to process queue\\n        window.onYouTubeIframeAPIReady = () => {\\n          // Call global callback if set\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n          youtube.ready.call(this);\\n        };\\n\\n        // Load the SDK\\n        loadScript(this.config.urls.youtube.sdk).catch(error => {\\n          this.debug.warn('YouTube API failed to load', error);\\n        });\\n      }\\n    },\\n    // Get the media title\\n    getTitle(videoId) {\\n      const url = format(this.config.urls.youtube.api, videoId);\\n      fetch(url).then(data => {\\n        if (is.object(data)) {\\n          const {\\n            title,\\n            height,\\n            width\\n          } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n        setAspectRatio.call(this);\\n      }).catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n    },\\n    // API ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.youtube;\\n      // Ignore already setup (race condition)\\n      const currentId = player.media && player.media.getAttribute('id');\\n      if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n        return;\\n      }\\n\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(this.config.attributes.embed.id);\\n      }\\n\\n      // Replace the <iframe> with a <div> due to YouTube API issues\\n      const videoId = parseId(source);\\n      const id = generateId(player.provider);\\n      // Replace media element\\n      const container = createElement('div', {\\n        id,\\n        'data-poster': config.customControls ? player.poster : undefined\\n      });\\n      player.media = replaceElement(container, player.media);\\n\\n      // Only load the poster when using custom controls\\n      if (config.customControls) {\\n        const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n        // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n        loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        }).catch(() => {});\\n      }\\n\\n      // Setup instance\\n      // https://developers.google.com/youtube/iframe_api_reference\\n      player.embed = new window.YT.Player(player.media, {\\n        videoId,\\n        host: getHost(config),\\n        playerVars: extend({}, {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null\\n        }, config),\\n        events: {\\n          onError(event) {\\n            // YouTube may fire onError twice, so only handle it once\\n            if (!player.media.error) {\\n              const code = event.data;\\n              // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n              const message = {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n              }[code] || 'An unknown error occurred';\\n              player.media.error = {\\n                code,\\n                message\\n              };\\n              triggerEvent.call(player, player.media, 'error');\\n            }\\n          },\\n          onPlaybackRateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get current speed\\n            player.media.playbackRate = instance.getPlaybackRate();\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          },\\n          onReady(event) {\\n            // Bail if onReady has already been called. See issue #1108\\n            if (is.function(player.media.play)) {\\n              return;\\n            }\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get the title\\n            youtube.getTitle.call(player, videoId);\\n\\n            // Create a faux HTML5 API using the YouTube API\\n            player.media.play = () => {\\n              assurePlaybackState.call(player, true);\\n              instance.playVideo();\\n            };\\n            player.media.pause = () => {\\n              assurePlaybackState.call(player, false);\\n              instance.pauseVideo();\\n            };\\n            player.media.stop = () => {\\n              instance.stopVideo();\\n            };\\n            player.media.duration = instance.getDuration();\\n            player.media.paused = true;\\n\\n            // Seeking\\n            player.media.currentTime = 0;\\n            Object.defineProperty(player.media, 'currentTime', {\\n              get() {\\n                return Number(instance.getCurrentTime());\\n              },\\n              set(time) {\\n                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n                if (player.paused && !player.embed.hasPlayed) {\\n                  player.embed.mute();\\n                }\\n\\n                // Set seeking state and trigger event\\n                player.media.seeking = true;\\n                triggerEvent.call(player, player.media, 'seeking');\\n\\n                // Seek after events sent\\n                instance.seekTo(time);\\n              }\\n            });\\n\\n            // Playback speed\\n            Object.defineProperty(player.media, 'playbackRate', {\\n              get() {\\n                return instance.getPlaybackRate();\\n              },\\n              set(input) {\\n                instance.setPlaybackRate(input);\\n              }\\n            });\\n\\n            // Volume\\n            let {\\n              volume\\n            } = player.config;\\n            Object.defineProperty(player.media, 'volume', {\\n              get() {\\n                return volume;\\n              },\\n              set(input) {\\n                volume = input;\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Muted\\n            let {\\n              muted\\n            } = player.config;\\n            Object.defineProperty(player.media, 'muted', {\\n              get() {\\n                return muted;\\n              },\\n              set(input) {\\n                const toggle = is.boolean(input) ? input : muted;\\n                muted = toggle;\\n                instance[toggle ? 'mute' : 'unMute']();\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Source\\n            Object.defineProperty(player.media, 'currentSrc', {\\n              get() {\\n                return instance.getVideoUrl();\\n              }\\n            });\\n\\n            // Ended\\n            Object.defineProperty(player.media, 'ended', {\\n              get() {\\n                return player.currentTime === player.duration;\\n              }\\n            });\\n\\n            // Get available speeds\\n            const speeds = instance.getAvailablePlaybackRates();\\n            // Filter based on config\\n            player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n            // Set the tabindex to avoid focus entering iframe\\n            if (player.supported.ui && config.customControls) {\\n              player.media.setAttribute('tabindex', -1);\\n            }\\n            triggerEvent.call(player, player.media, 'timeupdate');\\n            triggerEvent.call(player, player.media, 'durationchange');\\n\\n            // Reset timer\\n            clearInterval(player.timers.buffering);\\n\\n            // Setup buffering\\n            player.timers.buffering = setInterval(() => {\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n\\n              // Trigger progress only when we actually buffer something\\n              if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n                triggerEvent.call(player, player.media, 'progress');\\n              }\\n\\n              // Set last buffer point\\n              player.media.lastBuffered = player.media.buffered;\\n\\n              // Bail if we're at 100%\\n              if (player.media.buffered === 1) {\\n                clearInterval(player.timers.buffering);\\n\\n                // Trigger event\\n                triggerEvent.call(player, player.media, 'canplaythrough');\\n              }\\n            }, 200);\\n\\n            // Rebuild UI\\n            if (config.customControls) {\\n              setTimeout(() => ui.build.call(player), 50);\\n            }\\n          },\\n          onStateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Reset timer\\n            clearInterval(player.timers.playing);\\n            const seeked = player.media.seeking && [1, 2].includes(event.data);\\n            if (seeked) {\\n              // Unset seeking and fire seeked event\\n              player.media.seeking = false;\\n              triggerEvent.call(player, player.media, 'seeked');\\n            }\\n\\n            // Handle events\\n            // -1   Unstarted\\n            // 0    Ended\\n            // 1    Playing\\n            // 2    Paused\\n            // 3    Buffering\\n            // 5    Video cued\\n            switch (event.data) {\\n              case -1:\\n                // Update scrubber\\n                triggerEvent.call(player, player.media, 'timeupdate');\\n\\n                // Get loaded % from YouTube\\n                player.media.buffered = instance.getVideoLoadedFraction();\\n                triggerEvent.call(player, player.media, 'progress');\\n                break;\\n              case 0:\\n                assurePlaybackState.call(player, false);\\n\\n                // YouTube doesn't support loop for a single video, so mimick it.\\n                if (player.media.loop) {\\n                  // YouTube needs a call to `stopVideo` before playing again\\n                  instance.stopVideo();\\n                  instance.playVideo();\\n                } else {\\n                  triggerEvent.call(player, player.media, 'ended');\\n                }\\n                break;\\n              case 1:\\n                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                  player.media.pause();\\n                } else {\\n                  assurePlaybackState.call(player, true);\\n                  triggerEvent.call(player, player.media, 'playing');\\n\\n                  // Poll to get playback progress\\n                  player.timers.playing = setInterval(() => {\\n                    triggerEvent.call(player, player.media, 'timeupdate');\\n                  }, 50);\\n\\n                  // Check duration again due to YouTube bug\\n                  // https://github.com/sampotts/plyr/issues/374\\n                  // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                  if (player.media.duration !== instance.getDuration()) {\\n                    player.media.duration = instance.getDuration();\\n                    triggerEvent.call(player, player.media, 'durationchange');\\n                  }\\n                }\\n                break;\\n              case 2:\\n                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (!player.muted) {\\n                  player.embed.unMute();\\n                }\\n                assurePlaybackState.call(player, false);\\n                break;\\n              case 3:\\n                // Trigger waiting event to add loading classes to container as the video buffers.\\n                triggerEvent.call(player, player.media, 'waiting');\\n                break;\\n            }\\n            triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n              code: event.data\\n            });\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  // ==========================================================================\\n  const media = {\\n    // Setup media\\n    setup() {\\n      // If there's no media, bail\\n      if (!this.media) {\\n        this.debug.warn('No media element found!');\\n        return;\\n      }\\n\\n      // Add type class\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n      // Add provider class\\n      toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n      // Add video class for embeds\\n      // This will require changes if audio embeds are added\\n      if (this.isEmbed) {\\n        toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n      }\\n\\n      // Inject the player wrapper\\n      if (this.isVideo) {\\n        // Create the wrapper div\\n        this.elements.wrapper = createElement('div', {\\n          class: this.config.classNames.video\\n        });\\n\\n        // Wrap the video in a container\\n        wrap(this.media, this.elements.wrapper);\\n\\n        // Poster image container\\n        this.elements.poster = createElement('div', {\\n          class: this.config.classNames.poster\\n        });\\n        this.elements.wrapper.appendChild(this.elements.poster);\\n      }\\n      if (this.isHTML5) {\\n        html5.setup.call(this);\\n      } else if (this.isYouTube) {\\n        youtube.setup.call(this);\\n      } else if (this.isVimeo) {\\n        vimeo.setup.call(this);\\n      }\\n    }\\n  };\\n\\n  const destroy = instance => {\\n    // Destroy our adsManager\\n    if (instance.manager) {\\n      instance.manager.destroy();\\n    }\\n\\n    // Destroy our adsManager\\n    if (instance.elements.displayContainer) {\\n      instance.elements.displayContainer.destroy();\\n    }\\n    instance.elements.container.remove();\\n  };\\n  class Ads {\\n    /**\\n     * Ads constructor.\\n     * @param {Object} player\\n     * @return {Ads}\\n     */\\n    constructor(player) {\\n      /**\\n       * Load the IMA SDK\\n       */\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Check if the Google IMA3 SDK is loaded or load it ourselves\\n        if (!is.object(window.google) || !is.object(window.google.ima)) {\\n          loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n            this.ready();\\n          }).catch(() => {\\n            // Script failed to load or is blocked\\n            this.trigger('error', new Error('Google IMA SDK failed to load'));\\n          });\\n        } else {\\n          this.ready();\\n        }\\n      });\\n      /**\\n       * Get the ads instance ready\\n       */\\n      _defineProperty$1(this, \\\"ready\\\", () => {\\n        // Double check we're enabled\\n        if (!this.enabled) {\\n          destroy(this);\\n        }\\n\\n        // Start ticking our safety timer. If the whole advertisement\\n        // thing doesn't resolve within our set time; we bail\\n        this.startSafetyTimer(12000, 'ready()');\\n\\n        // Clear the safety timer\\n        this.managerPromise.then(() => {\\n          this.clearSafetyTimer('onAdsManagerLoaded()');\\n        });\\n\\n        // Set listeners on the Plyr instance\\n        this.listeners();\\n\\n        // Setup the IMA SDK\\n        this.setupIMA();\\n      });\\n      /**\\n       * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n       * so here we define our ad container. This div is set up to render on top of the video player.\\n       * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n       * handle to the content video player - the SDK will poll the current time of our player to\\n       * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n       * mobile devices, this initialization is done as the result of a user action.\\n       */\\n      _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n        // Create the container for our advertisements\\n        this.elements.container = createElement('div', {\\n          class: this.player.config.classNames.ads\\n        });\\n        this.player.elements.container.appendChild(this.elements.container);\\n\\n        // So we can run VPAID2\\n        google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n        // Set language\\n        google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n        // Set playback for iOS10+\\n        google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n        // We assume the adContainer is the video container of the plyr element that will house the ads\\n        this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n        // Create ads loader\\n        this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n        // Listen and respond to ads loaded and error events\\n        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n        // Request video ads to be pre-loaded\\n        this.requestAds();\\n      });\\n      /**\\n       * Request advertisements\\n       */\\n      _defineProperty$1(this, \\\"requestAds\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        try {\\n          // Request video ads\\n          const request = new google.ima.AdsRequest();\\n          request.adTagUrl = this.tagUrl;\\n\\n          // Specify the linear and nonlinear slot sizes. This helps the SDK\\n          // to select the correct creative if multiple are returned\\n          request.linearAdSlotWidth = container.offsetWidth;\\n          request.linearAdSlotHeight = container.offsetHeight;\\n          request.nonLinearAdSlotWidth = container.offsetWidth;\\n          request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n          // We only overlay ads as we only support video.\\n          request.forceNonLinearFullSlot = false;\\n\\n          // Mute based on current state\\n          request.setAdWillPlayMuted(!this.player.muted);\\n          this.loader.requestAds(request);\\n        } catch (error) {\\n          this.onAdError(error);\\n        }\\n      });\\n      /**\\n       * Update the ad countdown\\n       * @param {Boolean} start\\n       */\\n      _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n        if (!start) {\\n          clearInterval(this.countdownTimer);\\n          this.elements.container.removeAttribute('data-badge-text');\\n          return;\\n        }\\n        const update = () => {\\n          const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n          const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n          this.elements.container.setAttribute('data-badge-text', label);\\n        };\\n        this.countdownTimer = setInterval(update, 100);\\n      });\\n      /**\\n       * This method is called whenever the ads are ready inside the AdDisplayContainer\\n       * @param {Event} event - adsManagerLoadedEvent\\n       */\\n      _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n        // Load could occur after a source change (race condition)\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Get the ads manager\\n        const settings = new google.ima.AdsRenderingSettings();\\n\\n        // Tell the SDK to save and restore content video state on our behalf\\n        settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n        settings.enablePreloading = true;\\n\\n        // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n        // so it can determine when to start the mid- and post-roll\\n        this.manager = event.getAdsManager(this.player, settings);\\n\\n        // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n        this.cuePoints = this.manager.getCuePoints();\\n\\n        // Add listeners to the required events\\n        // Advertisement error events\\n        this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n        // Advertisement regular events\\n        Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n          this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n        });\\n\\n        // Resolve our adsManager\\n        this.trigger('loaded');\\n      });\\n      _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n        // Add advertisement cue's within the time line if available\\n        if (!is.empty(this.cuePoints)) {\\n          this.cuePoints.forEach(cuePoint => {\\n            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n              const seekElement = this.player.elements.progress;\\n              if (is.element(seekElement)) {\\n                const cuePercentage = 100 / this.player.duration * cuePoint;\\n                const cue = createElement('span', {\\n                  class: this.player.config.classNames.cues\\n                });\\n                cue.style.left = `${cuePercentage.toString()}%`;\\n                seekElement.appendChild(cue);\\n              }\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n       * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n       * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n        // don't have ad object associated\\n        const ad = event.getAd();\\n        const adData = event.getAdData();\\n\\n        // Proxy event\\n        const dispatchEvent = type => {\\n          triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n        };\\n\\n        // Bubble the event\\n        dispatchEvent(event.type);\\n        switch (event.type) {\\n          case google.ima.AdEvent.Type.LOADED:\\n            // This is the first event sent for an ad - it is possible to determine whether the\\n            // ad is a video ad or an overlay\\n            this.trigger('loaded');\\n\\n            // Start countdown\\n            this.pollCountdown(true);\\n            if (!ad.isLinear()) {\\n              // Position AdDisplayContainer correctly for overlay\\n              ad.width = container.offsetWidth;\\n              ad.height = container.offsetHeight;\\n            }\\n\\n            // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n            break;\\n          case google.ima.AdEvent.Type.STARTED:\\n            // Set volume to match player\\n            this.manager.setVolume(this.player.volume);\\n            break;\\n          case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n            // All ads for the current videos are done. We can now request new advertisements\\n            // in case the video is re-played\\n\\n            // TODO: Example for what happens when a next video in a playlist would be loaded.\\n            // So here we load a new video when all ads are done.\\n            // Then we load new ads within a new adsManager. When the video\\n            // Is started - after - the ads are loaded, then we get ads.\\n            // You can also easily test cancelling and reloading by running\\n            // player.ads.cancel() and player.ads.play from the console I guess.\\n            // this.player.source = {\\n            //     type: 'video',\\n            //     title: 'View From A Blue Moon',\\n            //     sources: [{\\n            //         src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n            // 'video/mp4', }], poster:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n            // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n            // };\\n\\n            // TODO: So there is still this thing where a video should only be allowed to start\\n            // playing when the IMA SDK is ready or has failed\\n\\n            if (this.player.ended) {\\n              this.loadAds();\\n            } else {\\n              // The SDK won't allow new ads to be called without receiving a contentComplete()\\n              this.loader.contentComplete();\\n            }\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n            // This event indicates the ad has started - the video player can adjust the UI,\\n            // for example display a pause button and remaining time. Fired when content should\\n            // be paused. This usually happens right before an ad is about to cover the content\\n\\n            this.pauseContent();\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n            // This event indicates the ad has finished - the video player can perform\\n            // appropriate UI actions, such as removing the timer for remaining time detection.\\n            // Fired when content should be resumed. This usually happens when an ad finishes\\n            // or collapses\\n\\n            this.pollCountdown();\\n            this.resumeContent();\\n            break;\\n          case google.ima.AdEvent.Type.LOG:\\n            if (adData.adError) {\\n              this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n            }\\n            break;\\n        }\\n      });\\n      /**\\n       * Any ad error handling comes through here\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdError\\\", event => {\\n        this.cancel();\\n        this.player.debug.warn('Ads error', event);\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events. This ensures\\n       * the mid- and post-roll launch at the correct time. And\\n       * resize the advertisement when the player resizes\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        let time;\\n        this.player.on('canplay', () => {\\n          this.addCuePoints();\\n        });\\n        this.player.on('ended', () => {\\n          this.loader.contentComplete();\\n        });\\n        this.player.on('timeupdate', () => {\\n          time = this.player.currentTime;\\n        });\\n        this.player.on('seeked', () => {\\n          const seekedTime = this.player.currentTime;\\n          if (is.empty(this.cuePoints)) {\\n            return;\\n          }\\n          this.cuePoints.forEach((cuePoint, index) => {\\n            if (time < cuePoint && cuePoint < seekedTime) {\\n              this.manager.discardAdBreak();\\n              this.cuePoints.splice(index, 1);\\n            }\\n          });\\n        });\\n\\n        // Listen to the resizing of the window. And resize ad accordingly\\n        // TODO: eventually implement ResizeObserver\\n        window.addEventListener('resize', () => {\\n          if (this.manager) {\\n            this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n          }\\n        });\\n      });\\n      /**\\n       * Initialize the adsManager and start playing advertisements\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        if (!this.managerPromise) {\\n          this.resumeContent();\\n        }\\n\\n        // Play the requested advertisement whenever the adsManager is ready\\n        this.managerPromise.then(() => {\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n\\n          // Initialize the container. Must be done via a user action on mobile devices\\n          this.elements.displayContainer.initialize();\\n          try {\\n            if (!this.initialized) {\\n              // Initialize the ads manager. Ad rules playlist will start at this time\\n              this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n              // Call play to start showing the ad. Single video and overlay ads will\\n              // start at this time; the call will be ignored for ad rules\\n              this.manager.start();\\n            }\\n            this.initialized = true;\\n          } catch (adError) {\\n            // An error may be thrown if there was a problem with the\\n            // VAST response\\n            this.onAdError(adError);\\n          }\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Resume our video\\n       */\\n      _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n        // Hide the advertisement container\\n        this.elements.container.style.zIndex = '';\\n\\n        // Ad is stopped\\n        this.playing = false;\\n\\n        // Play video\\n        silencePromise(this.player.media.play());\\n      });\\n      /**\\n       * Pause our video\\n       */\\n      _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n        // Show the advertisement container\\n        this.elements.container.style.zIndex = 3;\\n\\n        // Ad is playing\\n        this.playing = true;\\n\\n        // Pause our video.\\n        this.player.media.pause();\\n      });\\n      /**\\n       * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n       * allowed to call new ads based on google policies, as they interpret this as an accidental\\n       * video requests. https://developers.google.com/interactive-\\n       * media-ads/docs/sdks/android/faq#8\\n       */\\n      _defineProperty$1(this, \\\"cancel\\\", () => {\\n        // Pause our video\\n        if (this.initialized) {\\n          this.resumeContent();\\n        }\\n\\n        // Tell our instance that we're done for now\\n        this.trigger('error');\\n\\n        // Re-create our adsManager\\n        this.loadAds();\\n      });\\n      /**\\n       * Re-create our adsManager\\n       */\\n      _defineProperty$1(this, \\\"loadAds\\\", () => {\\n        // Tell our adsManager to go bye bye\\n        this.managerPromise.then(() => {\\n          // Destroy our adsManager\\n          if (this.manager) {\\n            this.manager.destroy();\\n          }\\n\\n          // Re-set our adsManager promises\\n          this.managerPromise = new Promise(resolve => {\\n            this.on('loaded', resolve);\\n            this.player.debug.log(this.manager);\\n          });\\n          // Now that the manager has been destroyed set it to also be un-initialized\\n          this.initialized = false;\\n\\n          // Now request some new advertisements\\n          this.requestAds();\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Handles callbacks after an ad event was invoked\\n       * @param {String} event - Event type\\n       * @param args\\n       */\\n      _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n        const handlers = this.events[event];\\n        if (is.array(handlers)) {\\n          handlers.forEach(handler => {\\n            if (is.function(handler)) {\\n              handler.apply(this, args);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       * @return {Ads}\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        if (!is.array(this.events[event])) {\\n          this.events[event] = [];\\n        }\\n        this.events[event].push(callback);\\n        return this;\\n      });\\n      /**\\n       * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n       * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n       * advertisement is playing, or when a user action is required to start, then we clear the\\n       * timer on ad ready\\n       * @param {Number} time\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n        this.player.debug.log(`Safety timer invoked from: ${from}`);\\n        this.safetyTimer = setTimeout(() => {\\n          this.cancel();\\n          this.clearSafetyTimer('startSafetyTimer()');\\n        }, time);\\n      });\\n      /**\\n       * Clear our safety timer(s)\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n        if (!is.nullOrUndefined(this.safetyTimer)) {\\n          this.player.debug.log(`Safety timer cleared from: ${from}`);\\n          clearTimeout(this.safetyTimer);\\n          this.safetyTimer = null;\\n        }\\n      });\\n      this.player = player;\\n      this.config = player.config.ads;\\n      this.playing = false;\\n      this.initialized = false;\\n      this.elements = {\\n        container: null,\\n        displayContainer: null\\n      };\\n      this.manager = null;\\n      this.loader = null;\\n      this.cuePoints = null;\\n      this.events = {};\\n      this.safetyTimer = null;\\n      this.countdownTimer = null;\\n\\n      // Setup a promise to resolve when the IMA manager is ready\\n      this.managerPromise = new Promise((resolve, reject) => {\\n        // The ad is loaded and ready\\n        this.on('loaded', resolve);\\n\\n        // Ads failed\\n        this.on('error', reject);\\n      });\\n      this.load();\\n    }\\n    get enabled() {\\n      const {\\n        config\\n      } = this;\\n      return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n    }\\n    // Build the tag URL\\n    get tagUrl() {\\n      const {\\n        config\\n      } = this;\\n      if (is.url(config.tagUrl)) {\\n        return config.tagUrl;\\n      }\\n      const params = {\\n        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n        AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n        AV_URL: window.location.hostname,\\n        cb: Date.now(),\\n        AV_WIDTH: 640,\\n        AV_HEIGHT: 480,\\n        AV_CDIM2: config.publisherId\\n      };\\n      const base = 'https://go.aniview.com/api/adserver6/vast/';\\n      return `${base}?${buildUrlParams(params)}`;\\n    }\\n  }\\n\\n  /**\\n   * Returns a number whose value is limited to the given range.\\n   *\\n   * Example: limit the output of this computation to between 0 and 255\\n   * (x * 255).clamp(0, 255)\\n   *\\n   * @param {Number} input\\n   * @param {Number} min The lower boundary of the output range\\n   * @param {Number} max The upper boundary of the output range\\n   * @returns A number within the bounds of min and max\\n   * @type Number\\n   */\\n  function clamp(input = 0, min = 0, max = 255) {\\n    return Math.min(Math.max(input, min), max);\\n  }\\n\\n  // Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\n  const parseVtt = vttDataString => {\\n    const processedList = [];\\n    const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n    frames.forEach(frame => {\\n      const result = {};\\n      const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n      lines.forEach(line => {\\n        if (!is.number(result.startTime)) {\\n          // The line with start and end times on it is the first line of interest\\n          const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n          if (matchTimes) {\\n            result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n            result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n          }\\n        } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n          // If we already have the startTime, then we're definitely up to the text line(s)\\n          const lineSplit = line.trim().split('#xywh=');\\n          [result.text] = lineSplit;\\n\\n          // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n          if (lineSplit[1]) {\\n            [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n          }\\n        }\\n      });\\n      if (result.text) {\\n        processedList.push(result);\\n      }\\n    });\\n    return processedList;\\n  };\\n\\n  /**\\n   * Preview thumbnails for seek hover and scrubbing\\n   * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n   * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n   *\\n   * Notes:\\n   * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n   * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n   * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n   */\\n\\n  const fitRatio = (ratio, outer) => {\\n    const targetRatio = outer.width / outer.height;\\n    const result = {};\\n    if (ratio > targetRatio) {\\n      result.width = outer.width;\\n      result.height = 1 / ratio * outer.width;\\n    } else {\\n      result.height = outer.height;\\n      result.width = ratio * outer.height;\\n    }\\n    return result;\\n  };\\n  class PreviewThumbnails {\\n    /**\\n     * PreviewThumbnails constructor.\\n     * @param {Plyr} player\\n     * @return {PreviewThumbnails}\\n     */\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        // Toggle the regular seek tooltip\\n        if (this.player.elements.display.seekTooltip) {\\n          this.player.elements.display.seekTooltip.hidden = this.enabled;\\n        }\\n        if (!this.enabled) return;\\n        this.getThumbnails().then(() => {\\n          if (!this.enabled) {\\n            return;\\n          }\\n\\n          // Render DOM elements\\n          this.render();\\n\\n          // Check to see if thumb container size was specified manually in CSS\\n          this.determineContainerAutoSizing();\\n\\n          // Set up listeners\\n          this.listeners();\\n          this.loaded = true;\\n        });\\n      });\\n      // Download VTT files and parse them\\n      _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n        return new Promise(resolve => {\\n          const {\\n            src\\n          } = this.player.config.previewThumbnails;\\n          if (is.empty(src)) {\\n            throw new Error('Missing previewThumbnails.src config attribute');\\n          }\\n\\n          // Resolve promise\\n          const sortAndResolve = () => {\\n            // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n            this.thumbnails.sort((x, y) => x.height - y.height);\\n            this.player.debug.log('Preview thumbnails', this.thumbnails);\\n            resolve();\\n          };\\n\\n          // Via callback()\\n          if (is.function(src)) {\\n            src(thumbnails => {\\n              this.thumbnails = thumbnails;\\n              sortAndResolve();\\n            });\\n          }\\n          // VTT urls\\n          else {\\n            // If string, convert into single-element list\\n            const urls = is.string(src) ? [src] : src;\\n            // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n            const promises = urls.map(u => this.getThumbnail(u));\\n            // Resolve\\n            Promise.all(promises).then(sortAndResolve);\\n          }\\n        });\\n      });\\n      // Process individual VTT file\\n      _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n        return new Promise(resolve => {\\n          fetch(url).then(response => {\\n            const thumbnail = {\\n              frames: parseVtt(response),\\n              height: null,\\n              urlPrefix: ''\\n            };\\n\\n            // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n            // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n            // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n            if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n              thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n            }\\n\\n            // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n            const tempImage = new Image();\\n            tempImage.onload = () => {\\n              thumbnail.height = tempImage.naturalHeight;\\n              thumbnail.width = tempImage.naturalWidth;\\n              this.thumbnails.push(thumbnail);\\n              resolve();\\n            };\\n            tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n          });\\n        });\\n      });\\n      _defineProperty$1(this, \\\"startMove\\\", event => {\\n        if (!this.loaded) return;\\n        if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n        // Wait until media has a duration\\n        if (!this.player.media.duration) return;\\n        if (event.type === 'touchmove') {\\n          // Calculate seek hover position as approx video seconds\\n          this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n        } else {\\n          var _this$player$config$m, _this$player$config$m2;\\n          // Calculate seek hover position as approx video seconds\\n          const clientRect = this.player.elements.progress.getBoundingClientRect();\\n          const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n          this.seekTime = this.player.media.duration * (percentage / 100);\\n          if (this.seekTime < 0) {\\n            // The mousemove fires for 10+px out to the left\\n            this.seekTime = 0;\\n          }\\n          if (this.seekTime > this.player.media.duration - 1) {\\n            // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n            this.seekTime = this.player.media.duration - 1;\\n          }\\n          this.mousePosX = event.pageX;\\n\\n          // Set time text inside image container\\n          this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n          // Get marker point for time\\n          const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n            time: t\\n          }) => t === Math.round(this.seekTime));\\n\\n          // Append the point label to the tooltip\\n          if (point) {\\n            // this.elements.thumb.time.innerText.concat('\\\\n');\\n            this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n          }\\n        }\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      });\\n      _defineProperty$1(this, \\\"endMove\\\", () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n        // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n        if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n          this.mouseDown = true;\\n\\n          // Wait until media has a duration\\n          if (this.player.media.duration) {\\n            this.toggleScrubbingContainer(true);\\n            this.toggleThumbContainer(false, true);\\n\\n            // Download and show image\\n            this.showImageAtCurrentTime();\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n        this.mouseDown = false;\\n\\n        // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n        if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n          // The video was already seeked/loaded at the chosen time - hide immediately\\n          this.toggleScrubbingContainer(false);\\n        } else {\\n          // The video hasn't seeked yet. Wait for that\\n          once.call(this.player, this.player.media, 'timeupdate', () => {\\n            // Re-check mousedown - we might have already started scrubbing again\\n            if (!this.mouseDown) {\\n              this.toggleScrubbingContainer(false);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n        this.player.on('play', () => {\\n          this.toggleThumbContainer(false, true);\\n        });\\n        this.player.on('seeked', () => {\\n          this.toggleThumbContainer(false);\\n        });\\n        this.player.on('timeupdate', () => {\\n          this.lastTime = this.player.media.currentTime;\\n        });\\n      });\\n      /**\\n       * Create HTML elements for image containers\\n       */\\n      _defineProperty$1(this, \\\"render\\\", () => {\\n        // Create HTML element: plyr__preview-thumbnail-container\\n        this.elements.thumb.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.thumbContainer\\n        });\\n\\n        // Wrapper for the image for styling\\n        this.elements.thumb.imageContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.imageContainer\\n        });\\n        this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n        // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n        const timeContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.timeContainer\\n        });\\n        this.elements.thumb.time = createElement('span', {}, '00:00');\\n        timeContainer.appendChild(this.elements.thumb.time);\\n        this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n        // Inject the whole thumb\\n        if (is.element(this.player.elements.progress)) {\\n          this.player.elements.progress.appendChild(this.elements.thumb.container);\\n        }\\n\\n        // Create HTML element: plyr__preview-scrubbing-container\\n        this.elements.scrubbing.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n        });\\n        this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n      });\\n      _defineProperty$1(this, \\\"destroy\\\", () => {\\n        if (this.elements.thumb.container) {\\n          this.elements.thumb.container.remove();\\n        }\\n        if (this.elements.scrubbing.container) {\\n          this.elements.scrubbing.container.remove();\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n        if (this.mouseDown) {\\n          this.setScrubbingContainerSize();\\n        } else {\\n          this.setThumbContainerSizeAndPos();\\n        }\\n\\n        // Find the desired thumbnail index\\n        // TODO: Handle a video longer than the thumbs where thumbNum is null\\n        const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n        const hasThumb = thumbNum >= 0;\\n        let qualityIndex = 0;\\n\\n        // Show the thumb container if we're not scrubbing\\n        if (!this.mouseDown) {\\n          this.toggleThumbContainer(hasThumb);\\n        }\\n\\n        // No matching thumb found\\n        if (!hasThumb) {\\n          return;\\n        }\\n\\n        // Check to see if we've already downloaded higher quality versions of this image\\n        this.thumbnails.forEach((thumbnail, index) => {\\n          if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n            qualityIndex = index;\\n          }\\n        });\\n\\n        // Only proceed if either thumb num or thumbfilename has changed\\n        if (thumbNum !== this.showingThumb) {\\n          this.showingThumb = thumbNum;\\n          this.loadImage(qualityIndex);\\n        }\\n      });\\n      // Show the image that's currently specified in this.showingThumb\\n      _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n        const thumbNum = this.showingThumb;\\n        const thumbnail = this.thumbnails[qualityIndex];\\n        const {\\n          urlPrefix\\n        } = thumbnail;\\n        const frame = thumbnail.frames[thumbNum];\\n        const thumbFilename = thumbnail.frames[thumbNum].text;\\n        const thumbUrl = urlPrefix + thumbFilename;\\n        if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n          // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n          // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n          if (this.loadingImage && this.usingSprites) {\\n            this.loadingImage.onload = null;\\n          }\\n\\n          // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n          // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n          // images causes a flicker. Putting a new image over the top does not\\n          const previewImage = new Image();\\n          previewImage.src = thumbUrl;\\n          previewImage.dataset.index = thumbNum;\\n          previewImage.dataset.filename = thumbFilename;\\n          this.showingThumbFilename = thumbFilename;\\n          this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n          // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n          previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n          this.loadingImage = previewImage;\\n          this.removeOldImages(previewImage);\\n        } else {\\n          // Update the existing image\\n          this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n          this.currentImageElement.dataset.index = thumbNum;\\n          this.removeOldImages(this.currentImageElement);\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n        this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n        this.setImageSizeAndOffset(previewImage, frame);\\n        if (newImage) {\\n          this.currentImageContainer.appendChild(previewImage);\\n          this.currentImageElement = previewImage;\\n          if (!this.loadedImages.includes(thumbFilename)) {\\n            this.loadedImages.push(thumbFilename);\\n          }\\n        }\\n\\n        // Preload images before and after the current one\\n        // Show higher quality of the same frame\\n        // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n        this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n      });\\n      // Remove all preview images that aren't the designated current image\\n      _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n        // Get a list of all images, convert it from a DOM list to an array\\n        Array.from(this.currentImageContainer.children).forEach(image => {\\n          if (image.tagName.toLowerCase() !== 'img') {\\n            return;\\n          }\\n          const removeDelay = this.usingSprites ? 500 : 1000;\\n          if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n            // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n            // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n            // eslint-disable-next-line no-param-reassign\\n            image.dataset.deleting = true;\\n\\n            // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n            const {\\n              currentImageContainer\\n            } = this;\\n            setTimeout(() => {\\n              currentImageContainer.removeChild(image);\\n              this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n            }, removeDelay);\\n          }\\n        });\\n      });\\n      // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n      // This will only preload the lowest quality\\n      _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n        return new Promise(resolve => {\\n          setTimeout(() => {\\n            const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n            if (this.showingThumbFilename === oldThumbFilename) {\\n              // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n              let thumbnailsClone;\\n              if (forward) {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n              } else {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n              }\\n              let foundOne = false;\\n              thumbnailsClone.forEach(frame => {\\n                const newThumbFilename = frame.text;\\n                if (newThumbFilename !== oldThumbFilename) {\\n                  // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                  if (!this.loadedImages.includes(newThumbFilename)) {\\n                    foundOne = true;\\n                    this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                    const {\\n                      urlPrefix\\n                    } = this.thumbnails[0];\\n                    const thumbURL = urlPrefix + newThumbFilename;\\n                    const previewImage = new Image();\\n                    previewImage.src = thumbURL;\\n                    previewImage.onload = () => {\\n                      this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                      if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                      // We don't resolve until the thumb is loaded\\n                      resolve();\\n                    };\\n                  }\\n                }\\n              });\\n\\n              // If there are none to preload then we want to resolve immediately\\n              if (!foundOne) {\\n                resolve();\\n              }\\n            }\\n          }, 300);\\n        });\\n      });\\n      // If user has been hovering current image for half a second, look for a higher quality one\\n      _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n        if (currentQualityIndex < this.thumbnails.length - 1) {\\n          // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n          let previewImageHeight = previewImage.naturalHeight;\\n          if (this.usingSprites) {\\n            previewImageHeight = frame.h;\\n          }\\n          if (previewImageHeight < this.thumbContainerHeight) {\\n            // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n            setTimeout(() => {\\n              // Make sure the mouse hasn't already moved on and started hovering at another image\\n              if (this.showingThumbFilename === thumbFilename) {\\n                this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n                this.loadImage(currentQualityIndex + 1);\\n              }\\n            }, 300);\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n        this.elements.thumb.container.classList.toggle(className, toggle);\\n        if (!toggle && clearShowing) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n        this.elements.scrubbing.container.classList.toggle(className, toggle);\\n        if (!toggle) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n        if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n          // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n          this.sizeSpecifiedInCSS = true;\\n        }\\n      });\\n      // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n      _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n        const {\\n          imageContainer\\n        } = this.elements.thumb;\\n        if (!this.sizeSpecifiedInCSS) {\\n          const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n          imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n          const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n          const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n          imageContainer.style.height = `${thumbHeight}px`;\\n        }\\n        this.setThumbContainerPos();\\n      });\\n      _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n        const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n        const containerRect = this.player.elements.container.getBoundingClientRect();\\n        const {\\n          container\\n        } = this.elements.thumb;\\n        // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n        const min = containerRect.left - scrubberRect.left + 10;\\n        const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n        // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n        const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n        const clamped = clamp(position, min, max);\\n\\n        // Move the popover position\\n        container.style.left = `${clamped}px`;\\n\\n        // The arrow can follow the cursor\\n        container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n      });\\n      // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n      _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n        const {\\n          width,\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        this.elements.scrubbing.container.style.width = `${width}px`;\\n        this.elements.scrubbing.container.style.height = `${height}px`;\\n      });\\n      // Sprites need to be offset to the correct location\\n      _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n        if (!this.usingSprites) return;\\n\\n        // Find difference between height and preview container height\\n        const multiplier = this.thumbContainerHeight / frame.h;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.left = `-${frame.x * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.top = `-${frame.y * multiplier}px`;\\n      });\\n      this.player = player;\\n      this.thumbnails = [];\\n      this.loaded = false;\\n      this.lastMouseMoveTime = Date.now();\\n      this.mouseDown = false;\\n      this.loadedImages = [];\\n      this.elements = {\\n        thumb: {},\\n        scrubbing: {}\\n      };\\n      this.load();\\n    }\\n    get enabled() {\\n      return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n    }\\n    get currentImageContainer() {\\n      return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n    }\\n    get usingSprites() {\\n      return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n    }\\n    get thumbAspectRatio() {\\n      if (this.usingSprites) {\\n        return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n      }\\n      return this.thumbnails[0].width / this.thumbnails[0].height;\\n    }\\n    get thumbContainerHeight() {\\n      if (this.mouseDown) {\\n        const {\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        return height;\\n      }\\n\\n      // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n      if (this.sizeSpecifiedInCSS) {\\n        return this.elements.thumb.imageContainer.clientHeight;\\n      }\\n      return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n    }\\n    get currentImageElement() {\\n      return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n    }\\n    set currentImageElement(element) {\\n      if (this.mouseDown) {\\n        this.currentScrubbingImageElement = element;\\n      } else {\\n        this.currentThumbnailImageElement = element;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  const source = {\\n    // Add elements to HTML5 media (source, tracks, etc)\\n    insertElements(type, attributes) {\\n      if (is.string(attributes)) {\\n        insertElement(type, this.media, {\\n          src: attributes\\n        });\\n      } else if (is.array(attributes)) {\\n        attributes.forEach(attribute => {\\n          insertElement(type, this.media, attribute);\\n        });\\n      }\\n    },\\n    // Update source\\n    // Sources are not checked for support so be careful\\n    change(input) {\\n      if (!getDeep(input, 'sources.length')) {\\n        this.debug.warn('Invalid source format');\\n        return;\\n      }\\n\\n      // Cancel current network requests\\n      html5.cancelRequests.call(this);\\n\\n      // Destroy instance and re-setup\\n      this.destroy.call(this, () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const {\\n          sources,\\n          type\\n        } = input;\\n        const [{\\n          provider = providers.html5,\\n          src\\n        }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : {\\n          src\\n        };\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes)\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      }, true);\\n    }\\n  };\\n\\n  // Private properties\\n  // TODO: Use a WeakMap for private globals\\n  // const globals = new WeakMap();\\n\\n  // Plyr instance\\n  class Plyr {\\n    constructor(target, options) {\\n      /**\\n       * Play the media, or play the advertisement (if they are not blocked)\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        if (!is.function(this.media.play)) {\\n          return null;\\n        }\\n\\n        // Intecept play with ads\\n        if (this.ads && this.ads.enabled) {\\n          this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n        }\\n\\n        // Return the promise (for HTML5)\\n        return this.media.play();\\n      });\\n      /**\\n       * Pause the media\\n       */\\n      _defineProperty$1(this, \\\"pause\\\", () => {\\n        if (!this.playing || !is.function(this.media.pause)) {\\n          return null;\\n        }\\n        return this.media.pause();\\n      });\\n      /**\\n       * Toggle playback based on current status\\n       * @param {Boolean} input\\n       */\\n      _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n        // Toggle based on current state if nothing passed\\n        const toggle = is.boolean(input) ? input : !this.playing;\\n        if (toggle) {\\n          return this.play();\\n        }\\n        return this.pause();\\n      });\\n      /**\\n       * Stop playback\\n       */\\n      _defineProperty$1(this, \\\"stop\\\", () => {\\n        if (this.isHTML5) {\\n          this.pause();\\n          this.restart();\\n        } else if (is.function(this.media.stop)) {\\n          this.media.stop();\\n        }\\n      });\\n      /**\\n       * Restart playback\\n       */\\n      _defineProperty$1(this, \\\"restart\\\", () => {\\n        this.currentTime = 0;\\n      });\\n      /**\\n       * Rewind\\n       * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n        this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Fast forward\\n       * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n        this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Increase volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n        const volume = this.media.muted ? 0 : this.volume;\\n        this.volume = volume + (is.number(step) ? step : 0);\\n      });\\n      /**\\n       * Decrease volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n        this.increaseVolume(-step);\\n      });\\n      /**\\n       * Trigger the airplay dialog\\n       * TODO: update player with state, support, enabled\\n       */\\n      _defineProperty$1(this, \\\"airplay\\\", () => {\\n        // Show dialog if supported\\n        if (support.airplay) {\\n          this.media.webkitShowPlaybackTargetPicker();\\n        }\\n      });\\n      /**\\n       * Toggle the player controls\\n       * @param {Boolean} [toggle] - Whether to show the controls\\n       */\\n      _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n        // Don't toggle if missing UI support or if it's audio\\n        if (this.supported.ui && !this.isAudio) {\\n          // Get state before change\\n          const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n          // Negate the argument if not undefined since adding the class to hides the controls\\n          const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n          // Apply and get updated state\\n          const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n          // Close menu\\n          if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n            controls.toggleMenu.call(this, false);\\n          }\\n\\n          // Trigger event on change\\n          if (hiding !== isHidden) {\\n            const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n            triggerEvent.call(this, this.media, eventName);\\n          }\\n          return !hiding;\\n        }\\n        return false;\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        on.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Add event listeners once\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n        once.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Remove event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n        off(this.elements.container, event, callback);\\n      });\\n      /**\\n       * Destroy an instance\\n       * Event listeners are removed when elements are removed\\n       * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n       * @param {Function} callback - Callback for when destroy is complete\\n       * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n       */\\n      _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n        if (!this.ready) {\\n          return;\\n        }\\n        const done = () => {\\n          // Reset overflow (incase destroyed while in fullscreen)\\n          document.body.style.overflow = '';\\n\\n          // GC for embed\\n          this.embed = null;\\n\\n          // If it's a soft destroy, make minimal changes\\n          if (soft) {\\n            if (Object.keys(this.elements).length) {\\n              // Remove elements\\n              removeElement(this.elements.buttons.play);\\n              removeElement(this.elements.captions);\\n              removeElement(this.elements.controls);\\n              removeElement(this.elements.wrapper);\\n\\n              // Clear for GC\\n              this.elements.buttons.play = null;\\n              this.elements.captions = null;\\n              this.elements.controls = null;\\n              this.elements.wrapper = null;\\n            }\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback();\\n            }\\n          } else {\\n            // Unbind listeners\\n            unbindListeners.call(this);\\n\\n            // Cancel current network requests\\n            html5.cancelRequests.call(this);\\n\\n            // Replace the container with the original element provided\\n            replaceElement(this.elements.original, this.elements.container);\\n\\n            // Event\\n            triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback.call(this.elements.original);\\n            }\\n\\n            // Reset state\\n            this.ready = false;\\n\\n            // Clear for garbage collection\\n            setTimeout(() => {\\n              this.elements = null;\\n              this.media = null;\\n            }, 200);\\n          }\\n        };\\n\\n        // Stop playback\\n        this.stop();\\n\\n        // Clear timeouts\\n        clearTimeout(this.timers.loading);\\n        clearTimeout(this.timers.controls);\\n        clearTimeout(this.timers.resized);\\n\\n        // Provider specific stuff\\n        if (this.isHTML5) {\\n          // Restore native video controls\\n          ui.toggleNativeControls.call(this, true);\\n\\n          // Clean up\\n          done();\\n        } else if (this.isYouTube) {\\n          // Clear timers\\n          clearInterval(this.timers.buffering);\\n          clearInterval(this.timers.playing);\\n\\n          // Destroy YouTube API\\n          if (this.embed !== null && is.function(this.embed.destroy)) {\\n            this.embed.destroy();\\n          }\\n\\n          // Clean up\\n          done();\\n        } else if (this.isVimeo) {\\n          // Destroy Vimeo API\\n          // then clean up (wait, to prevent postmessage errors)\\n          if (this.embed !== null) {\\n            this.embed.unload().then(done);\\n          }\\n\\n          // Vimeo does not always return\\n          setTimeout(done, 200);\\n        }\\n      });\\n      /**\\n       * Check for support for a mime type (HTML5 only)\\n       * @param {String} type - Mime type\\n       */\\n      _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n      this.timers = {};\\n\\n      // State\\n      this.ready = false;\\n      this.loading = false;\\n      this.failed = false;\\n\\n      // Touch device\\n      this.touch = support.touch;\\n\\n      // Set the media element\\n      this.media = target;\\n\\n      // String selector passed\\n      if (is.string(this.media)) {\\n        this.media = document.querySelectorAll(this.media);\\n      }\\n\\n      // jQuery, NodeList or Array passed, use first element\\n      if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n        // eslint-disable-next-line\\n        this.media = this.media[0];\\n      }\\n\\n      // Set config\\n      this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })());\\n\\n      // Elements cache\\n      this.elements = {\\n        container: null,\\n        fullscreen: null,\\n        captions: null,\\n        buttons: {},\\n        display: {},\\n        progress: {},\\n        inputs: {},\\n        settings: {\\n          popup: null,\\n          menu: null,\\n          panels: {},\\n          buttons: {}\\n        }\\n      };\\n\\n      // Captions\\n      this.captions = {\\n        active: null,\\n        currentTrack: -1,\\n        meta: new WeakMap()\\n      };\\n\\n      // Fullscreen\\n      this.fullscreen = {\\n        active: false\\n      };\\n\\n      // Options\\n      this.options = {\\n        speed: [],\\n        quality: []\\n      };\\n\\n      // Debugging\\n      // TODO: move to globals\\n      this.debug = new Console(this.config.debug);\\n\\n      // Log config options and support\\n      this.debug.log('Config', this.config);\\n      this.debug.log('Support', support);\\n\\n      // We need an element to setup\\n      if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n        this.debug.error('Setup failed: no suitable element passed');\\n        return;\\n      }\\n\\n      // Bail if the element is initialized\\n      if (this.media.plyr) {\\n        this.debug.warn('Target already setup');\\n        return;\\n      }\\n\\n      // Bail if not enabled\\n      if (!this.config.enabled) {\\n        this.debug.error('Setup failed: disabled by config');\\n        return;\\n      }\\n\\n      // Bail if disabled or no basic support\\n      // You may want to disable certain UAs etc\\n      if (!support.check().api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n\\n      // Cache original element state for .destroy()\\n      const clone = this.media.cloneNode(true);\\n      clone.autoplay = false;\\n      this.elements.original = clone;\\n\\n      // Set media type based on tag or data attribute\\n      // Supported: video, audio, vimeo, youtube\\n      const _type = this.media.tagName.toLowerCase();\\n      // Embed properties\\n      let iframe = null;\\n      let url = null;\\n\\n      // Different setup based on type\\n      switch (_type) {\\n        case 'div':\\n          // Find the frame\\n          iframe = this.media.querySelector('iframe');\\n\\n          // <iframe> type\\n          if (is.element(iframe)) {\\n            // Detect provider\\n            url = parseUrl(iframe.getAttribute('src'));\\n            this.provider = getProviderByUrl(url.toString());\\n\\n            // Rework elements\\n            this.elements.container = this.media;\\n            this.media = iframe;\\n\\n            // Reset classname\\n            this.elements.container.className = '';\\n\\n            // Get attributes from URL and set config\\n            if (url.search.length) {\\n              const truthy = ['1', 'true'];\\n              if (truthy.includes(url.searchParams.get('autoplay'))) {\\n                this.config.autoplay = true;\\n              }\\n              if (truthy.includes(url.searchParams.get('loop'))) {\\n                this.config.loop.active = true;\\n              }\\n\\n              // TODO: replace fullscreen.iosNative with this playsinline config option\\n              // YouTube requires the playsinline in the URL\\n              if (this.isYouTube) {\\n                this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n                this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n              } else {\\n                this.config.playsinline = true;\\n              }\\n            }\\n          } else {\\n            // <div> with attributes\\n            this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n            // Remove attribute\\n            this.media.removeAttribute(this.config.attributes.embed.provider);\\n          }\\n\\n          // Unsupported or missing provider\\n          if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n            this.debug.error('Setup failed: Invalid provider');\\n            return;\\n          }\\n\\n          // Audio will come later for external providers\\n          this.type = types.video;\\n          break;\\n        case 'video':\\n        case 'audio':\\n          this.type = _type;\\n          this.provider = providers.html5;\\n\\n          // Get config from attributes\\n          if (this.media.hasAttribute('crossorigin')) {\\n            this.config.crossorigin = true;\\n          }\\n          if (this.media.hasAttribute('autoplay')) {\\n            this.config.autoplay = true;\\n          }\\n          if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n            this.config.playsinline = true;\\n          }\\n          if (this.media.hasAttribute('muted')) {\\n            this.config.muted = true;\\n          }\\n          if (this.media.hasAttribute('loop')) {\\n            this.config.loop.active = true;\\n          }\\n          break;\\n        default:\\n          this.debug.error('Setup failed: unsupported type');\\n          return;\\n      }\\n\\n      // Check for support again but with type\\n      this.supported = support.check(this.type, this.provider);\\n\\n      // If no support for even API, bail\\n      if (!this.supported.api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n      this.eventListeners = [];\\n\\n      // Create listeners\\n      this.listeners = new Listeners(this);\\n\\n      // Setup local storage for user settings\\n      this.storage = new Storage(this);\\n\\n      // Store reference\\n      this.media.plyr = this;\\n\\n      // Wrap media\\n      if (!is.element(this.elements.container)) {\\n        this.elements.container = createElement('div');\\n        wrap(this.media, this.elements.container);\\n      }\\n\\n      // Migrate custom properties from media to container (so they work 😉)\\n      ui.migrateStyles.call(this);\\n\\n      // Add style hook\\n      ui.addStyleHook.call(this);\\n\\n      // Setup media\\n      media.setup.call(this);\\n\\n      // Listen for events if debugging\\n      if (this.config.debug) {\\n        on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n          this.debug.log(`event: ${event.type}`);\\n        });\\n      }\\n\\n      // Setup fullscreen\\n      this.fullscreen = new Fullscreen(this);\\n\\n      // Setup interface\\n      // If embed but not fully supported, build interface now to avoid flash of controls\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        ui.build.call(this);\\n      }\\n\\n      // Container listeners\\n      this.listeners.container();\\n\\n      // Global listeners\\n      this.listeners.global();\\n\\n      // Setup ads if provided\\n      if (this.config.ads.enabled) {\\n        this.ads = new Ads(this);\\n      }\\n\\n      // Autoplay if required\\n      if (this.isHTML5 && this.config.autoplay) {\\n        this.once('canplay', () => silencePromise(this.play()));\\n      }\\n\\n      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n      this.lastSeekTime = 0;\\n\\n      // Setup preview thumbnails if enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n\\n    // ---------------------------------------\\n    // API\\n    // ---------------------------------------\\n\\n    /**\\n     * Types and provider helpers\\n     */\\n    get isHTML5() {\\n      return this.provider === providers.html5;\\n    }\\n    get isEmbed() {\\n      return this.isYouTube || this.isVimeo;\\n    }\\n    get isYouTube() {\\n      return this.provider === providers.youtube;\\n    }\\n    get isVimeo() {\\n      return this.provider === providers.vimeo;\\n    }\\n    get isVideo() {\\n      return this.type === types.video;\\n    }\\n    get isAudio() {\\n      return this.type === types.audio;\\n    }\\n    /**\\n     * Get playing state\\n     */\\n    get playing() {\\n      return Boolean(this.ready && !this.paused && !this.ended);\\n    }\\n\\n    /**\\n     * Get paused state\\n     */\\n    get paused() {\\n      return Boolean(this.media.paused);\\n    }\\n\\n    /**\\n     * Get stopped state\\n     */\\n    get stopped() {\\n      return Boolean(this.paused && this.currentTime === 0);\\n    }\\n\\n    /**\\n     * Get ended state\\n     */\\n    get ended() {\\n      return Boolean(this.media.ended);\\n    }\\n    /**\\n     * Seek to a time\\n     * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n     */\\n    set currentTime(input) {\\n      // Bail if media duration isn't available yet\\n      if (!this.duration) {\\n        return;\\n      }\\n\\n      // Validate input\\n      const inputIsValid = is.number(input) && input > 0;\\n\\n      // Set\\n      this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n      // Logging\\n      this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n    }\\n\\n    /**\\n     * Get current time\\n     */\\n    get currentTime() {\\n      return Number(this.media.currentTime);\\n    }\\n\\n    /**\\n     * Get buffered\\n     */\\n    get buffered() {\\n      const {\\n        buffered\\n      } = this.media;\\n\\n      // YouTube / Vimeo return a float between 0-1\\n      if (is.number(buffered)) {\\n        return buffered;\\n      }\\n\\n      // HTML5\\n      // TODO: Handle buffered chunks of the media\\n      // (i.e. seek to another section buffers only that section)\\n      if (buffered && buffered.length && this.duration > 0) {\\n        return buffered.end(0) / this.duration;\\n      }\\n      return 0;\\n    }\\n\\n    /**\\n     * Get seeking status\\n     */\\n    get seeking() {\\n      return Boolean(this.media.seeking);\\n    }\\n\\n    /**\\n     * Get the duration of the current media\\n     */\\n    get duration() {\\n      // Faux duration set via config\\n      const fauxDuration = parseFloat(this.config.duration);\\n      // Media duration can be NaN or Infinity before the media has loaded\\n      const realDuration = (this.media || {}).duration;\\n      const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n      // If config duration is funky, use regular duration\\n      return fauxDuration || duration;\\n    }\\n\\n    /**\\n     * Set the player volume\\n     * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n     */\\n    set volume(value) {\\n      let volume = value;\\n      const max = 1;\\n      const min = 0;\\n      if (is.string(volume)) {\\n        volume = Number(volume);\\n      }\\n\\n      // Load volume from storage if no value specified\\n      if (!is.number(volume)) {\\n        volume = this.storage.get('volume');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.number(volume)) {\\n        ({\\n          volume\\n        } = this.config);\\n      }\\n\\n      // Maximum is volumeMax\\n      if (volume > max) {\\n        volume = max;\\n      }\\n      // Minimum is volumeMin\\n      if (volume < min) {\\n        volume = min;\\n      }\\n\\n      // Update config\\n      this.config.volume = volume;\\n\\n      // Set the player volume\\n      this.media.volume = volume;\\n\\n      // If muted, and we're increasing volume manually, reset muted state\\n      if (!is.empty(value) && this.muted && volume > 0) {\\n        this.muted = false;\\n      }\\n    }\\n\\n    /**\\n     * Get the current player volume\\n     */\\n    get volume() {\\n      return Number(this.media.volume);\\n    }\\n    /**\\n     * Set muted state\\n     * @param {Boolean} mute\\n     */\\n    set muted(mute) {\\n      let toggle = mute;\\n\\n      // Load muted state from storage\\n      if (!is.boolean(toggle)) {\\n        toggle = this.storage.get('muted');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.boolean(toggle)) {\\n        toggle = this.config.muted;\\n      }\\n\\n      // Update config\\n      this.config.muted = toggle;\\n\\n      // Set mute on the player\\n      this.media.muted = toggle;\\n    }\\n\\n    /**\\n     * Get current muted state\\n     */\\n    get muted() {\\n      return Boolean(this.media.muted);\\n    }\\n\\n    /**\\n     * Check if the media has audio\\n     */\\n    get hasAudio() {\\n      // Assume yes for all non HTML5 (as we can't tell...)\\n      if (!this.isHTML5) {\\n        return true;\\n      }\\n      if (this.isAudio) {\\n        return true;\\n      }\\n\\n      // Get audio tracks\\n      return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n    }\\n\\n    /**\\n     * Set playback speed\\n     * @param {Number} input - the speed of playback (0.5-2.0)\\n     */\\n    set speed(input) {\\n      let speed = null;\\n      if (is.number(input)) {\\n        speed = input;\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.storage.get('speed');\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.config.speed.selected;\\n      }\\n\\n      // Clamp to min/max\\n      const {\\n        minimumSpeed: min,\\n        maximumSpeed: max\\n      } = this;\\n      speed = clamp(speed, min, max);\\n\\n      // Update config\\n      this.config.speed.selected = speed;\\n\\n      // Set media speed\\n      setTimeout(() => {\\n        if (this.media) {\\n          this.media.playbackRate = speed;\\n        }\\n      }, 0);\\n    }\\n\\n    /**\\n     * Get current playback speed\\n     */\\n    get speed() {\\n      return Number(this.media.playbackRate);\\n    }\\n\\n    /**\\n     * Get the minimum allowed speed\\n     */\\n    get minimumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.min(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 0.5;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 0.0625;\\n    }\\n\\n    /**\\n     * Get the maximum allowed speed\\n     */\\n    get maximumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.max(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 2;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 16;\\n    }\\n\\n    /**\\n     * Set playback quality\\n     * Currently HTML5 & YouTube only\\n     * @param {Number} input - Quality level\\n     */\\n    set quality(input) {\\n      const config = this.config.quality;\\n      const options = this.options.quality;\\n      if (!options.length) {\\n        return;\\n      }\\n      let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n      let updateStorage = true;\\n      if (!options.includes(quality)) {\\n        const value = closest(options, quality);\\n        this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n        quality = value;\\n\\n        // Don't update storage if quality is not supported\\n        updateStorage = false;\\n      }\\n\\n      // Update config\\n      config.selected = quality;\\n\\n      // Set quality\\n      this.media.quality = quality;\\n\\n      // Save to storage\\n      if (updateStorage) {\\n        this.storage.set({\\n          quality\\n        });\\n      }\\n    }\\n\\n    /**\\n     * Get current quality level\\n     */\\n    get quality() {\\n      return this.media.quality;\\n    }\\n\\n    /**\\n     * Toggle loop\\n     * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n     * @param {Boolean} input - Whether to loop or not\\n     */\\n    set loop(input) {\\n      const toggle = is.boolean(input) ? input : this.config.loop.active;\\n      this.config.loop.active = toggle;\\n      this.media.loop = toggle;\\n\\n      // Set default to be a true toggle\\n      /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n           switch (type) {\\n              case 'start':\\n                  if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                      this.config.loop.end = null;\\n                  }\\n                  this.config.loop.start = this.currentTime;\\n                  // this.config.loop.indicator.start = this.elements.display.played.value;\\n                  break;\\n               case 'end':\\n                  if (this.config.loop.start >= this.currentTime) {\\n                      return this;\\n                  }\\n                  this.config.loop.end = this.currentTime;\\n                  // this.config.loop.indicator.end = this.elements.display.played.value;\\n                  break;\\n               case 'all':\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = this.duration - 2;\\n                  this.config.loop.indicator.start = 0;\\n                  this.config.loop.indicator.end = 100;\\n                  break;\\n               case 'toggle':\\n                  if (this.config.loop.active) {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = null;\\n                  } else {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = this.duration - 2;\\n                  }\\n                  break;\\n               default:\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = null;\\n                  break;\\n          } */\\n    }\\n\\n    /**\\n     * Get current loop state\\n     */\\n    get loop() {\\n      return Boolean(this.media.loop);\\n    }\\n\\n    /**\\n     * Set new media source\\n     * @param {Object} input - The new source object (see docs)\\n     */\\n    set source(input) {\\n      source.change.call(this, input);\\n    }\\n\\n    /**\\n     * Get current source\\n     */\\n    get source() {\\n      return this.media.currentSrc;\\n    }\\n\\n    /**\\n     * Get a download URL (either source or custom)\\n     */\\n    get download() {\\n      const {\\n        download\\n      } = this.config.urls;\\n      return is.url(download) ? download : this.source;\\n    }\\n\\n    /**\\n     * Set the download URL\\n     */\\n    set download(input) {\\n      if (!is.url(input)) {\\n        return;\\n      }\\n      this.config.urls.download = input;\\n      controls.setDownloadUrl.call(this);\\n    }\\n\\n    /**\\n     * Set the poster image for a video\\n     * @param {String} input - the URL for the new poster image\\n     */\\n    set poster(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Poster can only be set for video');\\n        return;\\n      }\\n      ui.setPoster.call(this, input, false).catch(() => {});\\n    }\\n\\n    /**\\n     * Get the current poster image\\n     */\\n    get poster() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n    }\\n\\n    /**\\n     * Get the current aspect ratio in use\\n     */\\n    get ratio() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n      return is.array(ratio) ? ratio.join(':') : ratio;\\n    }\\n\\n    /**\\n     * Set video aspect ratio\\n     */\\n    set ratio(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Aspect ratio can only be set for video');\\n        return;\\n      }\\n      if (!is.string(input) || !validateAspectRatio(input)) {\\n        this.debug.error(`Invalid aspect ratio specified (${input})`);\\n        return;\\n      }\\n      this.config.ratio = reduceAspectRatio(input);\\n      setAspectRatio.call(this);\\n    }\\n\\n    /**\\n     * Set the autoplay state\\n     * @param {Boolean} input - Whether to autoplay or not\\n     */\\n    set autoplay(input) {\\n      this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n    }\\n\\n    /**\\n     * Get the current autoplay state\\n     */\\n    get autoplay() {\\n      return Boolean(this.config.autoplay);\\n    }\\n\\n    /**\\n     * Toggle captions\\n     * @param {Boolean} input - Whether to enable captions\\n     */\\n    toggleCaptions(input) {\\n      captions.toggle.call(this, input, false);\\n    }\\n\\n    /**\\n     * Set the caption track by index\\n     * @param {Number} input - Caption index\\n     */\\n    set currentTrack(input) {\\n      captions.set.call(this, input, false);\\n      captions.setup.call(this);\\n    }\\n\\n    /**\\n     * Get the current caption track index (-1 if disabled)\\n     */\\n    get currentTrack() {\\n      const {\\n        toggled,\\n        currentTrack\\n      } = this.captions;\\n      return toggled ? currentTrack : -1;\\n    }\\n\\n    /**\\n     * Set the wanted language for captions\\n     * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n     * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n     */\\n    set language(input) {\\n      captions.setLanguage.call(this, input, false);\\n    }\\n\\n    /**\\n     * Get the current track's language\\n     */\\n    get language() {\\n      return (captions.getCurrentTrack.call(this) || {}).language;\\n    }\\n\\n    /**\\n     * Toggle picture-in-picture playback on WebKit/MacOS\\n     * TODO: update player with state, support, enabled\\n     * TODO: detect outside changes\\n     */\\n    set pip(input) {\\n      // Bail if no support\\n      if (!support.pip) {\\n        return;\\n      }\\n\\n      // Toggle based on current state if not passed\\n      const toggle = is.boolean(input) ? input : !this.pip;\\n\\n      // Toggle based on current state\\n      // Safari\\n      if (is.function(this.media.webkitSetPresentationMode)) {\\n        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n      }\\n\\n      // Chrome\\n      if (is.function(this.media.requestPictureInPicture)) {\\n        if (!this.pip && toggle) {\\n          this.media.requestPictureInPicture();\\n        } else if (this.pip && !toggle) {\\n          document.exitPictureInPicture();\\n        }\\n      }\\n    }\\n\\n    /**\\n     * Get the current picture-in-picture state\\n     */\\n    get pip() {\\n      if (!support.pip) {\\n        return null;\\n      }\\n\\n      // Safari\\n      if (!is.empty(this.media.webkitPresentationMode)) {\\n        return this.media.webkitPresentationMode === pip.active;\\n      }\\n\\n      // Chrome\\n      return this.media === document.pictureInPictureElement;\\n    }\\n\\n    /**\\n     * Sets the preview thumbnails for the current source\\n     */\\n    setPreviewThumbnails(thumbnailSource) {\\n      if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n        this.previewThumbnails.destroy();\\n        this.previewThumbnails = null;\\n      }\\n      Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n      // Create new instance if it is still enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n    /**\\n     * Check for support\\n     * @param {String} type - Player type (audio/video)\\n     * @param {String} provider - Provider (html5/youtube/vimeo)\\n     */\\n    static supported(type, provider) {\\n      return support.check(type, provider);\\n    }\\n\\n    /**\\n     * Load an SVG sprite into the page\\n     * @param {String} url - URL for the SVG sprite\\n     * @param {String} [id] - Unique ID\\n     */\\n    static loadSprite(url, id) {\\n      return loadSprite(url, id);\\n    }\\n\\n    /**\\n     * Setup multiple instances\\n     * @param {*} selector\\n     * @param {Object} options\\n     */\\n    static setup(selector, options = {}) {\\n      let targets = null;\\n      if (is.string(selector)) {\\n        targets = Array.from(document.querySelectorAll(selector));\\n      } else if (is.nodeList(selector)) {\\n        targets = Array.from(selector);\\n      } else if (is.array(selector)) {\\n        targets = selector.filter(is.element);\\n      }\\n      if (is.empty(targets)) {\\n        return null;\\n      }\\n      return targets.map(t => new Plyr(t, options));\\n    }\\n  }\\n  Plyr.defaults = cloneDeep(defaults);\\n\\n  return Plyr;\\n\\n}));\\n//# sourceMappingURL=plyr.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: null,\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.min.mjs b/node_modules/plyr/dist/plyr.min.mjs\nindex a865566..755f323 100644\n--- a/node_modules/plyr/dist/plyr.min.mjs\n+++ b/node_modules/plyr/dist/plyr.min.mjs\n@@ -1 +1 @@\n-function _defineProperty$1(e,t,i){return(t=_toPropertyKey(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function _toPrimitive(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}function _toPropertyKey(e){var t=_toPrimitive(e,\"string\");return\"symbol\"==typeof t?t:String(t)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function _defineProperties(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,i){return t&&_defineProperties(e.prototype,t),i&&_defineProperties(e,i),e}function _defineProperty(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(i),!0).forEach((function(t){_defineProperty(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}var defaults$1={addCSS:!0,thumbWidth:15,watch:!0};function matches$1(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}var getConstructor$1=function(e){return null!=e?e.constructor:null},instanceOf$1=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined$1=function(e){return null==e},isObject$1=function(e){return getConstructor$1(e)===Object},isNumber$1=function(e){return getConstructor$1(e)===Number&&!Number.isNaN(e)},isString$1=function(e){return getConstructor$1(e)===String},isBoolean$1=function(e){return getConstructor$1(e)===Boolean},isFunction$1=function(e){return getConstructor$1(e)===Function},isArray$1=function(e){return Array.isArray(e)},isNodeList$1=function(e){return instanceOf$1(e,NodeList)},isElement$1=function(e){return instanceOf$1(e,Element)},isEvent$1=function(e){return instanceOf$1(e,Event)},isEmpty$1=function(e){return isNullOrUndefined$1(e)||(isString$1(e)||isArray$1(e)||isNodeList$1(e))&&!e.length||isObject$1(e)&&!Object.keys(e).length},is$1={nullOrUndefined:isNullOrUndefined$1,object:isObject$1,number:isNumber$1,string:isString$1,boolean:isBoolean$1,function:isFunction$1,array:isArray$1,nodeList:isNodeList$1,element:isElement$1,event:isEvent$1,empty:isEmpty$1};function getDecimalPlaces(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var i=getDecimalPlaces(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,i){_classCallCheck(this,e),is$1.element(t)?this.element=t:is$1.string(t)&&(this.element=document.querySelector(t)),is$1.element(this.element)&&is$1.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults$1,{},i),this.init())}return _createClass(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!is$1.event(t))return null;var i,s=t.target,n=t.changedTouches[0],r=parseFloat(s.getAttribute(\"min\"))||0,a=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),r+round(i/100*(a-r),o)}},{key:\"set\",value:function(t){e.enabled&&is$1.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(is$1.empty(t)||is$1.string(t)?s=Array.from(document.querySelectorAll(is$1.string(t)?t:'input[type=\"range\"]')):is$1.element(t)?s=[t]:is$1.nodeList(t)?s=Array.from(t):is$1.array(t)&&(s=t.filter(is$1.element)),is$1.empty(s))return null;var n=_objectSpread2({},defaults$1,{},i);if(is$1.string(t)&&n.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){is$1.element(i)&&matches$1(i,t)&&new e(i,n)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const getConstructor=e=>null!=e?e.constructor:null,instanceOf=(e,t)=>Boolean(e&&t&&e instanceof t),isNullOrUndefined=e=>null==e,isObject=e=>getConstructor(e)===Object,isNumber=e=>getConstructor(e)===Number&&!Number.isNaN(e),isString=e=>getConstructor(e)===String,isBoolean=e=>getConstructor(e)===Boolean,isFunction=e=>\"function\"==typeof e,isArray=e=>Array.isArray(e),isWeakMap=e=>instanceOf(e,WeakMap),isNodeList=e=>instanceOf(e,NodeList),isTextNode=e=>getConstructor(e)===Text,isEvent=e=>instanceOf(e,Event),isKeyboardEvent=e=>instanceOf(e,KeyboardEvent),isCue=e=>instanceOf(e,window.TextTrackCue)||instanceOf(e,window.VTTCue),isTrack=e=>instanceOf(e,TextTrack)||!isNullOrUndefined(e)&&isString(e.kind),isPromise=e=>instanceOf(e,Promise)&&isFunction(e.then),isElement=e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,isEmpty=e=>isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length,isUrl=e=>{if(instanceOf(e,window.URL))return!0;if(!isString(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!isEmpty(new URL(t).hostname)}catch(e){return!1}};var is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,weakMap:isWeakMap,nodeList:isNodeList,element:isElement,textNode:isTextNode,event:isEvent,keyboardEvent:isKeyboardEvent,cue:isCue,track:isTrack,promise:isPromise,url:isUrl,empty:isEmpty};const transitionEndEvent=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!is.string(i)&&t[i]})();function repaint(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}const isIE=Boolean(window.document.documentMode),isEdge=/Edge/g.test(navigator.userAgent),isWebKit=\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone=/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS=\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos=/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1;var browser={isIE:isIE,isEdge:isEdge,isWebKit:isWebKit,isIPhone:isIPhone,isIPadOS:isIPadOS,isIos:isIos};function cloneDeep(e){return JSON.parse(JSON.stringify(e))}function getDeep(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function extend(e={},...t){if(!t.length)return e;const i=t.shift();return is.object(i)?(Object.keys(i).forEach((t=>{is.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),extend(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),extend(e,...t)):e}function wrap(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,r=e.nextSibling;s.appendChild(e),r?n.insertBefore(s,r):n.appendChild(s)}))}function setAttributes(e,t){is.element(e)&&!is.empty(t)&&Object.entries(t).filter((([,e])=>!is.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function createElement(e,t,i){const s=document.createElement(e);return is.object(t)&&setAttributes(s,t),is.string(i)&&(s.innerText=i),s}function insertAfter(e,t){is.element(e)&&is.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)}function insertElement(e,t,i,s){is.element(t)&&t.appendChild(createElement(e,i,s))}function removeElement(e){is.nodeList(e)||is.array(e)?Array.from(e).forEach(removeElement):is.element(e)&&is.element(e.parentNode)&&e.parentNode.removeChild(e)}function emptyElement(e){if(!is.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function replaceElement(e,t){return is.element(t)&&is.element(t.parentNode)&&is.element(e)?(t.parentNode.replaceChild(e,t),e):null}function getAttributesFromSelector(e,t){if(!is.string(e)||is.empty(e))return{};const i={},s=extend({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),r=t.replace(/[[\\]]/g,\"\").split(\"=\"),[a]=r,o=r.length>1?r[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":is.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[a]=o}})),extend(s,i)}function toggleHidden(e,t){if(!is.element(e))return;let i=t;is.boolean(i)||(i=!e.hidden),e.hidden=i}function toggleClass(e,t,i){if(is.nodeList(e))return Array.from(e).map((e=>toggleClass(e,t,i)));if(is.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function hasClass(e,t){return is.element(e)&&e.classList.contains(t)}function matches(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function closest$1(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(matches.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}function getElements(e){return this.elements.container.querySelectorAll(e)}function getElement(e){return this.elements.container.querySelector(e)}function setFocus(e=null,t=!1){is.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const defaultCodecs={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},support={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=support[e]||\"html5\"!==t;return{api:i,ui:i&&support.rangeInput}},pip:!(browser.isIPhone||!is.function(createElement(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||createElement(\"video\").disablePictureInPicture)),airplay:is.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(is.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(defaultCodecs).includes(i)&&(i+=`; codecs=\"${defaultCodecs[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==transitionEndEvent,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},supportsPassiveListeners=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function toggleListener(e,t,i,s=!1,n=!0,r=!1){if(!e||!(\"addEventListener\"in e)||is.empty(t)||!is.function(i))return;const a=t.split(\" \");let o=r;supportsPassiveListeners&&(o={passive:n,capture:r}),a.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function on(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!0,s,n)}function off(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!1,s,n)}function once(e,t=\"\",i,s=!0,n=!1){const r=(...a)=>{off(e,t,r,s,n),i.apply(this,a)};toggleListener.call(this,e,t,r,!0,s,n)}function triggerEvent(e,t=\"\",i=!1,s={}){if(!is.element(e)||is.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function unbindListeners(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ready(){return new Promise((e=>this.ready?setTimeout(e,0):on.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function silencePromise(e){is.promise(e)&&e.then(null,(()=>{}))}function dedupe(e){return is.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function closest(e,t){return is.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function supportsCSS(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const standardRatios=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function validateAspectRatio(e){if(!(is.array(e)||is.string(e)&&e.includes(\":\")))return!1;return(is.array(e)?e:e.split(\":\")).map(Number).every(is.number)}function reduceAspectRatio(e){if(!is.array(e)||!e.every(is.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function getAspectRatio(e){const t=e=>validateAspectRatio(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!is.empty(this.embed)&&is.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return reduceAspectRatio(i)}function setAspectRatio(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=getAspectRatio.call(this,e);if(!is.array(i))return{};const[s,n]=reduceAspectRatio(i),r=100/s*n;if(supportsCSS(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${r}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-r)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:r,ratio:i}}function roundAspectRatio(e,t,i=.05){const s=e/t,n=closest(Object.keys(standardRatios),s);return Math.abs(n-s)<=i?standardRatios[n]:[e,t]}function getViewportSize(){return[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)]}const html5={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!is.empty(t)||support.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:html5.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,is.empty(this.config.ratio)||setAspectRatio.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=html5.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&is.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=html5.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:r,readyState:a,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==r||a)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||silencePromise(e.play())})),e.media.load())}triggerEvent.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(removeElement(html5.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function generateId(e){return`${e}-${Math.floor(1e4*Math.random())}`}function format(e,...t){return is.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}function getPercentage(e,t){return 0===e||0===t||Number.isNaN(e)||Number.isNaN(t)?0:(e/t*100).toFixed(2)}const replaceAll=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),toTitleCase=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function toPascalCase(e=\"\"){let t=e.toString();return t=replaceAll(t,\"-\",\" \"),t=replaceAll(t,\"_\",\" \"),t=toTitleCase(t),replaceAll(t,\" \",\"\")}function toCamelCase(e=\"\"){let t=e.toString();return t=toPascalCase(t),t.charAt(0).toLowerCase()+t.slice(1)}function stripHTML(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}function getHTML(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const resources={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},i18n={get(e=\"\",t={}){if(is.empty(e)||is.empty(t))return\"\";let i=getDeep(t.i18n,e);if(is.empty(i))return Object.keys(resources).includes(e)?resources[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=replaceAll(i,e,t)})),i}};class Storage{constructor(e){_defineProperty$1(this,\"get\",(e=>{if(!Storage.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(is.empty(t))return null;const i=JSON.parse(t);return is.string(e)&&e.length?i[e]:i})),_defineProperty$1(this,\"set\",(e=>{if(!Storage.supported||!this.enabled)return;if(!is.object(e))return;let t=this.get();is.empty(t)&&(t={}),extend(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function fetch(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function loadSprite(e,t){if(!is.string(e))return;const i=\"cache\",s=is.string(t);let n=!1;const r=()=>null!==document.getElementById(t),a=(e,t)=>{e.innerHTML=t,s&&r()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!r()){const r=Storage.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),r){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);a(o,t.content)}}fetch(e).then((e=>{if(!is.empty(e)){if(r)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}a(o,e)}})).catch((()=>{}))}}const getHours=e=>Math.trunc(e/60/60%60,10),getMinutes=e=>Math.trunc(e/60%60,10),getSeconds=e=>Math.trunc(e%60,10);function formatTime(e=0,t=!1,i=!1){if(!is.number(e))return formatTime(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=getHours(e);const r=getMinutes(e),a=getSeconds(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(r)}:${s(a)}`}const controls={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||browser.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=getElement.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:getElements.call(this,this.config.selectors.buttons.play),pause:getElement.call(this,this.config.selectors.buttons.pause),restart:getElement.call(this,this.config.selectors.buttons.restart),rewind:getElement.call(this,this.config.selectors.buttons.rewind),fastForward:getElement.call(this,this.config.selectors.buttons.fastForward),mute:getElement.call(this,this.config.selectors.buttons.mute),pip:getElement.call(this,this.config.selectors.buttons.pip),airplay:getElement.call(this,this.config.selectors.buttons.airplay),settings:getElement.call(this,this.config.selectors.buttons.settings),captions:getElement.call(this,this.config.selectors.buttons.captions),fullscreen:getElement.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=getElement.call(this,this.config.selectors.progress),this.elements.inputs={seek:getElement.call(this,this.config.selectors.inputs.seek),volume:getElement.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:getElement.call(this,this.config.selectors.display.buffer),currentTime:getElement.call(this,this.config.selectors.display.currentTime),duration:getElement.call(this,this.config.selectors.display.duration)},is.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=controls.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,r=document.createElementNS(i,\"svg\");setAttributes(r,extend(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const a=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in a&&a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),r.appendChild(a),r},createLabel(e,t={}){const i=i18n.get(e,this.config);return createElement(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(is.empty(e))return null;const t=createElement(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(createElement(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=extend({},t);let s=toCamelCase(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||extend(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:is.empty(n.label)&&(n.label=s),is.empty(n.icon)&&(n.icon=e)}const r=createElement(n.element);return n.toggle?(r.appendChild(controls.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),r.appendChild(controls.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),r.appendChild(controls.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),r.appendChild(controls.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(r.appendChild(controls.createIcon.call(this,n.icon)),r.appendChild(controls.createLabel.call(this,n.label))),extend(i,getAttributesFromSelector(this.config.selectors.buttons[s],i)),setAttributes(r,i),\"play\"===s?(is.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(r)):this.elements.buttons[s]=r,r},createRange(e,t){const i=createElement(\"input\",extend(getAttributesFromSelector(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":i18n.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,controls.updateRangeFill.call(this,i),RangeTouch.setup(i),i},createProgress(e,t){const i=createElement(\"progress\",extend(getAttributesFromSelector(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(createElement(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?i18n.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=getAttributesFromSelector(this.config.selectors.display[e],t),s=createElement(\"div\",extend(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":i18n.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){on.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=matches(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))controls.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,is.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,is.element(t)||(t=e.parentNode.lastElementChild)),setFocus.call(this,t,!0))}}),!1),on.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&controls.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:r=!1}){const a=getAttributesFromSelector(this.config.selectors.inputs[i]),o=createElement(\"button\",extend(a,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${a.class?a.class:\"\"}`.trim(),\"aria-checked\":r,value:e})),l=createElement(\"span\");l.innerHTML=s,is.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>matches(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!is.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}controls.showMenuPanel.call(this,\"home\",is.keyboardEvent(t))}}),i,!1),controls.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!is.number(e))return e;return formatTime(e,getHours(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){is.element(e)&&is.number(t)&&(e.innerText=controls.formatTime(t,i))},updateVolume(){this.supported.ui&&(is.element(this.elements.inputs.volume)&&controls.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),is.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){is.element(e)&&(e.value=t,controls.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!is.event(e))return;let t=0;const i=(e,t)=>{const i=is.number(t)?t:0,s=is.element(e)?e:this.elements.display.buffer;if(is.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];is.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":t=getPercentage(this.currentTime,this.duration),\"timeupdate\"===e.type&&controls.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}},updateRangeFill(e){const t=is.event(e)?e.target:e;if(is.element(t)&&\"range\"===t.getAttribute(\"type\")){if(matches(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=controls.formatTime(this.currentTime),i=controls.formatTime(this.duration),s=i18n.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(matches(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(browser.isWebKit||browser.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!is.element(this.elements.inputs.seek)||!is.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,r=e=>toggleClass(s,n,e);if(this.touch)return void r(!1);let a=0;const o=this.elements.progress.getBoundingClientRect();if(is.event(e))a=100/o.width*(e.pageX-o.left);else{if(!hasClass(s,n))return;a=parseFloat(s.style.left,10)}a<0?a=0:a>100&&(a=100);const l=this.duration/100*a;s.innerText=controls.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${a}%`,is.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&r(\"mouseenter\"===e.type)},timeUpdate(e){const t=!is.element(this.elements.display.duration)&&this.config.invertTime;controls.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||controls.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return toggleHidden(this.elements.display.currentTime,!0),void toggleHidden(this.elements.progress,!0);is.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=is.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&controls.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&controls.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&controls.setMarkers.call(this),controls.updateSeekTooltip.call(this)},toggleMenuButton(e,t){toggleHidden(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,r=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=is.empty(i)?this[e]:i,is.empty(n)&&(n=this.config[e].default),!is.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(is.element(r)||(r=s&&s.querySelector('[role=\"menu\"]')),!is.element(r))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=controls.getLabel.call(this,e,n);const a=r&&r.querySelector(`[value=\"${n}\"]`);is.element(a)&&(a.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?i18n.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(is.number(t)){const e=i18n.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return toTitleCase(t);case\"captions\":return captions.getLabel.call(this);default:return null}},setQualityMenu(e){if(!is.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');is.array(e)&&(this.options.quality=dedupe(e).filter((e=>this.config.quality.options.includes(e))));const s=!is.empty(this.options.quality)&&this.options.quality.length>1;if(controls.toggleMenuButton.call(this,t,s),emptyElement(i),controls.checkMenu.call(this),!s)return;const n=e=>{const t=i18n.get(`qualityBadge.${e}`,this.config);return t.length?controls.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{controls.createMenuItem.call(this,{value:e,list:i,type:t,title:controls.getLabel.call(this,\"quality\",e),badge:n(e)})})),controls.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!is.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=captions.getTracks.call(this),s=Boolean(i.length);if(controls.toggleMenuButton.call(this,e,s),emptyElement(t),controls.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:captions.getLabel.call(this,e),badge:e.language&&controls.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:i18n.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(controls.createMenuItem.bind(this)),controls.updateSetting.call(this,e,t)},setSpeedMenu(){if(!is.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!is.empty(this.options.speed)&&this.options.speed.length>1;controls.toggleMenuButton.call(this,e,i),emptyElement(t),controls.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{controls.createMenuItem.call(this,{value:i,list:t,type:e,title:controls.getLabel.call(this,\"speed\",i)})})),controls.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!is.empty(e)&&Object.values(e).some((e=>!e.hidden));toggleHidden(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;is.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');setFocus.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!is.element(t)||!is.element(i))return;const{hidden:s}=t;let n=s;if(is.boolean(e))n=e;else if(is.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(is.event(e)){const s=is.function(e.composedPath)?e.composedPath()[0]:e.target,r=t.contains(s);if(r||!r&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),toggleHidden(t,!n),toggleClass(this.elements.container,this.config.classNames.menu.open,n),n&&is.keyboardEvent(e)?controls.focusFirstMenuItem.call(this,null,!0):n||s||setFocus.call(this,i,is.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return removeElement(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!is.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(support.transitions&&!support.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=controls.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",off.call(this,s,transitionEndEvent,t))};on.call(this,s,transitionEndEvent,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}toggleHidden(n,!0),toggleHidden(i,!1),controls.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;is.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:r,setQualityMenu:a,setSpeedMenu:o,showMenuPanel:l}=controls;this.elements.controls=null,is.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=createElement(\"div\",getAttributesFromSelector(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return dedupe(is.array(this.config.controls)?this.config.controls:[]).forEach((a=>{if(\"restart\"===a&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===a&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===a&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===a&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===a){const t=createElement(\"div\",{class:`${u.class} plyr__progress__container`}),i=createElement(\"div\",getAttributesFromSelector(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=createElement(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===a&&c.appendChild(r.call(this,\"currentTime\",u)),\"duration\"===a&&c.appendChild(r.call(this,\"duration\",u)),\"mute\"===a||\"volume\"===a){let{volume:t}=this.elements;if(is.element(t)&&c.contains(t)||(t=createElement(\"div\",extend({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===a&&t.appendChild(i.call(this,\"mute\")),\"volume\"===a&&!browser.isIos&&!browser.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",extend(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===a&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===a&&!is.empty(this.config.settings)){const s=createElement(\"div\",extend({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=createElement(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),r=createElement(\"div\"),a=createElement(\"div\",{id:`plyr-settings-${e.id}-home`}),o=createElement(\"div\",{role:\"menu\"});a.appendChild(o),r.appendChild(a),this.elements.settings.panels.home=a,this.config.settings.forEach((i=>{const s=createElement(\"button\",extend(getAttributesFromSelector(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),on.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=createElement(\"span\",null,i18n.get(i,this.config)),a=createElement(\"span\",{class:this.config.classNames.menu.value});a.innerHTML=e[i],n.appendChild(a),s.appendChild(n),o.appendChild(s);const c=createElement(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=createElement(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(createElement(\"span\",{\"aria-hidden\":!0},i18n.get(i,this.config))),u.appendChild(createElement(\"span\",{class:this.config.classNames.hidden},i18n.get(\"menuBack\",this.config))),on.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),on.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(createElement(\"div\",{role:\"menu\"})),r.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(r),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===a&&support.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===a&&support.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===a){const e=extend({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!is.url(t)&&this.isEmbed&&extend(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===a&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&a.call(this,html5.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=controls.getIconUrl.call(this);e.cors&&loadSprite(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;is.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),is.element(this.config.controls)||is.string(this.config.controls)?e=this.config.controls:(e=controls.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:captions.getLabel.call(this)}),i=!1);let s;i&&is.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=replaceAll(i,`{${e}}`,t)})),i})(e)),is.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),is.element(s)||(s=this.elements.container);if(s[is.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),is.element(this.elements.controls)||controls.findElements.call(this),!is.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>hasClass(e,t),set(i=!1){toggleClass(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{is.array(t)||is.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(browser.isEdge&&repaint(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=getElements.call(this,i);Array.from(s).forEach((e=>{toggleClass(e,this.config.classNames.hidden,!1),toggleClass(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let r=null;const a=`${this.config.classNames.tooltip}--visible`,o=e=>toggleClass(r,a,e);i.forEach((e=>{const t=createElement(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";r&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(r.style.left=i,r.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(r=createElement(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(r)),this.elements.markers={points:n,tip:r},this.elements.progress.appendChild(s)}};function parseUrl(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function buildUrlParams(e){const t=new URLSearchParams;return is.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const captions={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!support.textTracks)return void(is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this));if(is.element(this.elements.captions)||(this.elements.captions=createElement(\"div\",getAttributesFromSelector(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),insertAfter(this.elements.captions,this.elements.wrapper)),browser.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=parseUrl(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&fetch(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{removeElement(e)}))}))}const e=dedupe((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let t=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===t&&([t]=e);let i=this.storage.get(\"captions\");if(is.boolean(i)||({active:i}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:i,language:t,languages:e}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";on.call(this,this.media.textTracks,e,captions.update.bind(this))}setTimeout(captions.update.bind(this),0)},update(){const e=captions.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,r=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),on.call(this,e,\"cuechange\",(()=>captions.updateCues.call(this)))})),(r&&this.language!==i||!e.includes(n))&&(captions.setLanguage.call(this,i),captions.toggle.call(this,t&&r)),this.elements&&toggleClass(this.elements.container,this.config.classNames.captions.enabled,!is.empty(e)),is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=is.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=captions.getTracks.call(this),t=captions.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void captions.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),toggleClass(this.elements.container,s,n),this.captions.toggled=n,controls.updateSetting.call(this,\"captions\"),triggerEvent.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=captions.getTracks.call(this);if(-1!==e)if(is.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,controls.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),triggerEvent.call(this,this.media,\"languagechange\")}captions.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&captions.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else captions.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!is.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=captions.getTracks.call(this),n=captions.findTrack.call(this,[i]);captions.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=captions.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let r;return e.every((e=>(r=n.find((t=>t.language===e)),!r))),r||(t?n[0]:void 0)},getCurrentTrack(){return captions.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!is.track(t)&&support.textTracks&&this.captions.toggled&&(t=captions.getCurrentTrack.call(this)),is.track(t)?is.empty(t.label)?is.empty(t.language)?i18n.get(\"enabled\",this.config):e.language.toUpperCase():t.label:i18n.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!is.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!is.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=captions.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(getHTML)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){emptyElement(this.elements.captions);const e=createElement(\"span\",getAttributesFromSelector(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),triggerEvent.call(this,this.media,\"cuechange\")}}},defaults={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:{download:null,vimeo:{sdk:\"https://player.vimeo.com/api/player.js\",iframe:\"https://player.vimeo.com/video/{0}?{1}\",api:\"https://vimeo.com/api/oembed.json?url={0}\"},youtube:{sdk:\"https://www.youtube.com/iframe_api\",api:\"https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}\"},googleIMA:{sdk:\"https://imasdk.googleapis.com/js/sdkloader/ima3.js\"}},listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},pip={active:\"picture-in-picture\",inactive:\"inline\"},providers={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},types={audio:\"audio\",video:\"video\"};function getProviderByUrl(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?providers.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?providers.vimeo:null}const noop=()=>{};class Console{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):noop}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):noop}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):noop}}class Fullscreen{constructor(e){_defineProperty$1(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;is.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;triggerEvent.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),_defineProperty$1(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",toggleClass(this.target,this.player.config.classNames.fullscreen.fallback,e),browser.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=is.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),_defineProperty$1(this,\"trapFocus\",(e=>{if(browser.isIos||browser.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=getElements.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),_defineProperty$1(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":Fullscreen.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");toggleClass(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),_defineProperty$1(this,\"enter\",(()=>{this.supported&&(browser.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!Fullscreen.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?is.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),_defineProperty$1(this,\"exit\",(()=>{if(this.supported)if(browser.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),silencePromise(this.player.play());else if(!Fullscreen.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!is.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),_defineProperty$1(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=Fullscreen.prefix,this.property=Fullscreen.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&closest$1(this.player.elements.container,e.config.fullscreen.container),on.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),on.call(this.player,this.player.elements.container,\"dblclick\",(e=>{is.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),on.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return Fullscreen.nativeSupported&&!this.forceFallback}static get prefix(){if(is.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!is.function(document[`${t}ExitFullscreen`])&&!is.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,Fullscreen.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||Fullscreen.nativeSupported||!browser.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!Fullscreen.nativeSupported||this.forceFallback)return hasClass(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return browser.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function loadImage(e,t=1){return new Promise(((i,s)=>{const n=new Image,r=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:r,onerror:r,src:e})}))}const ui={addStyleHook(){toggleClass(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),toggleClass(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void ui.toggleNativeControls.call(this,!0);is.element(this.elements.controls)||(controls.inject.call(this),this.listeners.controls()),ui.toggleNativeControls.call(this),this.isHTML5&&captions.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,controls.updateVolume.call(this),controls.timeUpdate.call(this),controls.durationUpdate.call(this),ui.checkPlaying.call(this),toggleClass(this.elements.container,this.config.classNames.pip.supported,support.pip&&this.isHTML5&&this.isVideo),toggleClass(this.elements.container,this.config.classNames.airplay.supported,support.airplay&&this.isHTML5),toggleClass(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{triggerEvent.call(this,this.media,\"ready\")}),0),ui.setTitle.call(this),this.poster&&ui.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&controls.durationUpdate.call(this),this.config.mediaMetadata&&controls.setMediaMetadata.call(this)},setTitle(){let e=i18n.get(\"play\",this.config);if(is.string(this.config.title)&&!is.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=getElement.call(this,\"iframe\");if(!is.element(e))return;const t=is.empty(this.config.title)?\"video\":this.config.title,i=i18n.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){toggleClass(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ready.call(this).then((()=>loadImage(e))).catch((t=>{throw e===this.poster&&ui.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),ui.togglePoster.call(this,!0),e))))},checkPlaying(e){toggleClass(this.elements.container,this.config.classNames.playing,this.playing),toggleClass(this.elements.container,this.config.classNames.paused,this.paused),toggleClass(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",i18n.get(this.playing?\"pause\":\"play\",this.config))})),is.event(e)&&\"timeupdate\"===e.type||ui.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{toggleClass(this.elements.container,this.config.classNames.loading,this.loading),ui.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!is.empty(e)&&is.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),is.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Listeners{constructor(e){_defineProperty$1(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,toggleClass(t.container,e.config.classNames.isTouch,!0)})),_defineProperty$1(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&toggleListener.call(t,window,\"keydown keyup\",this.handleKey,e,!1),toggleListener.call(t,document.body,\"click\",this.toggleMenu,e),once.call(t,document.body,\"touchstart\",this.firstTouch)})),_defineProperty$1(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&on.call(e,i.container,\"keydown keyup\",this.handleKey,!1),on.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let r=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(ui.toggleControls.call(e,!0),r=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),r)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,r]=getAspectRatio.call(e),a=supportsCSS(`aspect-ratio: ${n} / ${r}`);if(!s)return void(a?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=getViewportSize(),c=o/l>n/r;a?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/r*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},r=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};on.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&is.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?on:off).call(e,window,\"resize\",r)}))})),_defineProperty$1(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(on.call(e,e.media,\"timeupdate seeking seeked\",(t=>controls.timeUpdate.call(e,t))),on.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>controls.durationUpdate.call(e,t))),on.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),on.call(e,e.media,\"progress playing seeking seeked\",(t=>controls.updateProgress.call(e,t))),on.call(e,e.media,\"volumechange\",(t=>controls.updateVolume.call(e,t))),on.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>ui.checkPlaying.call(e,t))),on.call(e,e.media,\"waiting canplay seeked playing\",(t=>ui.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=getElement.call(e,`.${e.config.classNames.video}`);if(!is.element(i))return;on.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{silencePromise(e.play())}),\"play\")):this.proxy(s,(()=>{silencePromise(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&on.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),on.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),on.call(e,e.media,\"ratechange\",(()=>{controls.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),on.call(e,e.media,\"qualitychange\",(t=>{controls.updateSetting.call(e,\"quality\",null,t.detail.quality)})),on.call(e,e.media,\"ready qualitychange\",(()=>{controls.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");on.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),triggerEvent.call(e,t.container,i.type,!0,s)}))})),_defineProperty$1(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let r=!0;is.function(n)&&(r=n.call(s,e)),!1!==r&&is.function(t)&&t.call(s,e)})),_defineProperty$1(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:r}=this,a=r.config.listeners[s],o=is.function(a);on.call(r,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),_defineProperty$1(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=browser.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{silencePromise(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{triggerEvent.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),controls.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),controls.toggleMenu.call(e,t)):controls.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&controls.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(is.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),r=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&r?(i.removeAttribute(s),silencePromise(e.play())):!r&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),browser.isIos){const t=getElements.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>repaint(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");is.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>controls.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),browser.isWebKit&&Array.from(getElements.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>controls.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!is.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,controls.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;toggleClass(t.controls,i.classNames.noTransition,!0),ui.toggleControls.call(e,!0),setTimeout((()=>{toggleClass(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),r=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(r/50);const{volume:a}=e.media;(1===r&&a<1||-1===r&&a>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:r,ctrlKey:a,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(r||a||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(is.element(n)){const{editable:s}=t.config.selectors,{seek:r}=i.inputs;if(n!==r&&matches(n,s))return;if(\" \"===e.key&&matches(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(d=parseInt(s,10),t.currentTime=t.duration/10*d);break;case\" \":case\"k\":u||silencePromise(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var d}toggleMenu(e){controls.toggleMenu.call(this.player,e)}}var commonjsGlobal=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}var loadjs_umd=createCommonjsModule((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,r,a,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)r=e[l],(a=i[r])?n(r,a):(s[r]=s[r]||[]).push(n)}function r(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function a(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var r,a,l=document,c=s.async,u=(s.numRetries||0)+1,d=s.before||e,h=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(h)?((a=l.createElement(\"link\")).rel=\"stylesheet\",a.href=m,(r=\"hideFocus\"in a)&&a.relList&&(r=0,a.rel=\"preload\",a.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(h)?(a=l.createElement(\"img\")).src=m:((a=l.createElement(\"script\")).src=t,a.async=void 0===c||c),a.onload=a.onerror=a.onbeforeload=function(e){var l=e.type[0];if(r)try{a.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==a.rel&&\"style\"==a.as)return a.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==d(t,a)&&l.head.appendChild(a)}function l(e,t,i){var s,n,r=(e=e.push?e:[e]).length,a=r,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--r||t(l)},n=0;n<a;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){a(o,e),t&&a({success:t,error:i},e),r(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){a(t,e)})),c},c.done=function(e){r(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function loadScript(e){return new Promise(((t,i)=>{loadjs_umd(e,{success:t,error:i})}))}function parseId$1(e){if(is.empty(e))return null;if(is.number(Number(e)))return e;return e.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:e}function parseHash(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}function assurePlaybackState$1(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}const vimeo={setup(){const e=this;toggleClass(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,setAspectRatio.call(e),is.object(window.Vimeo)?vimeo.ready.call(e):loadScript(e.config.urls.vimeo.sdk).then((()=>{vimeo.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let r=e.media.getAttribute(\"src\"),a=\"\";is.empty(r)?(r=e.media.getAttribute(e.config.attributes.embed.id),a=e.media.getAttribute(e.config.attributes.embed.hash)):a=parseHash(r);const o=a?{h:a}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=buildUrlParams({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=parseId$1(r),u=createElement(\"iframe\"),d=format(e.config.urls.vimeo.iframe,c,l);if(u.setAttribute(\"src\",d),u.setAttribute(\"allowfullscreen\",\"\"),u.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),is.empty(s)||u.setAttribute(\"referrerPolicy\",s),i||!t.customControls)u.setAttribute(\"data-poster\",e.poster),e.media=replaceElement(u,e.media);else{const t=createElement(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(u),e.media=replaceElement(t,e.media)}t.customControls||fetch(format(e.config.urls.vimeo.api,d)).then((t=>{!is.empty(t)&&t.thumbnail_url&&ui.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(u,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(assurePlaybackState$1.call(e,!0),e.embed.play()),e.media.pause=()=>(assurePlaybackState$1.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:h}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>h,set(t){const{embed:i,media:s,paused:n,volume:r}=e,a=n&&!i.hasPlayed;s.seeking=!0,triggerEvent.call(e,s,\"seeking\"),Promise.resolve(a&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>a&&i.pause())).then((()=>a&&i.setVolume(r))).catch((()=>{}))}});let m=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>m,set(t){e.embed.setPlaybackRate(t).then((()=>{m=t,triggerEvent.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:p}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>p,set(t){e.embed.setVolume(t).then((()=>{p=t,triggerEvent.call(e,e.media,\"volumechange\")}))}});let{muted:g}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>g,set(t){const i=!!is.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{g=i,triggerEvent.call(e,e.media,\"volumechange\")}))}});let f,{loop:y}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>y,set(t){const i=is.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{y=i}))}}),e.embed.getVideoUrl().then((t=>{f=t,controls.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>f}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=roundAspectRatio(i,s),setAspectRatio.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,ui.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{h=t,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,captions.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>stripHTML(e.text)));captions.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{assurePlaybackState$1.call(e,!t),t||triggerEvent.call(e,e.media,\"playing\")})),is.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{triggerEvent.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{assurePlaybackState$1.call(e,!0),triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{assurePlaybackState$1.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,h=t.seconds,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,triggerEvent.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&triggerEvent.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,triggerEvent.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,triggerEvent.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>ui.build.call(e)),0)}};function parseId(e){if(is.empty(e))return null;return e.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:e}function assurePlaybackState(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}function getHost(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const youtube={setup(){if(toggleClass(this.elements.wrapper,this.config.classNames.embed,!0),is.object(window.YT)&&is.function(window.YT.Player))youtube.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{is.function(e)&&e(),youtube.ready.call(this)},loadScript(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){fetch(format(this.config.urls.youtube.api,e)).then((e=>{if(is.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,ui.setTitle.call(this),this.embed.ratio=roundAspectRatio(s,i)}setAspectRatio.call(this)})).catch((()=>{setAspectRatio.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!is.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");is.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=parseId(s),r=createElement(\"div\",{id:generateId(e.provider),\"data-poster\":t.customControls?e.poster:void 0});if(e.media=replaceElement(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;loadImage(t(\"maxres\"),121).catch((()=>loadImage(t(\"sd\"),121))).catch((()=>loadImage(t(\"hq\")))).then((t=>ui.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:getHost(t),playerVars:extend({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},triggerEvent.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),triggerEvent.call(e,e.media,\"ratechange\")},onReady(i){if(is.function(e.media.play))return;const s=i.target;youtube.getTitle.call(e,n),e.media.play=()=>{assurePlaybackState.call(e,!0),s.playVideo()},e.media.pause=()=>{assurePlaybackState.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,triggerEvent.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:r}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>r,set(t){r=t,s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}});let{muted:a}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>a,set(t){const i=is.boolean(t)?t:a;a=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),triggerEvent.call(e,e.media,\"timeupdate\"),triggerEvent.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&triggerEvent.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),triggerEvent.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>ui.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")),i.data){case-1:triggerEvent.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),triggerEvent.call(e,e.media,\"progress\");break;case 0:assurePlaybackState.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):triggerEvent.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(assurePlaybackState.call(e,!0),triggerEvent.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{triggerEvent.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),triggerEvent.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),assurePlaybackState.call(e,!1);break;case 3:triggerEvent.call(e,e.media,\"waiting\")}triggerEvent.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},media={setup(){this.media?(toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),toggleClass(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=createElement(\"div\",{class:this.config.classNames.video}),wrap(this.media,this.elements.wrapper),this.elements.poster=createElement(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?html5.setup.call(this):this.isYouTube?youtube.setup.call(this):this.isVimeo&&vimeo.setup.call(this)):this.debug.warn(\"No media element found!\")}},destroy=e=>{e.manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()};class Ads{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.enabled&&(is.object(window.google)&&is.object(window.google.ima)?this.ready():loadScript(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),_defineProperty$1(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),_defineProperty$1(this,\"setupIMA\",(()=>{this.elements.container=createElement(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),_defineProperty$1(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),_defineProperty$1(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=formatTime(Math.max(this.manager.getRemainingTime(),0)),t=`${i18n.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),_defineProperty$1(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),_defineProperty$1(this,\"addCuePoints\",(()=>{is.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(is.element(t)){const i=100/this.player.duration*e,s=createElement(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),_defineProperty$1(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{triggerEvent.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),_defineProperty$1(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),_defineProperty$1(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;is.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),_defineProperty$1(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),_defineProperty$1(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,silencePromise(this.player.media.play())})),_defineProperty$1(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),_defineProperty$1(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),_defineProperty$1(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),_defineProperty$1(this,\"trigger\",((e,...t)=>{const i=this.events[e];is.array(i)&&i.forEach((e=>{is.function(e)&&e.apply(this,t)}))})),_defineProperty$1(this,\"on\",((e,t)=>(is.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),_defineProperty$1(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),_defineProperty$1(this,\"clearSafetyTimer\",(e=>{is.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!is.empty(e.publisherId)||is.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(is.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${buildUrlParams({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function clamp(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const parseVtt=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(is.number(i.startTime)){if(!is.empty(e.trim())&&is.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},fitRatio=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class PreviewThumbnails{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),_defineProperty$1(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(is.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(is.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(is.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),_defineProperty$1(this,\"getThumbnail\",(e=>new Promise((t=>{fetch(e).then((i=>{const s={frames:parseVtt(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),_defineProperty$1(this,\"startMove\",(e=>{if(this.loaded&&is.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=formatTime(this.seekTime);const r=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));r&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${r.label}<br>`)}this.showImageAtCurrentTime()}})),_defineProperty$1(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),_defineProperty$1(this,\"startScrubbing\",(e=>{(is.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),_defineProperty$1(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):once.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),_defineProperty$1(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),_defineProperty$1(this,\"render\",(()=>{this.elements.thumb.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=createElement(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),is.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),_defineProperty$1(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),_defineProperty$1(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),_defineProperty$1(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],r=i.frames[t].text,a=s+r;if(this.currentImageElement&&this.currentImageElement.dataset.filename===r)this.showImage(this.currentImageElement,n,e,t,r,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=a,i.dataset.index=t,i.dataset.filename=r,this.showingThumbFilename=r,this.player.debug.log(`Loading image: ${a}`),i.onload=()=>this.showImage(i,n,e,t,r,!0),this.loadingImage=i,this.removeOldImages(i)}})),_defineProperty$1(this,\"showImage\",((e,t,i,s,n,r=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${r}`),this.setImageSizeAndOffset(e,t),r&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),_defineProperty$1(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),_defineProperty$1(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let r=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){r=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),r||i()}}),300)})))),_defineProperty$1(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),_defineProperty$1(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),_defineProperty$1(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),_defineProperty$1(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,r=this.mousePosX-e.left-i.clientWidth/2,a=clamp(r,s,n);i.style.left=`${a}px`,i.style.setProperty(\"--preview-arrow-offset\",r-a+\"px\")})),_defineProperty$1(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),_defineProperty$1(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const source={insertElements(e,t){is.string(t)?insertElement(e,this.media,{src:t}):is.array(t)&&t.forEach((t=>{insertElement(e,this.media,t)}))},change(e){getDeep(e,\"sources.length\")?(html5.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],removeElement(this.media),this.media=null,is.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=providers.html5,src:n}]=t,r=\"html5\"===s?i:\"div\",a=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:support.check(i,s,this.config.playsinline),media:createElement(r,a)}),this.elements.container.appendChild(this.media),is.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),is.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),ui.addStyleHook.call(this),this.isHTML5&&source.insertElements.call(this,\"source\",t),this.config.title=e.title,media.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&source.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.isHTML5&&this.media.load(),is.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class Plyr{constructor(e,t){if(_defineProperty$1(this,\"play\",(()=>is.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>silencePromise(this.media.play()))),this.media.play()):null)),_defineProperty$1(this,\"pause\",(()=>this.playing&&is.function(this.media.pause)?this.media.pause():null)),_defineProperty$1(this,\"togglePlay\",(e=>(is.boolean(e)?e:!this.playing)?this.play():this.pause())),_defineProperty$1(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):is.function(this.media.stop)&&this.media.stop()})),_defineProperty$1(this,\"restart\",(()=>{this.currentTime=0})),_defineProperty$1(this,\"rewind\",(e=>{this.currentTime-=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"forward\",(e=>{this.currentTime+=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(is.number(e)?e:0)})),_defineProperty$1(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),_defineProperty$1(this,\"airplay\",(()=>{support.airplay&&this.media.webkitShowPlaybackTargetPicker()})),_defineProperty$1(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=hasClass(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=toggleClass(this.elements.container,this.config.classNames.hideControls,i);if(s&&is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!is.empty(this.config.settings)&&controls.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";triggerEvent.call(this,this.media,e)}return!s}return!1})),_defineProperty$1(this,\"on\",((e,t)=>{on.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"once\",((e,t)=>{once.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"off\",((e,t)=>{off(this.elements.container,e,t)})),_defineProperty$1(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(removeElement(this.elements.buttons.play),removeElement(this.elements.captions),removeElement(this.elements.controls),removeElement(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),is.function(e)&&e()):(unbindListeners.call(this),html5.cancelRequests.call(this),replaceElement(this.elements.original,this.elements.container),triggerEvent.call(this,this.elements.original,\"destroyed\",!0),is.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(ui.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&is.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),_defineProperty$1(this,\"supports\",(e=>support.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=support.touch,this.media=e,is.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||is.nodeList(this.media)||is.array(this.media))&&(this.media=this.media[0]),this.config=extend({},defaults,Plyr.defaults,t||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new Console(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",support),is.nullOrUndefined(this.media)||!is.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!support.check().api)return void this.debug.error(\"Setup failed: no support\");const i=this.media.cloneNode(!0);i.autoplay=!1,this.elements.original=i;const s=this.media.tagName.toLowerCase();let n=null,r=null;switch(s){case\"div\":if(n=this.media.querySelector(\"iframe\"),is.element(n)){if(r=parseUrl(n.getAttribute(\"src\")),this.provider=getProviderByUrl(r.toString()),this.elements.container=this.media,this.media=n,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(is.empty(this.provider)||!Object.values(providers).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=types.video;break;case\"video\":case\"audio\":this.type=s,this.provider=providers.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=support.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Listeners(this),this.storage=new Storage(this),this.media.plyr=this,is.element(this.elements.container)||(this.elements.container=createElement(\"div\"),wrap(this.media,this.elements.container)),ui.migrateStyles.call(this),ui.addStyleHook.call(this),media.setup.call(this),this.config.debug&&on.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new Fullscreen(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ads(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>silencePromise(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===providers.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===providers.youtube}get isVimeo(){return this.provider===providers.vimeo}get isVideo(){return this.type===types.video}get isAudio(){return this.type===types.audio}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=is.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return is.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=is.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;is.string(t)&&(t=Number(t)),is.number(t)||(t=this.storage.get(\"volume\")),is.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!is.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;is.boolean(t)||(t=this.storage.get(\"muted\")),is.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;is.number(e)&&(t=e),is.number(t)||(t=this.storage.get(\"speed\")),is.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=clamp(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!is.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(is.number),n=!0;if(!i.includes(s)){const e=closest(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=is.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){source.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return is.url(e)?e:this.source}set download(e){is.url(e)&&(this.config.urls.download=e,controls.setDownloadUrl.call(this))}set poster(e){this.isVideo?ui.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=reduceAspectRatio(getAspectRatio.call(this));return is.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?is.string(e)&&validateAspectRatio(e)?(this.config.ratio=reduceAspectRatio(e),setAspectRatio.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=is.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){captions.toggle.call(this,e,!1)}set currentTrack(e){captions.set.call(this,e,!1),captions.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){captions.setLanguage.call(this,e,!1)}get language(){return(captions.getCurrentTrack.call(this)||{}).language}set pip(e){if(!support.pip)return;const t=is.boolean(e)?e:!this.pip;is.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?pip.active:pip.inactive),is.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return support.pip?is.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===pip.active:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))}static supported(e,t){return support.check(e,t)}static loadSprite(e,t){return loadSprite(e,t)}static setup(e,t={}){let i=null;return is.string(e)?i=Array.from(document.querySelectorAll(e)):is.nodeList(e)?i=Array.from(e):is.array(e)&&(i=e.filter(is.element)),is.empty(i)?null:i.map((e=>new Plyr(e,t)))}}Plyr.defaults=cloneDeep(defaults);export{Plyr as default};\n\\ No newline at end of file\n+function _defineProperty$1(e,t,i){return(t=_toPropertyKey(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function _toPrimitive(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}function _toPropertyKey(e){var t=_toPrimitive(e,\"string\");return\"symbol\"==typeof t?t:String(t)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function _defineProperties(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,i){return t&&_defineProperties(e.prototype,t),i&&_defineProperties(e,i),e}function _defineProperty(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(i),!0).forEach((function(t){_defineProperty(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}var defaults$1={addCSS:!0,thumbWidth:15,watch:!0};function matches$1(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}var getConstructor$1=function(e){return null!=e?e.constructor:null},instanceOf$1=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined$1=function(e){return null==e},isObject$1=function(e){return getConstructor$1(e)===Object},isNumber$1=function(e){return getConstructor$1(e)===Number&&!Number.isNaN(e)},isString$1=function(e){return getConstructor$1(e)===String},isBoolean$1=function(e){return getConstructor$1(e)===Boolean},isFunction$1=function(e){return getConstructor$1(e)===Function},isArray$1=function(e){return Array.isArray(e)},isNodeList$1=function(e){return instanceOf$1(e,NodeList)},isElement$1=function(e){return instanceOf$1(e,Element)},isEvent$1=function(e){return instanceOf$1(e,Event)},isEmpty$1=function(e){return isNullOrUndefined$1(e)||(isString$1(e)||isArray$1(e)||isNodeList$1(e))&&!e.length||isObject$1(e)&&!Object.keys(e).length},is$1={nullOrUndefined:isNullOrUndefined$1,object:isObject$1,number:isNumber$1,string:isString$1,boolean:isBoolean$1,function:isFunction$1,array:isArray$1,nodeList:isNodeList$1,element:isElement$1,event:isEvent$1,empty:isEmpty$1};function getDecimalPlaces(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var i=getDecimalPlaces(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,i){_classCallCheck(this,e),is$1.element(t)?this.element=t:is$1.string(t)&&(this.element=document.querySelector(t)),is$1.element(this.element)&&is$1.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults$1,{},i),this.init())}return _createClass(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!is$1.event(t))return null;var i,s=t.target,n=t.changedTouches[0],r=parseFloat(s.getAttribute(\"min\"))||0,a=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),r+round(i/100*(a-r),o)}},{key:\"set\",value:function(t){e.enabled&&is$1.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(is$1.empty(t)||is$1.string(t)?s=Array.from(document.querySelectorAll(is$1.string(t)?t:'input[type=\"range\"]')):is$1.element(t)?s=[t]:is$1.nodeList(t)?s=Array.from(t):is$1.array(t)&&(s=t.filter(is$1.element)),is$1.empty(s))return null;var n=_objectSpread2({},defaults$1,{},i);if(is$1.string(t)&&n.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){is$1.element(i)&&matches$1(i,t)&&new e(i,n)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const getConstructor=e=>null!=e?e.constructor:null,instanceOf=(e,t)=>Boolean(e&&t&&e instanceof t),isNullOrUndefined=e=>null==e,isObject=e=>getConstructor(e)===Object,isNumber=e=>getConstructor(e)===Number&&!Number.isNaN(e),isString=e=>getConstructor(e)===String,isBoolean=e=>getConstructor(e)===Boolean,isFunction=e=>\"function\"==typeof e,isArray=e=>Array.isArray(e),isWeakMap=e=>instanceOf(e,WeakMap),isNodeList=e=>instanceOf(e,NodeList),isTextNode=e=>getConstructor(e)===Text,isEvent=e=>instanceOf(e,Event),isKeyboardEvent=e=>instanceOf(e,KeyboardEvent),isCue=e=>instanceOf(e,window.TextTrackCue)||instanceOf(e,window.VTTCue),isTrack=e=>instanceOf(e,TextTrack)||!isNullOrUndefined(e)&&isString(e.kind),isPromise=e=>instanceOf(e,Promise)&&isFunction(e.then),isElement=e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,isEmpty=e=>isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length,isUrl=e=>{if(instanceOf(e,window.URL))return!0;if(!isString(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!isEmpty(new URL(t).hostname)}catch(e){return!1}};var is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,weakMap:isWeakMap,nodeList:isNodeList,element:isElement,textNode:isTextNode,event:isEvent,keyboardEvent:isKeyboardEvent,cue:isCue,track:isTrack,promise:isPromise,url:isUrl,empty:isEmpty};const transitionEndEvent=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!is.string(i)&&t[i]})();function repaint(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}const isIE=Boolean(window.document.documentMode),isEdge=/Edge/g.test(navigator.userAgent),isWebKit=\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone=/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS=\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos=/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1;var browser={isIE:isIE,isEdge:isEdge,isWebKit:isWebKit,isIPhone:isIPhone,isIPadOS:isIPadOS,isIos:isIos};function cloneDeep(e){return JSON.parse(JSON.stringify(e))}function getDeep(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function extend(e={},...t){if(!t.length)return e;const i=t.shift();return is.object(i)?(Object.keys(i).forEach((t=>{is.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),extend(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),extend(e,...t)):e}function wrap(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,r=e.nextSibling;s.appendChild(e),r?n.insertBefore(s,r):n.appendChild(s)}))}function setAttributes(e,t){is.element(e)&&!is.empty(t)&&Object.entries(t).filter((([,e])=>!is.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function createElement(e,t,i){const s=document.createElement(e);return is.object(t)&&setAttributes(s,t),is.string(i)&&(s.innerText=i),s}function insertAfter(e,t){is.element(e)&&is.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)}function insertElement(e,t,i,s){is.element(t)&&t.appendChild(createElement(e,i,s))}function removeElement(e){is.nodeList(e)||is.array(e)?Array.from(e).forEach(removeElement):is.element(e)&&is.element(e.parentNode)&&e.parentNode.removeChild(e)}function emptyElement(e){if(!is.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function replaceElement(e,t){return is.element(t)&&is.element(t.parentNode)&&is.element(e)?(t.parentNode.replaceChild(e,t),e):null}function getAttributesFromSelector(e,t){if(!is.string(e)||is.empty(e))return{};const i={},s=extend({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),r=t.replace(/[[\\]]/g,\"\").split(\"=\"),[a]=r,o=r.length>1?r[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":is.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[a]=o}})),extend(s,i)}function toggleHidden(e,t){if(!is.element(e))return;let i=t;is.boolean(i)||(i=!e.hidden),e.hidden=i}function toggleClass(e,t,i){if(is.nodeList(e))return Array.from(e).map((e=>toggleClass(e,t,i)));if(is.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function hasClass(e,t){return is.element(e)&&e.classList.contains(t)}function matches(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function closest$1(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(matches.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}function getElements(e){return this.elements.container.querySelectorAll(e)}function getElement(e){return this.elements.container.querySelector(e)}function setFocus(e=null,t=!1){is.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const defaultCodecs={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},support={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=support[e]||\"html5\"!==t;return{api:i,ui:i&&support.rangeInput}},pip:!(browser.isIPhone||!is.function(createElement(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||createElement(\"video\").disablePictureInPicture)),airplay:is.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(is.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(defaultCodecs).includes(i)&&(i+=`; codecs=\"${defaultCodecs[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==transitionEndEvent,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},supportsPassiveListeners=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function toggleListener(e,t,i,s=!1,n=!0,r=!1){if(!e||!(\"addEventListener\"in e)||is.empty(t)||!is.function(i))return;const a=t.split(\" \");let o=r;supportsPassiveListeners&&(o={passive:n,capture:r}),a.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function on(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!0,s,n)}function off(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!1,s,n)}function once(e,t=\"\",i,s=!0,n=!1){const r=(...a)=>{off(e,t,r,s,n),i.apply(this,a)};toggleListener.call(this,e,t,r,!0,s,n)}function triggerEvent(e,t=\"\",i=!1,s={}){if(!is.element(e)||is.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function unbindListeners(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ready(){return new Promise((e=>this.ready?setTimeout(e,0):on.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function silencePromise(e){is.promise(e)&&e.then(null,(()=>{}))}function dedupe(e){return is.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function closest(e,t){return is.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function supportsCSS(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const standardRatios=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function validateAspectRatio(e){if(!(is.array(e)||is.string(e)&&e.includes(\":\")))return!1;return(is.array(e)?e:e.split(\":\")).map(Number).every(is.number)}function reduceAspectRatio(e){if(!is.array(e)||!e.every(is.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function getAspectRatio(e){const t=e=>validateAspectRatio(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!is.empty(this.embed)&&is.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return reduceAspectRatio(i)}function setAspectRatio(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=getAspectRatio.call(this,e);if(!is.array(i))return{};const[s,n]=reduceAspectRatio(i),r=100/s*n;if(supportsCSS(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${r}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-r)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:r,ratio:i}}function roundAspectRatio(e,t,i=.05){const s=e/t,n=closest(Object.keys(standardRatios),s);return Math.abs(n-s)<=i?standardRatios[n]:[e,t]}function getViewportSize(){return[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)]}const html5={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!is.empty(t)||support.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:html5.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,is.empty(this.config.ratio)||setAspectRatio.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=html5.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&is.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=html5.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:r,readyState:a,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==r||a)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||silencePromise(e.play())})),e.media.load())}triggerEvent.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(removeElement(html5.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function generateId(e){return`${e}-${Math.floor(1e4*Math.random())}`}function format(e,...t){return is.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}function getPercentage(e,t){return 0===e||0===t||Number.isNaN(e)||Number.isNaN(t)?0:(e/t*100).toFixed(2)}const replaceAll=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),toTitleCase=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function toPascalCase(e=\"\"){let t=e.toString();return t=replaceAll(t,\"-\",\" \"),t=replaceAll(t,\"_\",\" \"),t=toTitleCase(t),replaceAll(t,\" \",\"\")}function toCamelCase(e=\"\"){let t=e.toString();return t=toPascalCase(t),t.charAt(0).toLowerCase()+t.slice(1)}function stripHTML(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}function getHTML(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const resources={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},i18n={get(e=\"\",t={}){if(is.empty(e)||is.empty(t))return\"\";let i=getDeep(t.i18n,e);if(is.empty(i))return Object.keys(resources).includes(e)?resources[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=replaceAll(i,e,t)})),i}};class Storage{constructor(e){_defineProperty$1(this,\"get\",(e=>{if(!Storage.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(is.empty(t))return null;const i=JSON.parse(t);return is.string(e)&&e.length?i[e]:i})),_defineProperty$1(this,\"set\",(e=>{if(!Storage.supported||!this.enabled)return;if(!is.object(e))return;let t=this.get();is.empty(t)&&(t={}),extend(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function fetch(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function loadSprite(e,t){if(!is.string(e))return;const i=\"cache\",s=is.string(t);let n=!1;const r=()=>null!==document.getElementById(t),a=(e,t)=>{e.innerHTML=t,s&&r()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!r()){const r=Storage.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),r){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);a(o,t.content)}}fetch(e).then((e=>{if(!is.empty(e)){if(r)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}a(o,e)}})).catch((()=>{}))}}const getHours=e=>Math.trunc(e/60/60%60,10),getMinutes=e=>Math.trunc(e/60%60,10),getSeconds=e=>Math.trunc(e%60,10);function formatTime(e=0,t=!1,i=!1){if(!is.number(e))return formatTime(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=getHours(e);const r=getMinutes(e),a=getSeconds(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(r)}:${s(a)}`}const controls={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||browser.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=getElement.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:getElements.call(this,this.config.selectors.buttons.play),pause:getElement.call(this,this.config.selectors.buttons.pause),restart:getElement.call(this,this.config.selectors.buttons.restart),rewind:getElement.call(this,this.config.selectors.buttons.rewind),fastForward:getElement.call(this,this.config.selectors.buttons.fastForward),mute:getElement.call(this,this.config.selectors.buttons.mute),pip:getElement.call(this,this.config.selectors.buttons.pip),airplay:getElement.call(this,this.config.selectors.buttons.airplay),settings:getElement.call(this,this.config.selectors.buttons.settings),captions:getElement.call(this,this.config.selectors.buttons.captions),fullscreen:getElement.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=getElement.call(this,this.config.selectors.progress),this.elements.inputs={seek:getElement.call(this,this.config.selectors.inputs.seek),volume:getElement.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:getElement.call(this,this.config.selectors.display.buffer),currentTime:getElement.call(this,this.config.selectors.display.currentTime),duration:getElement.call(this,this.config.selectors.display.duration)},is.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=controls.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,r=document.createElementNS(i,\"svg\");setAttributes(r,extend(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const a=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in a&&a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),r.appendChild(a),r},createLabel(e,t={}){const i=i18n.get(e,this.config);return createElement(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(is.empty(e))return null;const t=createElement(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(createElement(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=extend({},t);let s=toCamelCase(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||extend(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:is.empty(n.label)&&(n.label=s),is.empty(n.icon)&&(n.icon=e)}const r=createElement(n.element);return n.toggle?(r.appendChild(controls.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),r.appendChild(controls.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),r.appendChild(controls.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),r.appendChild(controls.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(r.appendChild(controls.createIcon.call(this,n.icon)),r.appendChild(controls.createLabel.call(this,n.label))),extend(i,getAttributesFromSelector(this.config.selectors.buttons[s],i)),setAttributes(r,i),\"play\"===s?(is.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(r)):this.elements.buttons[s]=r,r},createRange(e,t){const i=createElement(\"input\",extend(getAttributesFromSelector(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":i18n.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,controls.updateRangeFill.call(this,i),RangeTouch.setup(i),i},createProgress(e,t){const i=createElement(\"progress\",extend(getAttributesFromSelector(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(createElement(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?i18n.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=getAttributesFromSelector(this.config.selectors.display[e],t),s=createElement(\"div\",extend(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":i18n.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){on.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=matches(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))controls.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,is.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,is.element(t)||(t=e.parentNode.lastElementChild)),setFocus.call(this,t,!0))}}),!1),on.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&controls.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:r=!1}){const a=getAttributesFromSelector(this.config.selectors.inputs[i]),o=createElement(\"button\",extend(a,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${a.class?a.class:\"\"}`.trim(),\"aria-checked\":r,value:e})),l=createElement(\"span\");l.innerHTML=s,is.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>matches(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!is.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}controls.showMenuPanel.call(this,\"home\",is.keyboardEvent(t))}}),i,!1),controls.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!is.number(e))return e;return formatTime(e,getHours(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){is.element(e)&&is.number(t)&&(e.innerText=controls.formatTime(t,i))},updateVolume(){this.supported.ui&&(is.element(this.elements.inputs.volume)&&controls.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),is.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){is.element(e)&&(e.value=t,controls.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!is.event(e))return;let t=0;const i=(e,t)=>{const i=is.number(t)?t:0,s=is.element(e)?e:this.elements.display.buffer;if(is.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];is.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":t=getPercentage(this.currentTime,this.duration),\"timeupdate\"===e.type&&controls.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}},updateRangeFill(e){const t=is.event(e)?e.target:e;if(is.element(t)&&\"range\"===t.getAttribute(\"type\")){if(matches(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=controls.formatTime(this.currentTime),i=controls.formatTime(this.duration),s=i18n.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(matches(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(browser.isWebKit||browser.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!is.element(this.elements.inputs.seek)||!is.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,r=e=>toggleClass(s,n,e);if(this.touch)return void r(!1);let a=0;const o=this.elements.progress.getBoundingClientRect();if(is.event(e))a=100/o.width*(e.pageX-o.left);else{if(!hasClass(s,n))return;a=parseFloat(s.style.left,10)}a<0?a=0:a>100&&(a=100);const l=this.duration/100*a;s.innerText=controls.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${a}%`,is.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&r(\"mouseenter\"===e.type)},timeUpdate(e){const t=!is.element(this.elements.display.duration)&&this.config.invertTime;controls.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||controls.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return toggleHidden(this.elements.display.currentTime,!0),void toggleHidden(this.elements.progress,!0);is.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=is.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&controls.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&controls.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&controls.setMarkers.call(this),controls.updateSeekTooltip.call(this)},toggleMenuButton(e,t){toggleHidden(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,r=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=is.empty(i)?this[e]:i,is.empty(n)&&(n=this.config[e].default),!is.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(is.element(r)||(r=s&&s.querySelector('[role=\"menu\"]')),!is.element(r))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=controls.getLabel.call(this,e,n);const a=r&&r.querySelector(`[value=\"${n}\"]`);is.element(a)&&(a.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?i18n.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(is.number(t)){const e=i18n.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return toTitleCase(t);case\"captions\":return captions.getLabel.call(this);default:return null}},setQualityMenu(e){if(!is.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');is.array(e)&&(this.options.quality=dedupe(e).filter((e=>this.config.quality.options.includes(e))));const s=!is.empty(this.options.quality)&&this.options.quality.length>1;if(controls.toggleMenuButton.call(this,t,s),emptyElement(i),controls.checkMenu.call(this),!s)return;const n=e=>{const t=i18n.get(`qualityBadge.${e}`,this.config);return t.length?controls.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{controls.createMenuItem.call(this,{value:e,list:i,type:t,title:controls.getLabel.call(this,\"quality\",e),badge:n(e)})})),controls.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!is.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=captions.getTracks.call(this),s=Boolean(i.length);if(controls.toggleMenuButton.call(this,e,s),emptyElement(t),controls.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:captions.getLabel.call(this,e),badge:e.language&&controls.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:i18n.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(controls.createMenuItem.bind(this)),controls.updateSetting.call(this,e,t)},setSpeedMenu(){if(!is.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!is.empty(this.options.speed)&&this.options.speed.length>1;controls.toggleMenuButton.call(this,e,i),emptyElement(t),controls.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{controls.createMenuItem.call(this,{value:i,list:t,type:e,title:controls.getLabel.call(this,\"speed\",i)})})),controls.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!is.empty(e)&&Object.values(e).some((e=>!e.hidden));toggleHidden(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;is.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');setFocus.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!is.element(t)||!is.element(i))return;const{hidden:s}=t;let n=s;if(is.boolean(e))n=e;else if(is.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(is.event(e)){const s=is.function(e.composedPath)?e.composedPath()[0]:e.target,r=t.contains(s);if(r||!r&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),toggleHidden(t,!n),toggleClass(this.elements.container,this.config.classNames.menu.open,n),n&&is.keyboardEvent(e)?controls.focusFirstMenuItem.call(this,null,!0):n||s||setFocus.call(this,i,is.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return removeElement(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!is.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(support.transitions&&!support.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=controls.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",off.call(this,s,transitionEndEvent,t))};on.call(this,s,transitionEndEvent,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}toggleHidden(n,!0),toggleHidden(i,!1),controls.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;is.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:r,setQualityMenu:a,setSpeedMenu:o,showMenuPanel:l}=controls;this.elements.controls=null,is.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=createElement(\"div\",getAttributesFromSelector(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return dedupe(is.array(this.config.controls)?this.config.controls:[]).forEach((a=>{if(\"restart\"===a&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===a&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===a&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===a&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===a){const t=createElement(\"div\",{class:`${u.class} plyr__progress__container`}),i=createElement(\"div\",getAttributesFromSelector(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=createElement(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===a&&c.appendChild(r.call(this,\"currentTime\",u)),\"duration\"===a&&c.appendChild(r.call(this,\"duration\",u)),\"mute\"===a||\"volume\"===a){let{volume:t}=this.elements;if(is.element(t)&&c.contains(t)||(t=createElement(\"div\",extend({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===a&&t.appendChild(i.call(this,\"mute\")),\"volume\"===a&&!browser.isIos&&!browser.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",extend(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===a&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===a&&!is.empty(this.config.settings)){const s=createElement(\"div\",extend({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=createElement(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),r=createElement(\"div\"),a=createElement(\"div\",{id:`plyr-settings-${e.id}-home`}),o=createElement(\"div\",{role:\"menu\"});a.appendChild(o),r.appendChild(a),this.elements.settings.panels.home=a,this.config.settings.forEach((i=>{const s=createElement(\"button\",extend(getAttributesFromSelector(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),on.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=createElement(\"span\",null,i18n.get(i,this.config)),a=createElement(\"span\",{class:this.config.classNames.menu.value});a.innerHTML=e[i],n.appendChild(a),s.appendChild(n),o.appendChild(s);const c=createElement(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=createElement(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(createElement(\"span\",{\"aria-hidden\":!0},i18n.get(i,this.config))),u.appendChild(createElement(\"span\",{class:this.config.classNames.hidden},i18n.get(\"menuBack\",this.config))),on.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),on.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(createElement(\"div\",{role:\"menu\"})),r.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(r),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===a&&support.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===a&&support.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===a){const e=extend({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!is.url(t)&&this.isEmbed&&extend(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===a&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&a.call(this,html5.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=controls.getIconUrl.call(this);e.cors&&loadSprite(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;is.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),is.element(this.config.controls)||is.string(this.config.controls)?e=this.config.controls:(e=controls.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:captions.getLabel.call(this)}),i=!1);let s;i&&is.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=replaceAll(i,`{${e}}`,t)})),i})(e)),is.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),is.element(s)||(s=this.elements.container);if(s[is.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),is.element(this.elements.controls)||controls.findElements.call(this),!is.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>hasClass(e,t),set(i=!1){toggleClass(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{is.array(t)||is.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(browser.isEdge&&repaint(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=getElements.call(this,i);Array.from(s).forEach((e=>{toggleClass(e,this.config.classNames.hidden,!1),toggleClass(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let r=null;const a=`${this.config.classNames.tooltip}--visible`,o=e=>toggleClass(r,a,e);i.forEach((e=>{const t=createElement(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";r&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(r.style.left=i,r.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(r=createElement(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(r)),this.elements.markers={points:n,tip:r},this.elements.progress.appendChild(s)}};function parseUrl(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function buildUrlParams(e){const t=new URLSearchParams;return is.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const captions={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!support.textTracks)return void(is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this));if(is.element(this.elements.captions)||(this.elements.captions=createElement(\"div\",getAttributesFromSelector(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),insertAfter(this.elements.captions,this.elements.wrapper)),browser.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=parseUrl(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&fetch(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{removeElement(e)}))}))}const e=dedupe((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let t=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===t&&([t]=e);let i=this.storage.get(\"captions\");if(is.boolean(i)||({active:i}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:i,language:t,languages:e}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";on.call(this,this.media.textTracks,e,captions.update.bind(this))}setTimeout(captions.update.bind(this),0)},update(){const e=captions.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,r=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),on.call(this,e,\"cuechange\",(()=>captions.updateCues.call(this)))})),(r&&this.language!==i||!e.includes(n))&&(captions.setLanguage.call(this,i),captions.toggle.call(this,t&&r)),this.elements&&toggleClass(this.elements.container,this.config.classNames.captions.enabled,!is.empty(e)),is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=is.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=captions.getTracks.call(this),t=captions.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void captions.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),toggleClass(this.elements.container,s,n),this.captions.toggled=n,controls.updateSetting.call(this,\"captions\"),triggerEvent.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=captions.getTracks.call(this);if(-1!==e)if(is.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,controls.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),triggerEvent.call(this,this.media,\"languagechange\")}captions.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&captions.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else captions.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!is.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=captions.getTracks.call(this),n=captions.findTrack.call(this,[i]);captions.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=captions.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let r;return e.every((e=>(r=n.find((t=>t.language===e)),!r))),r||(t?n[0]:void 0)},getCurrentTrack(){return captions.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!is.track(t)&&support.textTracks&&this.captions.toggled&&(t=captions.getCurrentTrack.call(this)),is.track(t)?is.empty(t.label)?is.empty(t.language)?i18n.get(\"enabled\",this.config):e.language.toUpperCase():t.label:i18n.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!is.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!is.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=captions.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(getHTML)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){emptyElement(this.elements.captions);const e=createElement(\"span\",getAttributesFromSelector(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),triggerEvent.call(this,this.media,\"cuechange\")}}},defaults={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:null,listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},pip={active:\"picture-in-picture\",inactive:\"inline\"},providers={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},types={audio:\"audio\",video:\"video\"};function getProviderByUrl(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?providers.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?providers.vimeo:null}const noop=()=>{};class Console{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):noop}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):noop}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):noop}}class Fullscreen{constructor(e){_defineProperty$1(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;is.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;triggerEvent.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),_defineProperty$1(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",toggleClass(this.target,this.player.config.classNames.fullscreen.fallback,e),browser.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=is.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),_defineProperty$1(this,\"trapFocus\",(e=>{if(browser.isIos||browser.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=getElements.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),_defineProperty$1(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":Fullscreen.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");toggleClass(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),_defineProperty$1(this,\"enter\",(()=>{this.supported&&(browser.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!Fullscreen.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?is.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),_defineProperty$1(this,\"exit\",(()=>{if(this.supported)if(browser.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),silencePromise(this.player.play());else if(!Fullscreen.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!is.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),_defineProperty$1(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=Fullscreen.prefix,this.property=Fullscreen.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&closest$1(this.player.elements.container,e.config.fullscreen.container),on.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),on.call(this.player,this.player.elements.container,\"dblclick\",(e=>{is.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),on.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return Fullscreen.nativeSupported&&!this.forceFallback}static get prefix(){if(is.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!is.function(document[`${t}ExitFullscreen`])&&!is.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,Fullscreen.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||Fullscreen.nativeSupported||!browser.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!Fullscreen.nativeSupported||this.forceFallback)return hasClass(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return browser.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function loadImage(e,t=1){return new Promise(((i,s)=>{const n=new Image,r=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:r,onerror:r,src:e})}))}const ui={addStyleHook(){toggleClass(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),toggleClass(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void ui.toggleNativeControls.call(this,!0);is.element(this.elements.controls)||(controls.inject.call(this),this.listeners.controls()),ui.toggleNativeControls.call(this),this.isHTML5&&captions.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,controls.updateVolume.call(this),controls.timeUpdate.call(this),controls.durationUpdate.call(this),ui.checkPlaying.call(this),toggleClass(this.elements.container,this.config.classNames.pip.supported,support.pip&&this.isHTML5&&this.isVideo),toggleClass(this.elements.container,this.config.classNames.airplay.supported,support.airplay&&this.isHTML5),toggleClass(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{triggerEvent.call(this,this.media,\"ready\")}),0),ui.setTitle.call(this),this.poster&&ui.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&controls.durationUpdate.call(this),this.config.mediaMetadata&&controls.setMediaMetadata.call(this)},setTitle(){let e=i18n.get(\"play\",this.config);if(is.string(this.config.title)&&!is.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=getElement.call(this,\"iframe\");if(!is.element(e))return;const t=is.empty(this.config.title)?\"video\":this.config.title,i=i18n.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){toggleClass(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ready.call(this).then((()=>loadImage(e))).catch((t=>{throw e===this.poster&&ui.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),ui.togglePoster.call(this,!0),e))))},checkPlaying(e){toggleClass(this.elements.container,this.config.classNames.playing,this.playing),toggleClass(this.elements.container,this.config.classNames.paused,this.paused),toggleClass(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",i18n.get(this.playing?\"pause\":\"play\",this.config))})),is.event(e)&&\"timeupdate\"===e.type||ui.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{toggleClass(this.elements.container,this.config.classNames.loading,this.loading),ui.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!is.empty(e)&&is.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),is.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Listeners{constructor(e){_defineProperty$1(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,toggleClass(t.container,e.config.classNames.isTouch,!0)})),_defineProperty$1(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&toggleListener.call(t,window,\"keydown keyup\",this.handleKey,e,!1),toggleListener.call(t,document.body,\"click\",this.toggleMenu,e),once.call(t,document.body,\"touchstart\",this.firstTouch)})),_defineProperty$1(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&on.call(e,i.container,\"keydown keyup\",this.handleKey,!1),on.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let r=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(ui.toggleControls.call(e,!0),r=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),r)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,r]=getAspectRatio.call(e),a=supportsCSS(`aspect-ratio: ${n} / ${r}`);if(!s)return void(a?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=getViewportSize(),c=o/l>n/r;a?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/r*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},r=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};on.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&is.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?on:off).call(e,window,\"resize\",r)}))})),_defineProperty$1(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(on.call(e,e.media,\"timeupdate seeking seeked\",(t=>controls.timeUpdate.call(e,t))),on.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>controls.durationUpdate.call(e,t))),on.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),on.call(e,e.media,\"progress playing seeking seeked\",(t=>controls.updateProgress.call(e,t))),on.call(e,e.media,\"volumechange\",(t=>controls.updateVolume.call(e,t))),on.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>ui.checkPlaying.call(e,t))),on.call(e,e.media,\"waiting canplay seeked playing\",(t=>ui.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=getElement.call(e,`.${e.config.classNames.video}`);if(!is.element(i))return;on.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{silencePromise(e.play())}),\"play\")):this.proxy(s,(()=>{silencePromise(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&on.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),on.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),on.call(e,e.media,\"ratechange\",(()=>{controls.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),on.call(e,e.media,\"qualitychange\",(t=>{controls.updateSetting.call(e,\"quality\",null,t.detail.quality)})),on.call(e,e.media,\"ready qualitychange\",(()=>{controls.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");on.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),triggerEvent.call(e,t.container,i.type,!0,s)}))})),_defineProperty$1(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let r=!0;is.function(n)&&(r=n.call(s,e)),!1!==r&&is.function(t)&&t.call(s,e)})),_defineProperty$1(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:r}=this,a=r.config.listeners[s],o=is.function(a);on.call(r,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),_defineProperty$1(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=browser.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{silencePromise(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{triggerEvent.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),controls.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),controls.toggleMenu.call(e,t)):controls.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&controls.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(is.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),r=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&r?(i.removeAttribute(s),silencePromise(e.play())):!r&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),browser.isIos){const t=getElements.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>repaint(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");is.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>controls.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),browser.isWebKit&&Array.from(getElements.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>controls.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!is.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,controls.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;toggleClass(t.controls,i.classNames.noTransition,!0),ui.toggleControls.call(e,!0),setTimeout((()=>{toggleClass(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),r=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(r/50);const{volume:a}=e.media;(1===r&&a<1||-1===r&&a>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:r,ctrlKey:a,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(r||a||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(is.element(n)){const{editable:s}=t.config.selectors,{seek:r}=i.inputs;if(n!==r&&matches(n,s))return;if(\" \"===e.key&&matches(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(d=parseInt(s,10),t.currentTime=t.duration/10*d);break;case\" \":case\"k\":u||silencePromise(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var d}toggleMenu(e){controls.toggleMenu.call(this.player,e)}}var commonjsGlobal=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}var loadjs_umd=createCommonjsModule((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,r,a,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)r=e[l],(a=i[r])?n(r,a):(s[r]=s[r]||[]).push(n)}function r(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function a(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var r,a,l=document,c=s.async,u=(s.numRetries||0)+1,d=s.before||e,h=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(h)?((a=l.createElement(\"link\")).rel=\"stylesheet\",a.href=m,(r=\"hideFocus\"in a)&&a.relList&&(r=0,a.rel=\"preload\",a.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(h)?(a=l.createElement(\"img\")).src=m:((a=l.createElement(\"script\")).src=t,a.async=void 0===c||c),a.onload=a.onerror=a.onbeforeload=function(e){var l=e.type[0];if(r)try{a.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==a.rel&&\"style\"==a.as)return a.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==d(t,a)&&l.head.appendChild(a)}function l(e,t,i){var s,n,r=(e=e.push?e:[e]).length,a=r,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--r||t(l)},n=0;n<a;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){a(o,e),t&&a({success:t,error:i},e),r(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){a(t,e)})),c},c.done=function(e){r(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function loadScript(e){return new Promise(((t,i)=>{loadjs_umd(e,{success:t,error:i})}))}function parseId$1(e){if(is.empty(e))return null;if(is.number(Number(e)))return e;return e.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:e}function parseHash(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}function assurePlaybackState$1(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}const vimeo={setup(){const e=this;toggleClass(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,setAspectRatio.call(e),is.object(window.Vimeo)?vimeo.ready.call(e):loadScript(e.config.urls.vimeo.sdk).then((()=>{vimeo.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let r=e.media.getAttribute(\"src\"),a=\"\";is.empty(r)?(r=e.media.getAttribute(e.config.attributes.embed.id),a=e.media.getAttribute(e.config.attributes.embed.hash)):a=parseHash(r);const o=a?{h:a}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=buildUrlParams({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=parseId$1(r),u=createElement(\"iframe\"),d=format(e.config.urls.vimeo.iframe,c,l);if(u.setAttribute(\"src\",d),u.setAttribute(\"allowfullscreen\",\"\"),u.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),is.empty(s)||u.setAttribute(\"referrerPolicy\",s),i||!t.customControls)u.setAttribute(\"data-poster\",e.poster),e.media=replaceElement(u,e.media);else{const t=createElement(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(u),e.media=replaceElement(t,e.media)}t.customControls||fetch(format(e.config.urls.vimeo.api,d)).then((t=>{!is.empty(t)&&t.thumbnail_url&&ui.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(u,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(assurePlaybackState$1.call(e,!0),e.embed.play()),e.media.pause=()=>(assurePlaybackState$1.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:h}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>h,set(t){const{embed:i,media:s,paused:n,volume:r}=e,a=n&&!i.hasPlayed;s.seeking=!0,triggerEvent.call(e,s,\"seeking\"),Promise.resolve(a&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>a&&i.pause())).then((()=>a&&i.setVolume(r))).catch((()=>{}))}});let m=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>m,set(t){e.embed.setPlaybackRate(t).then((()=>{m=t,triggerEvent.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:p}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>p,set(t){e.embed.setVolume(t).then((()=>{p=t,triggerEvent.call(e,e.media,\"volumechange\")}))}});let{muted:g}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>g,set(t){const i=!!is.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{g=i,triggerEvent.call(e,e.media,\"volumechange\")}))}});let f,{loop:y}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>y,set(t){const i=is.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{y=i}))}}),e.embed.getVideoUrl().then((t=>{f=t,controls.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>f}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=roundAspectRatio(i,s),setAspectRatio.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,ui.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{h=t,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,captions.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>stripHTML(e.text)));captions.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{assurePlaybackState$1.call(e,!t),t||triggerEvent.call(e,e.media,\"playing\")})),is.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{triggerEvent.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{assurePlaybackState$1.call(e,!0),triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{assurePlaybackState$1.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,h=t.seconds,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,triggerEvent.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&triggerEvent.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,triggerEvent.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,triggerEvent.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>ui.build.call(e)),0)}};function parseId(e){if(is.empty(e))return null;return e.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:e}function assurePlaybackState(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}function getHost(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const youtube={setup(){if(toggleClass(this.elements.wrapper,this.config.classNames.embed,!0),is.object(window.YT)&&is.function(window.YT.Player))youtube.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{is.function(e)&&e(),youtube.ready.call(this)},loadScript(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){fetch(format(this.config.urls.youtube.api,e)).then((e=>{if(is.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,ui.setTitle.call(this),this.embed.ratio=roundAspectRatio(s,i)}setAspectRatio.call(this)})).catch((()=>{setAspectRatio.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!is.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");is.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=parseId(s),r=createElement(\"div\",{id:generateId(e.provider),\"data-poster\":t.customControls?e.poster:void 0});if(e.media=replaceElement(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;loadImage(t(\"maxres\"),121).catch((()=>loadImage(t(\"sd\"),121))).catch((()=>loadImage(t(\"hq\")))).then((t=>ui.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:getHost(t),playerVars:extend({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},triggerEvent.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),triggerEvent.call(e,e.media,\"ratechange\")},onReady(i){if(is.function(e.media.play))return;const s=i.target;youtube.getTitle.call(e,n),e.media.play=()=>{assurePlaybackState.call(e,!0),s.playVideo()},e.media.pause=()=>{assurePlaybackState.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,triggerEvent.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:r}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>r,set(t){r=t,s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}});let{muted:a}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>a,set(t){const i=is.boolean(t)?t:a;a=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),triggerEvent.call(e,e.media,\"timeupdate\"),triggerEvent.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&triggerEvent.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),triggerEvent.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>ui.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")),i.data){case-1:triggerEvent.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),triggerEvent.call(e,e.media,\"progress\");break;case 0:assurePlaybackState.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):triggerEvent.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(assurePlaybackState.call(e,!0),triggerEvent.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{triggerEvent.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),triggerEvent.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),assurePlaybackState.call(e,!1);break;case 3:triggerEvent.call(e,e.media,\"waiting\")}triggerEvent.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},media={setup(){this.media?(toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),toggleClass(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=createElement(\"div\",{class:this.config.classNames.video}),wrap(this.media,this.elements.wrapper),this.elements.poster=createElement(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?html5.setup.call(this):this.isYouTube?youtube.setup.call(this):this.isVimeo&&vimeo.setup.call(this)):this.debug.warn(\"No media element found!\")}},destroy=e=>{e.manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()};class Ads{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.enabled&&(is.object(window.google)&&is.object(window.google.ima)?this.ready():loadScript(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),_defineProperty$1(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),_defineProperty$1(this,\"setupIMA\",(()=>{this.elements.container=createElement(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),_defineProperty$1(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),_defineProperty$1(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=formatTime(Math.max(this.manager.getRemainingTime(),0)),t=`${i18n.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),_defineProperty$1(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),_defineProperty$1(this,\"addCuePoints\",(()=>{is.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(is.element(t)){const i=100/this.player.duration*e,s=createElement(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),_defineProperty$1(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{triggerEvent.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),_defineProperty$1(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),_defineProperty$1(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;is.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),_defineProperty$1(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),_defineProperty$1(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,silencePromise(this.player.media.play())})),_defineProperty$1(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),_defineProperty$1(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),_defineProperty$1(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),_defineProperty$1(this,\"trigger\",((e,...t)=>{const i=this.events[e];is.array(i)&&i.forEach((e=>{is.function(e)&&e.apply(this,t)}))})),_defineProperty$1(this,\"on\",((e,t)=>(is.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),_defineProperty$1(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),_defineProperty$1(this,\"clearSafetyTimer\",(e=>{is.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!is.empty(e.publisherId)||is.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(is.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${buildUrlParams({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function clamp(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const parseVtt=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(is.number(i.startTime)){if(!is.empty(e.trim())&&is.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},fitRatio=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class PreviewThumbnails{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),_defineProperty$1(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(is.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(is.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(is.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),_defineProperty$1(this,\"getThumbnail\",(e=>new Promise((t=>{fetch(e).then((i=>{const s={frames:parseVtt(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),_defineProperty$1(this,\"startMove\",(e=>{if(this.loaded&&is.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=formatTime(this.seekTime);const r=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));r&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${r.label}<br>`)}this.showImageAtCurrentTime()}})),_defineProperty$1(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),_defineProperty$1(this,\"startScrubbing\",(e=>{(is.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),_defineProperty$1(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):once.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),_defineProperty$1(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),_defineProperty$1(this,\"render\",(()=>{this.elements.thumb.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=createElement(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),is.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),_defineProperty$1(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),_defineProperty$1(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),_defineProperty$1(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],r=i.frames[t].text,a=s+r;if(this.currentImageElement&&this.currentImageElement.dataset.filename===r)this.showImage(this.currentImageElement,n,e,t,r,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=a,i.dataset.index=t,i.dataset.filename=r,this.showingThumbFilename=r,this.player.debug.log(`Loading image: ${a}`),i.onload=()=>this.showImage(i,n,e,t,r,!0),this.loadingImage=i,this.removeOldImages(i)}})),_defineProperty$1(this,\"showImage\",((e,t,i,s,n,r=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${r}`),this.setImageSizeAndOffset(e,t),r&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),_defineProperty$1(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),_defineProperty$1(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let r=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){r=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),r||i()}}),300)})))),_defineProperty$1(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),_defineProperty$1(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),_defineProperty$1(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),_defineProperty$1(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,r=this.mousePosX-e.left-i.clientWidth/2,a=clamp(r,s,n);i.style.left=`${a}px`,i.style.setProperty(\"--preview-arrow-offset\",r-a+\"px\")})),_defineProperty$1(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),_defineProperty$1(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const source={insertElements(e,t){is.string(t)?insertElement(e,this.media,{src:t}):is.array(t)&&t.forEach((t=>{insertElement(e,this.media,t)}))},change(e){getDeep(e,\"sources.length\")?(html5.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],removeElement(this.media),this.media=null,is.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=providers.html5,src:n}]=t,r=\"html5\"===s?i:\"div\",a=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:support.check(i,s,this.config.playsinline),media:createElement(r,a)}),this.elements.container.appendChild(this.media),is.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),is.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),ui.addStyleHook.call(this),this.isHTML5&&source.insertElements.call(this,\"source\",t),this.config.title=e.title,media.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&source.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.isHTML5&&this.media.load(),is.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class Plyr{constructor(e,t){if(_defineProperty$1(this,\"play\",(()=>is.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>silencePromise(this.media.play()))),this.media.play()):null)),_defineProperty$1(this,\"pause\",(()=>this.playing&&is.function(this.media.pause)?this.media.pause():null)),_defineProperty$1(this,\"togglePlay\",(e=>(is.boolean(e)?e:!this.playing)?this.play():this.pause())),_defineProperty$1(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):is.function(this.media.stop)&&this.media.stop()})),_defineProperty$1(this,\"restart\",(()=>{this.currentTime=0})),_defineProperty$1(this,\"rewind\",(e=>{this.currentTime-=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"forward\",(e=>{this.currentTime+=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(is.number(e)?e:0)})),_defineProperty$1(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),_defineProperty$1(this,\"airplay\",(()=>{support.airplay&&this.media.webkitShowPlaybackTargetPicker()})),_defineProperty$1(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=hasClass(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=toggleClass(this.elements.container,this.config.classNames.hideControls,i);if(s&&is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!is.empty(this.config.settings)&&controls.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";triggerEvent.call(this,this.media,e)}return!s}return!1})),_defineProperty$1(this,\"on\",((e,t)=>{on.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"once\",((e,t)=>{once.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"off\",((e,t)=>{off(this.elements.container,e,t)})),_defineProperty$1(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(removeElement(this.elements.buttons.play),removeElement(this.elements.captions),removeElement(this.elements.controls),removeElement(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),is.function(e)&&e()):(unbindListeners.call(this),html5.cancelRequests.call(this),replaceElement(this.elements.original,this.elements.container),triggerEvent.call(this,this.elements.original,\"destroyed\",!0),is.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(ui.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&is.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),_defineProperty$1(this,\"supports\",(e=>support.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=support.touch,this.media=e,is.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||is.nodeList(this.media)||is.array(this.media))&&(this.media=this.media[0]),this.config=extend({},defaults,Plyr.defaults,t||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new Console(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",support),is.nullOrUndefined(this.media)||!is.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!support.check().api)return void this.debug.error(\"Setup failed: no support\");const i=this.media.cloneNode(!0);i.autoplay=!1,this.elements.original=i;const s=this.media.tagName.toLowerCase();let n=null,r=null;switch(s){case\"div\":if(n=this.media.querySelector(\"iframe\"),is.element(n)){if(r=parseUrl(n.getAttribute(\"src\")),this.provider=getProviderByUrl(r.toString()),this.elements.container=this.media,this.media=n,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(is.empty(this.provider)||!Object.values(providers).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=types.video;break;case\"video\":case\"audio\":this.type=s,this.provider=providers.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=support.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Listeners(this),this.storage=new Storage(this),this.media.plyr=this,is.element(this.elements.container)||(this.elements.container=createElement(\"div\"),wrap(this.media,this.elements.container)),ui.migrateStyles.call(this),ui.addStyleHook.call(this),media.setup.call(this),this.config.debug&&on.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new Fullscreen(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ads(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>silencePromise(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===providers.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===providers.youtube}get isVimeo(){return this.provider===providers.vimeo}get isVideo(){return this.type===types.video}get isAudio(){return this.type===types.audio}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=is.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return is.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=is.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;is.string(t)&&(t=Number(t)),is.number(t)||(t=this.storage.get(\"volume\")),is.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!is.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;is.boolean(t)||(t=this.storage.get(\"muted\")),is.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;is.number(e)&&(t=e),is.number(t)||(t=this.storage.get(\"speed\")),is.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=clamp(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!is.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(is.number),n=!0;if(!i.includes(s)){const e=closest(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=is.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){source.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return is.url(e)?e:this.source}set download(e){is.url(e)&&(this.config.urls.download=e,controls.setDownloadUrl.call(this))}set poster(e){this.isVideo?ui.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=reduceAspectRatio(getAspectRatio.call(this));return is.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?is.string(e)&&validateAspectRatio(e)?(this.config.ratio=reduceAspectRatio(e),setAspectRatio.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=is.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){captions.toggle.call(this,e,!1)}set currentTrack(e){captions.set.call(this,e,!1),captions.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){captions.setLanguage.call(this,e,!1)}get language(){return(captions.getCurrentTrack.call(this)||{}).language}set pip(e){if(!support.pip)return;const t=is.boolean(e)?e:!this.pip;is.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?pip.active:pip.inactive),is.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return support.pip?is.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===pip.active:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))}static supported(e,t){return support.check(e,t)}static loadSprite(e,t){return loadSprite(e,t)}static setup(e,t={}){let i=null;return is.string(e)?i=Array.from(document.querySelectorAll(e)):is.nodeList(e)?i=Array.from(e):is.array(e)&&(i=e.filter(is.element)),is.empty(i)?null:i.map((e=>new Plyr(e,t)))}}Plyr.defaults=cloneDeep(defaults);export{Plyr as default};\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.min.mjs.map b/node_modules/plyr/dist/plyr.min.mjs.map\nindex 230e83a..cb079be 100644\n--- a/node_modules/plyr/dist/plyr.min.mjs.map\n+++ b/node_modules/plyr/dist/plyr.min.mjs.map\n@@ -1 +1 @@\n-{\"version\":3,\"sources\":[\"plyr.mjs\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"_toPropertyKey\",\"Object\",\"defineProperty\",\"enumerable\",\"configurable\",\"writable\",\"_toPrimitive\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"call\",\"TypeError\",\"String\",\"Number\",\"arg\",\"_classCallCheck\",\"e\",\"t\",\"_defineProperties\",\"n\",\"length\",\"r\",\"_createClass\",\"prototype\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"matches\",\"Array\",\"from\",\"document\",\"querySelectorAll\",\"includes\",\"this\",\"trigger\",\"Event\",\"bubbles\",\"dispatchEvent\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isNumber\",\"isNaN\",\"isString\",\"isBoolean\",\"Boolean\",\"isFunction\",\"Function\",\"isArray\",\"isNodeList\",\"NodeList\",\"isElement\",\"Element\",\"isEvent\",\"isEmpty\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"string\",\"boolean\",\"function\",\"array\",\"nodeList\",\"element\",\"event\",\"empty\",\"getDecimalPlaces\",\"concat\",\"match\",\"Math\",\"max\",\"round\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"set\",\"target\",\"i\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"u\",\"c\",\"getBoundingClientRect\",\"a\",\"width\",\"clientX\",\"left\",\"disabled\",\"preventDefault\",\"get\",\"type\",\"MutationObserver\",\"addedNodes\",\"observe\",\"body\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isWeakMap\",\"WeakMap\",\"isTextNode\",\"Text\",\"isKeyboardEvent\",\"KeyboardEvent\",\"isCue\",\"window\",\"TextTrackCue\",\"VTTCue\",\"isTrack\",\"TextTrack\",\"kind\",\"isPromise\",\"Promise\",\"then\",\"nodeType\",\"ownerDocument\",\"isUrl\",\"URL\",\"startsWith\",\"hostname\",\"_\",\"weakMap\",\"textNode\",\"keyboardEvent\",\"cue\",\"track\",\"promise\",\"url\",\"transitionEndEvent\",\"createElement\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"isIE\",\"documentMode\",\"isEdge\",\"test\",\"navigator\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"browser\",\"cloneDeep\",\"JSON\",\"parse\",\"stringify\",\"getDeep\",\"path\",\"split\",\"reduce\",\"extend\",\"sources\",\"source\",\"shift\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"appendChild\",\"insertBefore\",\"setAttributes\",\"attributes\",\"entries\",\"setAttribute\",\"text\",\"innerText\",\"insertAfter\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"replace\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"method\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"closest\",\"el\",\"parentElement\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"callback\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"detail\",\"CustomEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"indexOf\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"getViewportSize\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"generateId\",\"prefix\",\"floor\",\"random\",\"format\",\"toString\",\"getPercentage\",\"current\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"slice\",\"toLowerCase\",\"toPascalCase\",\"toCamelCase\",\"stripHTML\",\"fragment\",\"createDocumentFragment\",\"innerHTML\",\"firstChild\",\"getHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"storage\",\"setItem\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"Error\",\"status\",\"open\",\"send\",\"error\",\"loadSprite\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"location\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"join\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sort\",\"b\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"values\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"href\",\"urls\",\"isEmbed\",\"inject\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"params\",\"URLSearchParams\",\"isYouTube\",\"protocol\",\"blob\",\"createObjectURL\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"has\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"global\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"inactive\",\"providers\",\"types\",\"getProviderByUrl\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"head\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"attribute\",\"hasAttribute\",\"done\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"commonjsGlobal\",\"globalThis\",\"self\",\"createCommonjsModule\",\"fn\",\"module\",\"exports\",\"loadjs_umd\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"doc\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathname\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"defaultPrevented\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"factory\",\"loadScript\",\"parseId\",\"$2\",\"parseHash\",\"found\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"hashParam\",\"sidedock\",\"gesture\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"setInterval\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"destroy\",\"manager\",\"displayContainer\",\"remove\",\"Ads\",\"google\",\"ima\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"Plyr\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"search\",\"truthy\",\"searchParams\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.min.mjs\",\"sourcesContent\":[\"function _defineProperty$1(obj, key, value) {\\n  key = _toPropertyKey(key);\\n  if (key in obj) {\\n    Object.defineProperty(obj, key, {\\n      value: value,\\n      enumerable: true,\\n      configurable: true,\\n      writable: true\\n    });\\n  } else {\\n    obj[key] = value;\\n  }\\n  return obj;\\n}\\nfunction _toPrimitive(input, hint) {\\n  if (typeof input !== \\\"object\\\" || input === null) return input;\\n  var prim = input[Symbol.toPrimitive];\\n  if (prim !== undefined) {\\n    var res = prim.call(input, hint || \\\"default\\\");\\n    if (typeof res !== \\\"object\\\") return res;\\n    throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n  }\\n  return (hint === \\\"string\\\" ? String : Number)(input);\\n}\\nfunction _toPropertyKey(arg) {\\n  var key = _toPrimitive(arg, \\\"string\\\");\\n  return typeof key === \\\"symbol\\\" ? key : String(key);\\n}\\n\\nfunction _classCallCheck(e, t) {\\n  if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n}\\nfunction _defineProperties(e, t) {\\n  for (var n = 0; n < t.length; n++) {\\n    var r = t[n];\\n    r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n  }\\n}\\nfunction _createClass(e, t, n) {\\n  return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n}\\nfunction _defineProperty(e, t, n) {\\n  return t in e ? Object.defineProperty(e, t, {\\n    value: n,\\n    enumerable: !0,\\n    configurable: !0,\\n    writable: !0\\n  }) : e[t] = n, e;\\n}\\nfunction ownKeys(e, t) {\\n  var n = Object.keys(e);\\n  if (Object.getOwnPropertySymbols) {\\n    var r = Object.getOwnPropertySymbols(e);\\n    t && (r = r.filter(function (t) {\\n      return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n    })), n.push.apply(n, r);\\n  }\\n  return n;\\n}\\nfunction _objectSpread2(e) {\\n  for (var t = 1; t < arguments.length; t++) {\\n    var n = null != arguments[t] ? arguments[t] : {};\\n    t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n      _defineProperty(e, t, n[t]);\\n    }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n      Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n    });\\n  }\\n  return e;\\n}\\nvar defaults$1 = {\\n  addCSS: !0,\\n  thumbWidth: 15,\\n  watch: !0\\n};\\nfunction matches$1(e, t) {\\n  return function () {\\n    return Array.from(document.querySelectorAll(t)).includes(this);\\n  }.call(e, t);\\n}\\nfunction trigger(e, t) {\\n  if (e && t) {\\n    var n = new Event(t, {\\n      bubbles: !0\\n    });\\n    e.dispatchEvent(n);\\n  }\\n}\\nvar getConstructor$1 = function (e) {\\n    return null != e ? e.constructor : null;\\n  },\\n  instanceOf$1 = function (e, t) {\\n    return !!(e && t && e instanceof t);\\n  },\\n  isNullOrUndefined$1 = function (e) {\\n    return null == e;\\n  },\\n  isObject$1 = function (e) {\\n    return getConstructor$1(e) === Object;\\n  },\\n  isNumber$1 = function (e) {\\n    return getConstructor$1(e) === Number && !Number.isNaN(e);\\n  },\\n  isString$1 = function (e) {\\n    return getConstructor$1(e) === String;\\n  },\\n  isBoolean$1 = function (e) {\\n    return getConstructor$1(e) === Boolean;\\n  },\\n  isFunction$1 = function (e) {\\n    return getConstructor$1(e) === Function;\\n  },\\n  isArray$1 = function (e) {\\n    return Array.isArray(e);\\n  },\\n  isNodeList$1 = function (e) {\\n    return instanceOf$1(e, NodeList);\\n  },\\n  isElement$1 = function (e) {\\n    return instanceOf$1(e, Element);\\n  },\\n  isEvent$1 = function (e) {\\n    return instanceOf$1(e, Event);\\n  },\\n  isEmpty$1 = function (e) {\\n    return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n  },\\n  is$1 = {\\n    nullOrUndefined: isNullOrUndefined$1,\\n    object: isObject$1,\\n    number: isNumber$1,\\n    string: isString$1,\\n    boolean: isBoolean$1,\\n    function: isFunction$1,\\n    array: isArray$1,\\n    nodeList: isNodeList$1,\\n    element: isElement$1,\\n    event: isEvent$1,\\n    empty: isEmpty$1\\n  };\\nfunction getDecimalPlaces(e) {\\n  var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n  return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n}\\nfunction round(e, t) {\\n  if (1 > t) {\\n    var n = getDecimalPlaces(t);\\n    return parseFloat(e.toFixed(n));\\n  }\\n  return Math.round(e / t) * t;\\n}\\nvar RangeTouch = function () {\\n  function e(t, n) {\\n    _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n  }\\n  return _createClass(e, [{\\n    key: \\\"init\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n    }\\n  }, {\\n    key: \\\"destroy\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n    }\\n  }, {\\n    key: \\\"listeners\\\",\\n    value: function (e) {\\n      var t = this,\\n        n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n      [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n        t.element[n](e, function (e) {\\n          return t.set(e);\\n        }, !1);\\n      });\\n    }\\n  }, {\\n    key: \\\"get\\\",\\n    value: function (t) {\\n      if (!e.enabled || !is$1.event(t)) return null;\\n      var n,\\n        r = t.target,\\n        i = t.changedTouches[0],\\n        o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n        s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n        u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n        c = r.getBoundingClientRect(),\\n        a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n      return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n    }\\n  }, {\\n    key: \\\"set\\\",\\n    value: function (t) {\\n      e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n    }\\n  }], [{\\n    key: \\\"setup\\\",\\n    value: function (t) {\\n      var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n        r = null;\\n      if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n      var i = _objectSpread2({}, defaults$1, {}, n);\\n      if (is$1.string(t) && i.watch) {\\n        var o = new MutationObserver(function (n) {\\n          Array.from(n).forEach(function (n) {\\n            Array.from(n.addedNodes).forEach(function (n) {\\n              is$1.element(n) && matches$1(n, t) && new e(n, i);\\n            });\\n          });\\n        });\\n        o.observe(document.body, {\\n          childList: !0,\\n          subtree: !0\\n        });\\n      }\\n      return r.map(function (t) {\\n        return new e(t, n);\\n      });\\n    }\\n  }, {\\n    key: \\\"enabled\\\",\\n    get: function () {\\n      return \\\"ontouchstart\\\" in document.documentElement;\\n    }\\n  }]), e;\\n}();\\n\\n// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = input => input === null || typeof input === 'undefined';\\nconst isObject = input => getConstructor(input) === Object;\\nconst isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = input => getConstructor(input) === String;\\nconst isBoolean = input => getConstructor(input) === Boolean;\\nconst isFunction = input => typeof input === 'function';\\nconst isArray = input => Array.isArray(input);\\nconst isWeakMap = input => instanceOf(input, WeakMap);\\nconst isNodeList = input => instanceOf(input, NodeList);\\nconst isTextNode = input => getConstructor(input) === Text;\\nconst isEvent = input => instanceOf(input, Event);\\nconst isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\nconst isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\nconst isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\nconst isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\nconst isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\nconst isUrl = input => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\nvar is = {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty\\n};\\n\\n// ==========================================================================\\nconst transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend'\\n  };\\n  const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nfunction repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\\n// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\nvar browser = {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos\\n};\\n\\n// ==========================================================================\\n\\n// Clone nested objects\\nfunction cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nfunction getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nfunction extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n  const source = sources.shift();\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n  Object.keys(source).forEach(key => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, {\\n          [key]: {}\\n        });\\n      }\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, {\\n        [key]: source[key]\\n      });\\n    }\\n  });\\n  return extend(target, ...sources);\\n}\\n\\n// ==========================================================================\\n\\n// Wrap an element\\nfunction wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets).reverse().forEach((element, index) => {\\n    const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n    // Cache the current parent and sibling.\\n    const parent = element.parentNode;\\n    const sibling = element.nextSibling;\\n\\n    // Wrap the element (is automatically removed from its current\\n    // parent).\\n    child.appendChild(element);\\n\\n    // If the element had a sibling, insert the wrapper before\\n    // the sibling to maintain the HTML structure; otherwise, just\\n    // append it to the parent.\\n    if (sibling) {\\n      parent.insertBefore(child, sibling);\\n    } else {\\n      parent.appendChild(child);\\n    }\\n  });\\n}\\n\\n// Set attributes\\nfunction setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nfunction createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nfunction insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nfunction insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nfunction removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nfunction emptyElement(element) {\\n  if (!is.element(element)) return;\\n  let {\\n    length\\n  } = element.childNodes;\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nfunction replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nfunction getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n  sel.split(',').forEach(s => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n        break;\\n    }\\n  });\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nfunction toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n  let hide = hidden;\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nfunction toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map(e => toggleClass(e, className, force));\\n  }\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n  return false;\\n}\\n\\n// Has class name\\nfunction hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nfunction matches(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n  const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nfunction closest$1(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n  const method = prototype.closest || closestElement;\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nfunction getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nfunction getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nfunction setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({\\n    preventScroll: true,\\n    focusVisible\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora'\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n    return {\\n      api,\\n      ui\\n    };\\n  },\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n    return false;\\n  })(),\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n};\\n\\n// ==========================================================================\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      }\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nfunction toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach(type => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({\\n        element,\\n        type,\\n        callback,\\n        options\\n      });\\n    }\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nfunction on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nfunction off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nfunction once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nfunction triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: {\\n      ...detail,\\n      plyr: this\\n    }\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nfunction unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach(item => {\\n      const {\\n        element,\\n        type,\\n        callback,\\n        options\\n      } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nfunction ready() {\\n  return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n}\\n\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nfunction silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Remove duplicates in an array\\nfunction dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nfunction closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n  return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n}\\n\\n// ==========================================================================\\n\\n// Check support for a CSS declaration\\nfunction supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n  ...out,\\n  [x / y]: [x, y]\\n}), {});\\n\\n// Validate an aspect ratio\\nfunction validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n  const ratio = is.array(input) ? input : input.split(':');\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nfunction reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n  const divider = getDivider(width, height);\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nfunction getAspectRatio(input) {\\n  const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({\\n      ratio\\n    } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const {\\n      videoWidth,\\n      videoHeight\\n    } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nfunction setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n  const {\\n    wrapper\\n  } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = 100 / x * y;\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n  return {\\n    padding,\\n    ratio\\n  };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nfunction roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nfunction getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\\n// ==========================================================================\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter(source => {\\n      const type = source.getAttribute('type');\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n      return support.mime.call(this, type);\\n    });\\n  },\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n  },\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const {\\n            currentTime,\\n            paused,\\n            preload,\\n            readyState,\\n            playbackRate\\n          } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input\\n        });\\n      }\\n    });\\n  },\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Generate a random ID\\nfunction generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nfunction format(input, ...args) {\\n  if (is.empty(input)) return input;\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nfunction getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n  return (current / max * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nconst replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nconst toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nfunction toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nfunction toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nfunction stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nfunction getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\\n// ==========================================================================\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube'\\n};\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n    let string = getDeep(config.i18n, key);\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n      return '';\\n    }\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title\\n    };\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n    return string;\\n  }\\n};\\n\\nclass Storage {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"get\\\", key => {\\n      if (!Storage.supported || !this.enabled) {\\n        return null;\\n      }\\n      const store = window.localStorage.getItem(this.key);\\n      if (is.empty(store)) {\\n        return null;\\n      }\\n      const json = JSON.parse(store);\\n      return is.string(key) && key.length ? json[key] : json;\\n    });\\n    _defineProperty$1(this, \\\"set\\\", object => {\\n      // Bail if we don't have localStorage support or it's disabled\\n      if (!Storage.supported || !this.enabled) {\\n        return;\\n      }\\n\\n      // Can only store objectst\\n      if (!is.object(object)) {\\n        return;\\n      }\\n\\n      // Get current storage\\n      let storage = this.get();\\n\\n      // Default to empty object\\n      if (is.empty(storage)) {\\n        storage = {};\\n      }\\n\\n      // Update the working copy of the values\\n      extend(storage, object);\\n\\n      // Update storage\\n      try {\\n        window.localStorage.setItem(this.key, JSON.stringify(storage));\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    });\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nfunction fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Load an external SVG sprite\\nfunction loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url).then(result => {\\n      if (is.empty(result)) {\\n        return;\\n      }\\n      if (useStorage) {\\n        try {\\n          window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n            content: result\\n          }));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      }\\n      update(container, result);\\n    }).catch(() => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Time helpers\\nconst getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\nconst getMinutes = value => Math.trunc(value / 60 % 60, 10);\\nconst getSeconds = value => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nfunction formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = value => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\\n// ==========================================================================\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n    return {\\n      url: this.config.iconUrl,\\n      cors\\n    };\\n  },\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume)\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration)\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n      return false;\\n    }\\n  },\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(icon, extend(attributes, {\\n      'aria-hidden': 'true',\\n      focusable: 'false'\\n    }));\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n    return icon;\\n  },\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = {\\n      ...attr,\\n      class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n    };\\n    return createElement('span', attributes, text);\\n  },\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value\\n    });\\n    badge.appendChild(createElement('span', {\\n      class: this.config.classNames.menu.badge\\n    }, text));\\n    return badge;\\n  },\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null\\n    };\\n    ['element', 'icon', 'label'].forEach(key => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n        class: 'icon--pressed'\\n      }));\\n      button.appendChild(controls.createIcon.call(this, props.icon, {\\n        class: 'icon--not-pressed'\\n      }));\\n\\n      // Label/Tooltip\\n      button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n        class: 'label--pressed'\\n      }));\\n      button.appendChild(controls.createLabel.call(this, props.label, {\\n        class: 'label--not-pressed'\\n      }));\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n    return button;\\n  },\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n      type: 'range',\\n      min: 0,\\n      max: 100,\\n      step: 0.01,\\n      value: 0,\\n      autocomplete: 'off',\\n      // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n      role: 'slider',\\n      'aria-label': i18n.get(type, this.config),\\n      'aria-valuemin': 0,\\n      'aria-valuemax': 100,\\n      'aria-valuenow': 0\\n    }, attributes));\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n    return input;\\n  },\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n      min: 0,\\n      max: 100,\\n      value: 0,\\n      role: 'progressbar',\\n      'aria-hidden': true\\n    }, attributes));\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered'\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n    this.elements.display[type] = progress;\\n    return progress;\\n  },\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n    const container = createElement('div', extend(attributes, {\\n      class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n      'aria-label': i18n.get(type, this.config),\\n      role: 'timer'\\n    }), '00:00');\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n    return container;\\n  },\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(this, menuItem, 'keydown keyup', event => {\\n      // We only care about space and ⬆️ ⬇️️ ➡️\\n      if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Prevent play / seek\\n      event.preventDefault();\\n      event.stopPropagation();\\n\\n      // We're just here to prevent the keydown bubbling\\n      if (event.type === 'keydown') {\\n        return;\\n      }\\n      const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n      // Show the respective menu\\n      if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n        controls.showMenuPanel.call(this, type, true);\\n      } else {\\n        let target;\\n        if (event.key !== ' ') {\\n          if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n            target = menuItem.nextElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.firstElementChild;\\n            }\\n          } else {\\n            target = menuItem.previousElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.lastElementChild;\\n            }\\n          }\\n          setFocus.call(this, target, true);\\n        }\\n      }\\n    }, false);\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', event => {\\n      if (event.key !== 'Return') return;\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n  // Create a settings menu item\\n  createMenuItem({\\n    value,\\n    list,\\n    type,\\n    title,\\n    badge = null,\\n    checked = false\\n  }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n    const menuItem = createElement('button', extend(attributes, {\\n      type: 'button',\\n      role: 'menuitemradio',\\n      class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n      'aria-checked': checked,\\n      value\\n    }));\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n        }\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      }\\n    });\\n    this.listeners.bind(menuItem, 'click keyup', event => {\\n      if (is.keyboardEvent(event) && event.key !== ' ') {\\n        return;\\n      }\\n      event.preventDefault();\\n      event.stopPropagation();\\n      menuItem.checked = true;\\n      switch (type) {\\n        case 'language':\\n          this.currentTrack = Number(value);\\n          break;\\n        case 'quality':\\n          this.quality = value;\\n          break;\\n        case 'speed':\\n          this.speed = parseFloat(value);\\n          break;\\n      }\\n      controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n    }, type, false);\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n    list.appendChild(menuItem);\\n  },\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n    return formatTime(time, forceHours, inverted);\\n  },\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n    let value = 0;\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n          break;\\n      }\\n    }\\n  },\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n  },\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    var _this$config$markers, _this$config$markers$;\\n    // Bail if setting not true\\n    if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n      return;\\n    }\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = show => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n    if (is.event(event)) {\\n      percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n    const time = this.duration / 100 * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n      time: t\\n    }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n          return label;\\n        }\\n        return toTitleCase(value);\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n      default:\\n        return null;\\n    }\\n  },\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = quality => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n      if (!label.length) {\\n        return null;\\n      }\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality.sort((a, b) => {\\n      const sorting = this.config.quality.options;\\n      return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n    }).forEach(quality => {\\n      controls.createMenuItem.call(this, {\\n        value: quality,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'quality', quality),\\n        badge: getBadge(quality)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n         const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n         // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n         // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n         // Empty the menu\\n        emptyElement(list);\\n         options.forEach(option => {\\n            const item = createElement('li');\\n             const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n             if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n             item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language'\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language'\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach(speed => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const {\\n      buttons\\n    } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n    let target = pane;\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n    }\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const {\\n      popup\\n    } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const {\\n      hidden\\n    } = popup;\\n    let show = hidden;\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n    return {\\n      width,\\n      height\\n    };\\n  },\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find(node => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = event => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = {\\n      class: 'plyr__controls__item'\\n    };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`\\n        });\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(createRange.call(this, 'seek', {\\n          id: `plyr-seek-${data.id}`\\n        }));\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement('span', {\\n            class: this.config.classNames.tooltip\\n          }, '00:00');\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let {\\n          volume\\n        } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__volume`.trim()\\n          }));\\n          this.elements.volume = volume;\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n            id: `plyr-volume-${data.id}`\\n          })));\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement('div', extend({}, defaultAttributes, {\\n          class: `${defaultAttributes.class} plyr__menu`.trim(),\\n          hidden: ''\\n        }));\\n        wrapper.appendChild(createButton.call(this, 'settings', {\\n          'aria-haspopup': true,\\n          'aria-controls': `plyr-settings-${data.id}`,\\n          'aria-expanded': false\\n        }));\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: ''\\n        });\\n        const inner = createElement('div');\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu'\\n        });\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach(type => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n            role: 'menuitem',\\n            'aria-haspopup': true,\\n            hidden: ''\\n          }));\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: ''\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(createElement('span', {\\n            'aria-hidden': true\\n          }, i18n.get(type, this.config)));\\n\\n          // Screen reader label\\n          backButton.appendChild(createElement('span', {\\n            class: this.config.classNames.hidden\\n          }, i18n.get('menuBack', this.config)));\\n\\n          // Go back via keyboard\\n          on.call(this, pane, 'keydown', event => {\\n            if (event.key !== 'ArrowLeft') return;\\n\\n            // Prevent seek\\n            event.preventDefault();\\n            event.stopPropagation();\\n\\n            // Show the respective menu\\n            showMenuPanel.call(this, 'home', true);\\n          }, false);\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(createElement('div', {\\n            role: 'menu'\\n          }));\\n          inner.appendChild(pane);\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank'\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n        const {\\n          download\\n        } = this.config.urls;\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider\\n          });\\n        }\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n    setSpeedMenu.call(this);\\n    return container;\\n  },\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this)\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = input => {\\n      let result = input;\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = button => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          }\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n        if (is.array(button) || is.nodeList(button)) {\\n          Array.from(button).filter(Boolean).forEach(addProperty);\\n        } else {\\n          addProperty(button);\\n        }\\n      });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const {\\n        classNames,\\n        selectors\\n      } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n      Array.from(labels).forEach(label => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n  // Add markers\\n  setMarkers() {\\n    var _this$config$markers2, _this$config$markers3;\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n      time\\n    }) => time > 0 && time < this.duration);\\n    if (!(points !== null && points !== void 0 && points.length)) return;\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach(point => {\\n      const markerElement = createElement('span', {\\n        class: this.config.classNames.marker\\n      }, '');\\n      const left = `${point.time / this.duration * 100}%`;\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement('span', {\\n        class: this.config.classNames.tooltip\\n      }, '');\\n      containerFragment.appendChild(tipElement);\\n    }\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement\\n    };\\n    this.elements.progress.appendChild(containerFragment);\\n  }\\n};\\n\\n// ==========================================================================\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nfunction parseUrl(input, safe = true) {\\n  let url = input;\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nfunction buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n  return params;\\n}\\n\\n// ==========================================================================\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n      // Clear menu and hide\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n      Array.from(elements).forEach(track => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n        if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n          fetch(src, 'blob').then(blob => {\\n            track.setAttribute('src', window.URL.createObjectURL(blob));\\n          }).catch(() => {\\n            removeElement(track);\\n          });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({\\n        active\\n      } = this.config.captions);\\n    }\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const {\\n      active,\\n      language,\\n      meta,\\n      currentTrackNode\\n    } = this.captions;\\n    const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks.filter(track => !meta.get(track)).forEach(track => {\\n        this.debug.log('Track added', track);\\n\\n        // Attempt to store if the original dom element was \\\"default\\\"\\n        meta.set(track, {\\n          default: track.mode === 'showing'\\n        });\\n\\n        // Turn off native caption rendering to avoid double captions\\n        // Note: mode='hidden' forces a track to download. To ensure every track\\n        // isn't downloaded at once, only 'showing' tracks should be reassigned\\n        // eslint-disable-next-line no-param-reassign\\n        if (track.mode === 'showing') {\\n          // eslint-disable-next-line no-param-reassign\\n          track.mode = 'hidden';\\n        }\\n\\n        // Add event listener for cue changes\\n        on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n      });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    const {\\n      toggled\\n    } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({\\n          captions: active\\n        });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const {\\n        language\\n      } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({\\n          language\\n        });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n    languages.every(language => {\\n      track = sorted.find(t => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n      return i18n.get('enabled', this.config);\\n    }\\n    return i18n.get('disabled', this.config);\\n  },\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n      cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n  // Custom media title\\n  title: '',\\n  // Logging to console\\n  debug: false,\\n  // Auto play (if supported)\\n  autoplay: false,\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n  // Pass a custom duration\\n  duration: null,\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n  // Auto hide the controls\\n  hideControls: true,\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null\\n  },\\n  // Set loops\\n  loop: {\\n    active: false\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n  },\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false\\n  },\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true\\n  },\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false\\n  },\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true,\\n    // Allow fullscreen?\\n    fallback: true,\\n    // Fallback using full viewport/window\\n    iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr'\\n  },\\n  // Default controls\\n  controls: ['play-large',\\n  // 'restart',\\n  // 'rewind',\\n  'play',\\n  // 'fast-forward',\\n  'progress', 'current-time',\\n  // 'duration',\\n  'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n  // 'download',\\n  'fullscreen'],\\n  settings: ['captions', 'quality', 'speed'],\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD'\\n    }\\n  },\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}'\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\\n    }\\n  },\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null\\n  },\\n  // Events to watch and bubble\\n  events: [\\n  // Events to watch on HTML5 media elements and bubble\\n  // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n  'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n  // Custom events\\n  'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n  // YouTube\\n  'statechange',\\n  // Quality\\n  'qualitychange',\\n  // Ads\\n  'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls'\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]'\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]'\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop',\\n      // Used later\\n      volume: '.plyr__volume--display'\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption'\\n  },\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time'\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open'\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active'\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback'\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active'\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active'\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n    }\\n  },\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash'\\n    }\\n  },\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: ''\\n  },\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: ''\\n  },\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null,\\n    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false\\n  },\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0,\\n    // No related vids\\n    showinfo: 0,\\n    // Hide info\\n    iv_load_policy: 3,\\n    // Hide annotations\\n    modestbranding: 1,\\n    // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: []\\n  },\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: []\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nconst pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline'\\n};\\n\\n// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nconst providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo'\\n};\\nconst types = {\\n  audio: 'audio',\\n  video: 'video'\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nfunction getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n  return null;\\n}\\n\\n// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\nclass Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"onChange\\\", () => {\\n      if (!this.supported) return;\\n\\n      // Update toggle button\\n      const button = this.player.elements.buttons.fullscreen;\\n      if (is.element(button)) {\\n        button.pressed = this.active;\\n      }\\n\\n      // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n      const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n      // Trigger an event\\n      triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n    });\\n    _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n      // Store or restore scroll position\\n      if (toggle) {\\n        this.scrollPosition = {\\n          x: window.scrollX ?? 0,\\n          y: window.scrollY ?? 0\\n        };\\n      } else {\\n        window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n      }\\n\\n      // Toggle scroll\\n      document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n      // Toggle class hook\\n      toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n      // Force full viewport on iPhone X+\\n      if (browser.isIos) {\\n        let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n        const property = 'viewport-fit=cover';\\n\\n        // Inject the viewport meta if required\\n        if (!viewport) {\\n          viewport = document.createElement('meta');\\n          viewport.setAttribute('name', 'viewport');\\n        }\\n\\n        // Check if the property already exists\\n        const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n        if (toggle) {\\n          this.cleanupViewport = !hasProperty;\\n          if (!hasProperty) viewport.content += `,${property}`;\\n        } else if (this.cleanupViewport) {\\n          viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n        }\\n      }\\n\\n      // Toggle button and fire events\\n      this.onChange();\\n    });\\n    // Trap focus inside container\\n    _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n      // Bail if iOS/iPadOS, not active, not the tab key\\n      if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n      // Get the current focused element\\n      const focused = document.activeElement;\\n      const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n      const [first] = focusable;\\n      const last = focusable[focusable.length - 1];\\n      if (focused === last && !event.shiftKey) {\\n        // Move focus to first element that can be tabbed if Shift isn't used\\n        first.focus();\\n        event.preventDefault();\\n      } else if (focused === first && event.shiftKey) {\\n        // Move focus to last element that can be tabbed if Shift is used\\n        last.focus();\\n        event.preventDefault();\\n      }\\n    });\\n    // Update UI\\n    _defineProperty$1(this, \\\"update\\\", () => {\\n      if (this.supported) {\\n        let mode;\\n        if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n        this.player.debug.log(`${mode} fullscreen enabled`);\\n      } else {\\n        this.player.debug.log('Fullscreen not supported and fallback disabled');\\n      }\\n\\n      // Add styling hook to show button\\n      toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n    });\\n    // Make an element fullscreen\\n    _defineProperty$1(this, \\\"enter\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen doesn't need the request step\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.requestFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(true);\\n      } else if (!this.prefix) {\\n        this.target.requestFullscreen({\\n          navigationUI: 'hide'\\n        });\\n      } else if (!is.empty(this.prefix)) {\\n        this.target[`${this.prefix}Request${this.property}`]();\\n      }\\n    });\\n    // Bail from fullscreen\\n    _defineProperty$1(this, \\\"exit\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.exitFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n        silencePromise(this.player.play());\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(false);\\n      } else if (!this.prefix) {\\n        (document.cancelFullScreen || document.exitFullscreen).call(document);\\n      } else if (!is.empty(this.prefix)) {\\n        const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n        document[`${this.prefix}${action}${this.property}`]();\\n      }\\n    });\\n    // Toggle state\\n    _defineProperty$1(this, \\\"toggle\\\", () => {\\n      if (!this.active) this.enter();else this.exit();\\n    });\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = {\\n      x: 0,\\n      y: 0\\n    };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n      // TODO: Filter for target??\\n      this.onChange();\\n    });\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n    prefixes.some(pre => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n      return false;\\n    });\\n    return value;\\n  }\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n    // Fullscreen is enabled in config\\n    this.player.config.fullscreen.enabled,\\n    // Must be a video\\n    this.player.isVideo,\\n    // Either native is supported or fallback enabled\\n    Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n    // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n    // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n    !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n    const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n}\\n\\n// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nfunction loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n    Object.assign(image, {\\n      onload: handler,\\n      onerror: handler,\\n      src\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach(button => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return ready.call(this)\\n    // Load image\\n    .then(() => loadImage(poster)).catch(error => {\\n      // Hide poster on error unless it's been set by another call\\n      if (poster === this.poster) {\\n        ui.togglePoster.call(this, false);\\n      }\\n      // Rethrow\\n      throw error;\\n    }).then(() => {\\n      // Prevent race conditions\\n      if (poster !== this.poster) {\\n        throw new Error('setPoster cancelled by later call to setPoster');\\n      }\\n    }).then(() => {\\n      Object.assign(this.elements.poster.style, {\\n        backgroundImage: `url('${poster}')`,\\n        // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n        backgroundSize: ''\\n      });\\n      ui.togglePoster.call(this, true);\\n      return poster;\\n    });\\n  },\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach(target => {\\n      Object.assign(target, {\\n        pressed: this.playing\\n      });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(() => {\\n      // Update progress bar loading class state\\n      toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n      // Update controls visibility\\n      ui.toggleControls.call(this);\\n    }, this.loading ? 250 : 0);\\n  },\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const {\\n      controls: controlsElement\\n    } = this.elements;\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n    }\\n  },\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({\\n      ...this.media.style\\n    })\\n    // We're only fussed about Plyr specific properties\\n    .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n      // Set on the container\\n      this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n      // Clean up from media element\\n      this.media.style.removeProperty(key);\\n    });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  }\\n};\\n\\nclass Listeners {\\n  constructor(_player) {\\n    // Device is touch enabled\\n    _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      player.touch = true;\\n\\n      // Add touch class\\n      toggleClass(elements.container, player.config.classNames.isTouch, true);\\n    });\\n    // Global window & document listeners\\n    _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n      const {\\n        player\\n      } = this;\\n\\n      // Keyboard shortcuts\\n      if (player.config.keyboard.global) {\\n        toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n      }\\n\\n      // Click anywhere closes menu\\n      toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n      // Detect touch by events\\n      once.call(player, document.body, 'touchstart', this.firstTouch);\\n    });\\n    // Container listeners\\n    _defineProperty$1(this, \\\"container\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        config,\\n        elements,\\n        timers\\n      } = player;\\n\\n      // Keyboard shortcuts\\n      if (!config.keyboard.global && config.keyboard.focused) {\\n        on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n      }\\n\\n      // Toggle controls on mouse events and entering fullscreen\\n      on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n        const {\\n          controls: controlsElement\\n        } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Set a gutter for Vimeo\\n      const setGutter = () => {\\n        if (!player.isVimeo || player.config.vimeo.premium) {\\n          return;\\n        }\\n        const target = elements.wrapper;\\n        const {\\n          active\\n        } = player.fullscreen;\\n        const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n        const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n        // If not active, remove styles\\n        if (!active) {\\n          if (useNativeAspectRatio) {\\n            target.style.width = null;\\n            target.style.height = null;\\n          } else {\\n            target.style.maxWidth = null;\\n            target.style.margin = null;\\n          }\\n          return;\\n        }\\n\\n        // Determine which dimension will overflow and constrain view\\n        const [viewportWidth, viewportHeight] = getViewportSize();\\n        const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n        if (useNativeAspectRatio) {\\n          target.style.width = overflow ? 'auto' : '100%';\\n          target.style.height = overflow ? '100%' : 'auto';\\n        } else {\\n          target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n          target.style.margin = overflow ? '0 auto' : null;\\n        }\\n      };\\n\\n      // Handle resizing\\n      const resized = () => {\\n        clearTimeout(timers.resized);\\n        timers.resized = setTimeout(setGutter, 50);\\n      };\\n      on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n        const {\\n          target\\n        } = player.fullscreen;\\n\\n        // Ignore events not from target\\n        if (target !== elements.container) {\\n          return;\\n        }\\n\\n        // If it's not an embed and no ratio specified\\n        if (!player.isEmbed && is.empty(player.config.ratio)) {\\n          return;\\n        }\\n\\n        // Set Vimeo gutter\\n        setGutter();\\n\\n        // Watch for resizes\\n        const method = event.type === 'enterfullscreen' ? on : off;\\n        method.call(player, window, 'resize', resized);\\n      });\\n    });\\n    // Listen for media events\\n    _defineProperty$1(this, \\\"media\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n\\n      // Time change on media\\n      on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n      // Display duration\\n      on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n      // Handle the media finishing\\n      on.call(player, player.media, 'ended', () => {\\n        // Show poster on end\\n        if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n          // Restart\\n          player.restart();\\n\\n          // Call pause otherwise IE11 will start playing the video again\\n          player.pause();\\n        }\\n      });\\n\\n      // Check for buffer progress\\n      on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n      // Handle volume changes\\n      on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n      // Handle play/pause\\n      on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n      // Loading state\\n      on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n      // Click video\\n      if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n        // Re-fetch the wrapper\\n        const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n        // Bail if there's no wrapper (this should never happen)\\n        if (!is.element(wrapper)) {\\n          return;\\n        }\\n\\n        // On click play, pause or restart\\n        on.call(player, elements.container, 'click', event => {\\n          const targets = [elements.container, wrapper];\\n\\n          // Ignore if click if not container or in video wrapper\\n          if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n            return;\\n          }\\n\\n          // Touch devices will just show controls (if hidden)\\n          if (player.touch && player.config.hideControls) {\\n            return;\\n          }\\n          if (player.ended) {\\n            this.proxy(event, player.restart, 'restart');\\n            this.proxy(event, () => {\\n              silencePromise(player.play());\\n            }, 'play');\\n          } else {\\n            this.proxy(event, () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          }\\n        });\\n      }\\n\\n      // Disable right click\\n      if (player.supported.ui && player.config.disableContextMenu) {\\n        on.call(player, elements.wrapper, 'contextmenu', event => {\\n          event.preventDefault();\\n        }, false);\\n      }\\n\\n      // Volume change\\n      on.call(player, player.media, 'volumechange', () => {\\n        // Save to storage\\n        player.storage.set({\\n          volume: player.volume,\\n          muted: player.muted\\n        });\\n      });\\n\\n      // Speed change\\n      on.call(player, player.media, 'ratechange', () => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'speed');\\n\\n        // Save to storage\\n        player.storage.set({\\n          speed: player.speed\\n        });\\n      });\\n\\n      // Quality change\\n      on.call(player, player.media, 'qualitychange', event => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n      });\\n\\n      // Update download link when ready and if quality changes\\n      on.call(player, player.media, 'ready qualitychange', () => {\\n        controls.setDownloadUrl.call(player);\\n      });\\n\\n      // Proxy events to container\\n      // Bubble up key events for Edge\\n      const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n      on.call(player, player.media, proxyEvents, event => {\\n        let {\\n          detail = {}\\n        } = event;\\n\\n        // Get error details from media\\n        if (event.type === 'error') {\\n          detail = player.media.error;\\n        }\\n        triggerEvent.call(player, elements.container, event.type, true, detail);\\n      });\\n    });\\n    // Run default and custom handlers\\n    _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      let returned = true;\\n\\n      // Execute custom handler\\n      if (hasCustomHandler) {\\n        returned = customHandler.call(player, event);\\n      }\\n\\n      // Only call default handler if not prevented in custom handler\\n      if (returned !== false && is.function(defaultHandler)) {\\n        defaultHandler.call(player, event);\\n      }\\n    });\\n    // Trigger custom and default handlers\\n    _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n    });\\n    // Listen for control events\\n    _defineProperty$1(this, \\\"controls\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      // IE doesn't support input event, so we fallback to change\\n      const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n      // Play/pause toggle\\n      if (elements.buttons.play) {\\n        Array.from(elements.buttons.play).forEach(button => {\\n          this.bind(button, 'click', () => {\\n            silencePromise(player.togglePlay());\\n          }, 'play');\\n        });\\n      }\\n\\n      // Pause\\n      this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n      // Rewind\\n      this.bind(elements.buttons.rewind, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      }, 'rewind');\\n\\n      // Rewind\\n      this.bind(elements.buttons.fastForward, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      }, 'fastForward');\\n\\n      // Mute toggle\\n      this.bind(elements.buttons.mute, 'click', () => {\\n        player.muted = !player.muted;\\n      }, 'mute');\\n\\n      // Captions toggle\\n      this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n      // Download\\n      this.bind(elements.buttons.download, 'click', () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      }, 'download');\\n\\n      // Fullscreen toggle\\n      this.bind(elements.buttons.fullscreen, 'click', () => {\\n        player.fullscreen.toggle();\\n      }, 'fullscreen');\\n\\n      // Picture-in-Picture\\n      this.bind(elements.buttons.pip, 'click', () => {\\n        player.pip = 'toggle';\\n      }, 'pip');\\n\\n      // Airplay\\n      this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n      // Settings menu - click toggle\\n      this.bind(elements.buttons.settings, 'click', event => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n        controls.toggleMenu.call(player, event);\\n      }, null, false); // Can't be passive as we're preventing default\\n\\n      // Settings menu - keyboard toggle\\n      // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n      // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n      this.bind(elements.buttons.settings, 'keyup', event => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      }, null, false // Can't be passive as we're preventing default\\n      );\\n\\n      // Escape closes menu\\n      this.bind(elements.settings.menu, 'keydown', event => {\\n        if (event.key === 'Escape') {\\n          controls.toggleMenu.call(player, event);\\n        }\\n      });\\n\\n      // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n      this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n        const rect = elements.progress.getBoundingClientRect();\\n        const percent = 100 / rect.width * (event.pageX - rect.left);\\n        event.currentTarget.setAttribute('seek-value', percent);\\n      });\\n\\n      // Pause while seeking\\n      this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n        const seek = event.currentTarget;\\n        const attribute = 'play-on-seeked';\\n        if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Record seek time so we can prevent hiding controls for a few seconds after seek\\n        player.lastSeekTime = Date.now();\\n\\n        // Was playing before?\\n        const play = seek.hasAttribute(attribute);\\n        // Done seeking\\n        const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n        // If we're done seeking and it was playing, resume playback\\n        if (play && done) {\\n          seek.removeAttribute(attribute);\\n          silencePromise(player.play());\\n        } else if (!done && player.playing) {\\n          seek.setAttribute(attribute, '');\\n          player.pause();\\n        }\\n      });\\n\\n      // Fix range inputs on iOS\\n      // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n      // it takes over further interactions on the page. This is a hack\\n      if (browser.isIos) {\\n        const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n        Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n      }\\n\\n      // Seek\\n      this.bind(elements.inputs.seek, inputEvent, event => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n        seek.removeAttribute('seek-value');\\n        player.currentTime = seekTo / seek.max * player.duration;\\n      }, 'seek');\\n\\n      // Seek tooltip\\n      this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n      // Preview thumbnails plugin\\n      // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n      this.bind(elements.progress, 'mousemove touchmove', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startMove(event);\\n        }\\n      });\\n\\n      // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n      this.bind(elements.progress, 'mouseleave touchend click', () => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endMove(false, true);\\n        }\\n      });\\n\\n      // Show scrubbing preview\\n      this.bind(elements.progress, 'mousedown touchstart', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startScrubbing(event);\\n        }\\n      });\\n      this.bind(elements.progress, 'mouseup touchend', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endScrubbing(event);\\n        }\\n      });\\n\\n      // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n      if (browser.isWebKit) {\\n        Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n          this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n        });\\n      }\\n\\n      // Current time invert\\n      // Only if one time element is used for both currentTime and duration\\n      if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n        this.bind(elements.display.currentTime, 'click', () => {\\n          // Do nothing if we're at the start\\n          if (player.currentTime === 0) {\\n            return;\\n          }\\n          player.config.invertTime = !player.config.invertTime;\\n          controls.timeUpdate.call(player);\\n        });\\n      }\\n\\n      // Volume\\n      this.bind(elements.inputs.volume, inputEvent, event => {\\n        player.volume = event.target.value;\\n      }, 'volume');\\n\\n      // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n        elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n      });\\n\\n      // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n      if (elements.fullscreen) {\\n        Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n          this.bind(child, 'mouseenter mouseleave', event => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n      }\\n\\n      // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n      });\\n\\n      // Show controls when they receive focus (e.g., when using keyboard tab key)\\n      this.bind(elements.controls, 'focusin', () => {\\n        const {\\n          config,\\n          timers\\n        } = player;\\n\\n        // Skip transition to prevent focus from scrolling the parent element\\n        toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n        // Toggle\\n        ui.toggleControls.call(player, true);\\n\\n        // Restore transition\\n        setTimeout(() => {\\n          toggleClass(elements.controls, config.classNames.noTransition, false);\\n        }, 0);\\n\\n        // Delay a little more for mouse users\\n        const delay = this.touch ? 3000 : 4000;\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Hide again after delay\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Mouse wheel for volume\\n      this.bind(elements.inputs.volume, 'wheel', event => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const {\\n          volume\\n        } = player.media;\\n        if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n          event.preventDefault();\\n        }\\n      }, 'volume', false);\\n    });\\n    this.player = _player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const {\\n      player\\n    } = this;\\n    const {\\n      elements\\n    } = player;\\n    const {\\n      key,\\n      type,\\n      altKey,\\n      ctrlKey,\\n      metaKey,\\n      shiftKey\\n    } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = increment => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = player.duration / 10 * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const {\\n          editable\\n        } = player.config.selectors;\\n        const {\\n          seek\\n        } = elements.inputs;\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n}\\n\\nvar commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\nfunction createCommonjsModule(fn, module) {\\n\\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n}\\n\\nvar loadjs_umd = createCommonjsModule(function (module, exports) {\\n  (function (root, factory) {\\n    {\\n      module.exports = factory();\\n    }\\n  })(commonjsGlobal, function () {\\n    /**\\n     * Global dependencies.\\n     * @global {Object} document - DOM\\n     */\\n\\n    var devnull = function () {},\\n      bundleIdCache = {},\\n      bundleResultCache = {},\\n      bundleCallbackQueue = {};\\n\\n    /**\\n     * Subscribe to bundle load event.\\n     * @param {string[]} bundleIds - Bundle ids\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function subscribe(bundleIds, callbackFn) {\\n      // listify\\n      bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n      var depsNotFound = [],\\n        i = bundleIds.length,\\n        numWaiting = i,\\n        fn,\\n        bundleId,\\n        r,\\n        q;\\n\\n      // define callback function\\n      fn = function (bundleId, pathsNotFound) {\\n        if (pathsNotFound.length) depsNotFound.push(bundleId);\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(depsNotFound);\\n      };\\n\\n      // register callback\\n      while (i--) {\\n        bundleId = bundleIds[i];\\n\\n        // execute callback if in result cache\\n        r = bundleResultCache[bundleId];\\n        if (r) {\\n          fn(bundleId, r);\\n          continue;\\n        }\\n\\n        // add to callback queue\\n        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n        q.push(fn);\\n      }\\n    }\\n\\n    /**\\n     * Publish bundle load event.\\n     * @param {string} bundleId - Bundle id\\n     * @param {string[]} pathsNotFound - List of files not found\\n     */\\n    function publish(bundleId, pathsNotFound) {\\n      // exit if id isn't defined\\n      if (!bundleId) return;\\n      var q = bundleCallbackQueue[bundleId];\\n\\n      // cache result\\n      bundleResultCache[bundleId] = pathsNotFound;\\n\\n      // exit if queue is empty\\n      if (!q) return;\\n\\n      // empty callback queue\\n      while (q.length) {\\n        q[0](bundleId, pathsNotFound);\\n        q.splice(0, 1);\\n      }\\n    }\\n\\n    /**\\n     * Execute callbacks.\\n     * @param {Object or Function} args - The callback args\\n     * @param {string[]} depsNotFound - List of dependencies not found\\n     */\\n    function executeCallbacks(args, depsNotFound) {\\n      // accept function as argument\\n      if (args.call) args = {\\n        success: args\\n      };\\n\\n      // success and error callbacks\\n      if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n    }\\n\\n    /**\\n     * Load individual file.\\n     * @param {string} path - The file path\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFile(path, callbackFn, args, numTries) {\\n      var doc = document,\\n        async = args.async,\\n        maxTries = (args.numRetries || 0) + 1,\\n        beforeCallbackFn = args.before || devnull,\\n        pathname = path.replace(/[\\\\?|#].*$/, ''),\\n        pathStripped = path.replace(/^(css|img)!/, ''),\\n        isLegacyIECss,\\n        e;\\n      numTries = numTries || 0;\\n      if (/(^css!|\\\\.css$)/.test(pathname)) {\\n        // css\\n        e = doc.createElement('link');\\n        e.rel = 'stylesheet';\\n        e.href = pathStripped;\\n\\n        // tag IE9+\\n        isLegacyIECss = 'hideFocus' in e;\\n\\n        // use preload in IE Edge (to detect load errors)\\n        if (isLegacyIECss && e.relList) {\\n          isLegacyIECss = 0;\\n          e.rel = 'preload';\\n          e.as = 'style';\\n        }\\n      } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n        // image\\n        e = doc.createElement('img');\\n        e.src = pathStripped;\\n      } else {\\n        // javascript\\n        e = doc.createElement('script');\\n        e.src = path;\\n        e.async = async === undefined ? true : async;\\n      }\\n      e.onload = e.onerror = e.onbeforeload = function (ev) {\\n        var result = ev.type[0];\\n\\n        // treat empty stylesheets as failures to get around lack of onerror\\n        // support in IE9-11\\n        if (isLegacyIECss) {\\n          try {\\n            if (!e.sheet.cssText.length) result = 'e';\\n          } catch (x) {\\n            // sheets objects created from load errors don't allow access to\\n            // `cssText` (unless error is Code:18 SecurityError)\\n            if (x.code != 18) result = 'e';\\n          }\\n        }\\n\\n        // handle retries in case of load failure\\n        if (result == 'e') {\\n          // increment counter\\n          numTries += 1;\\n\\n          // exit function and try again\\n          if (numTries < maxTries) {\\n            return loadFile(path, callbackFn, args, numTries);\\n          }\\n        } else if (e.rel == 'preload' && e.as == 'style') {\\n          // activate preloaded stylesheets\\n          return e.rel = 'stylesheet'; // jshint ignore:line\\n        }\\n\\n        // execute callback\\n        callbackFn(path, result, ev.defaultPrevented);\\n      };\\n\\n      // add to document (unless callback returns `false`)\\n      if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n    }\\n\\n    /**\\n     * Load multiple files.\\n     * @param {string[]} paths - The file paths\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFiles(paths, callbackFn, args) {\\n      // listify paths\\n      paths = paths.push ? paths : [paths];\\n      var numWaiting = paths.length,\\n        x = numWaiting,\\n        pathsNotFound = [],\\n        fn,\\n        i;\\n\\n      // define callback function\\n      fn = function (path, result, defaultPrevented) {\\n        // handle error\\n        if (result == 'e') pathsNotFound.push(path);\\n\\n        // handle beforeload event. If defaultPrevented then that means the load\\n        // will be blocked (ex. Ghostery/ABP on Safari)\\n        if (result == 'b') {\\n          if (defaultPrevented) pathsNotFound.push(path);else return;\\n        }\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(pathsNotFound);\\n      };\\n\\n      // load scripts\\n      for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n    }\\n\\n    /**\\n     * Initiate script load and register bundle.\\n     * @param {(string|string[])} paths - The file paths\\n     * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n     *   callback or (3) object literal with success/error arguments, numRetries,\\n     *   etc.\\n     * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n     *   literal with success/error arguments, numRetries, etc.\\n     */\\n    function loadjs(paths, arg1, arg2) {\\n      var bundleId, args;\\n\\n      // bundleId (if string)\\n      if (arg1 && arg1.trim) bundleId = arg1;\\n\\n      // args (default is {})\\n      args = (bundleId ? arg2 : arg1) || {};\\n\\n      // throw error if bundle is already defined\\n      if (bundleId) {\\n        if (bundleId in bundleIdCache) {\\n          throw \\\"LoadJS\\\";\\n        } else {\\n          bundleIdCache[bundleId] = true;\\n        }\\n      }\\n      function loadFn(resolve, reject) {\\n        loadFiles(paths, function (pathsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, pathsNotFound);\\n\\n          // resolve Promise\\n          if (resolve) {\\n            executeCallbacks({\\n              success: resolve,\\n              error: reject\\n            }, pathsNotFound);\\n          }\\n\\n          // publish bundle load event\\n          publish(bundleId, pathsNotFound);\\n        }, args);\\n      }\\n      if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n    }\\n\\n    /**\\n     * Execute callbacks when dependencies have been satisfied.\\n     * @param {(string|string[])} deps - List of bundle ids\\n     * @param {Object} args - success/error arguments\\n     */\\n    loadjs.ready = function ready(deps, args) {\\n      // subscribe to bundle load event\\n      subscribe(deps, function (depsNotFound) {\\n        // execute callbacks\\n        executeCallbacks(args, depsNotFound);\\n      });\\n      return loadjs;\\n    };\\n\\n    /**\\n     * Manually satisfy bundle dependencies.\\n     * @param {string} bundleId - The bundle id\\n     */\\n    loadjs.done = function done(bundleId) {\\n      publish(bundleId, []);\\n    };\\n\\n    /**\\n     * Reset loadjs dependencies statuses\\n     */\\n    loadjs.reset = function reset() {\\n      bundleIdCache = {};\\n      bundleResultCache = {};\\n      bundleCallbackQueue = {};\\n    };\\n\\n    /**\\n     * Determine if bundle has already been defined\\n     * @param String} bundleId - The bundle id\\n     */\\n    loadjs.isDefined = function isDefined(bundleId) {\\n      return bundleId in bundleIdCache;\\n    };\\n\\n    // export\\n    return loadjs;\\n  });\\n});\\n\\n// ==========================================================================\\nfunction loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs_umd(url, {\\n      success: resolve,\\n      error: reject\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Parse Vimeo ID from URL\\nfunction parseId$1(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState$1(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk).then(() => {\\n        vimeo.ready.call(player);\\n      }).catch(error => {\\n        player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n      });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const {\\n      premium,\\n      referrerPolicy,\\n      ...frameParams\\n    } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? {\\n      h: hash\\n    } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams\\n    });\\n    const id = parseId$1(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted\\n    });\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState$1.call(player, true);\\n      return player.embed.play();\\n    };\\n    player.media.pause = () => {\\n      assurePlaybackState$1.call(player, false);\\n      return player.embed.pause();\\n    };\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let {\\n      currentTime\\n    } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const {\\n          embed,\\n          media,\\n          paused,\\n          volume\\n        } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n        // Seek\\n        .then(() => embed.setCurrentTime(time))\\n        // Restore paused\\n        .then(() => restorePause && embed.pause())\\n        // Restore volume\\n        .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n          // Do nothing\\n        });\\n      }\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed.setPlaybackRate(input).then(() => {\\n          speed = input;\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        }).catch(() => {\\n          // Cannot set Playback Rate, Video is probably not on Pro account\\n          player.options.speed = [1];\\n        });\\n      }\\n    });\\n\\n    // Volume\\n    let {\\n      volume\\n    } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Muted\\n    let {\\n      muted\\n    } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Loop\\n    let {\\n      loop\\n    } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      }\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed.getVideoUrl().then(value => {\\n      currentSrc = value;\\n      controls.setDownloadUrl.call(player);\\n    }).catch(error => {\\n      this.debug.warn(error);\\n    });\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      }\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      }\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then(state => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then(title => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then(value => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then(value => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then(tracks => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n    player.embed.on('cuechange', ({\\n      cues = []\\n    }) => {\\n      const strippedCues = cues.map(cue => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then(paused => {\\n        assurePlaybackState$1.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('play', () => {\\n      assurePlaybackState$1.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('pause', () => {\\n      assurePlaybackState$1.call(player, false);\\n    });\\n    player.embed.on('timeupdate', data => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n    player.embed.on('progress', data => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then(value => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n    player.embed.on('error', detail => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch(error => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n    fetch(url).then(data => {\\n      if (is.object(data)) {\\n        const {\\n          title,\\n          height,\\n          width\\n        } = data;\\n\\n        // Set title\\n        this.config.title = title;\\n        ui.setTitle.call(this);\\n\\n        // Set aspect ratio\\n        this.embed.ratio = roundAspectRatio(width, height);\\n      }\\n      setAspectRatio.call(this);\\n    }).catch(() => {\\n      // Set aspect ratio\\n      setAspectRatio.call(this);\\n    });\\n  },\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', {\\n      id,\\n      'data-poster': config.customControls ? player.poster : undefined\\n    });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n      .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n      .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n      .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n        // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n        if (!src.includes('maxres')) {\\n          player.elements.poster.style.backgroundSize = 'cover';\\n        }\\n      }).catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend({}, {\\n        // Autoplay\\n        autoplay: player.config.autoplay ? 1 : 0,\\n        // iframe interface language\\n        hl: player.config.hl,\\n        // Only show controls if not fully supported or opted out\\n        controls: player.supported.ui && config.customControls ? 0 : 1,\\n        // Disable keyboard as we handle it\\n        disablekb: 1,\\n        // Allow iOS inline playback\\n        playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n        // Captions are flaky on YouTube\\n        cc_load_policy: player.captions.active ? 1 : 0,\\n        cc_lang_pref: player.config.captions.language,\\n        // Tracking for stats\\n        widget_referrer: window ? window.location.href : null\\n      }, config),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message = {\\n              2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n              5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n              100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n              101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n            }[code] || 'An unknown error occurred';\\n            player.media.error = {\\n              code,\\n              message\\n            };\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            }\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            }\\n          });\\n\\n          // Volume\\n          let {\\n            volume\\n          } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Muted\\n          let {\\n            muted\\n          } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            }\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            }\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n              break;\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n              break;\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n              break;\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n              break;\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n              break;\\n          }\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data\\n          });\\n        }\\n      }\\n    });\\n  }\\n};\\n\\n// ==========================================================================\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster\\n      });\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  }\\n};\\n\\nconst destroy = instance => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n  instance.elements.container.remove();\\n};\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    /**\\n     * Load the IMA SDK\\n     */\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Check if the Google IMA3 SDK is loaded or load it ourselves\\n      if (!is.object(window.google) || !is.object(window.google.ima)) {\\n        loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n          this.ready();\\n        }).catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n      } else {\\n        this.ready();\\n      }\\n    });\\n    /**\\n     * Get the ads instance ready\\n     */\\n    _defineProperty$1(this, \\\"ready\\\", () => {\\n      // Double check we're enabled\\n      if (!this.enabled) {\\n        destroy(this);\\n      }\\n\\n      // Start ticking our safety timer. If the whole advertisement\\n      // thing doesn't resolve within our set time; we bail\\n      this.startSafetyTimer(12000, 'ready()');\\n\\n      // Clear the safety timer\\n      this.managerPromise.then(() => {\\n        this.clearSafetyTimer('onAdsManagerLoaded()');\\n      });\\n\\n      // Set listeners on the Plyr instance\\n      this.listeners();\\n\\n      // Setup the IMA SDK\\n      this.setupIMA();\\n    });\\n    /**\\n     * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n     * so here we define our ad container. This div is set up to render on top of the video player.\\n     * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n     * handle to the content video player - the SDK will poll the current time of our player to\\n     * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n     * mobile devices, this initialization is done as the result of a user action.\\n     */\\n    _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n      // Create the container for our advertisements\\n      this.elements.container = createElement('div', {\\n        class: this.player.config.classNames.ads\\n      });\\n      this.player.elements.container.appendChild(this.elements.container);\\n\\n      // So we can run VPAID2\\n      google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n      // Set language\\n      google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n      // Set playback for iOS10+\\n      google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n      // We assume the adContainer is the video container of the plyr element that will house the ads\\n      this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n      // Create ads loader\\n      this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n      // Listen and respond to ads loaded and error events\\n      this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n      this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n      // Request video ads to be pre-loaded\\n      this.requestAds();\\n    });\\n    /**\\n     * Request advertisements\\n     */\\n    _defineProperty$1(this, \\\"requestAds\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      try {\\n        // Request video ads\\n        const request = new google.ima.AdsRequest();\\n        request.adTagUrl = this.tagUrl;\\n\\n        // Specify the linear and nonlinear slot sizes. This helps the SDK\\n        // to select the correct creative if multiple are returned\\n        request.linearAdSlotWidth = container.offsetWidth;\\n        request.linearAdSlotHeight = container.offsetHeight;\\n        request.nonLinearAdSlotWidth = container.offsetWidth;\\n        request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n        // We only overlay ads as we only support video.\\n        request.forceNonLinearFullSlot = false;\\n\\n        // Mute based on current state\\n        request.setAdWillPlayMuted(!this.player.muted);\\n        this.loader.requestAds(request);\\n      } catch (error) {\\n        this.onAdError(error);\\n      }\\n    });\\n    /**\\n     * Update the ad countdown\\n     * @param {Boolean} start\\n     */\\n    _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n      if (!start) {\\n        clearInterval(this.countdownTimer);\\n        this.elements.container.removeAttribute('data-badge-text');\\n        return;\\n      }\\n      const update = () => {\\n        const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n        const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n        this.elements.container.setAttribute('data-badge-text', label);\\n      };\\n      this.countdownTimer = setInterval(update, 100);\\n    });\\n    /**\\n     * This method is called whenever the ads are ready inside the AdDisplayContainer\\n     * @param {Event} event - adsManagerLoadedEvent\\n     */\\n    _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n      // Load could occur after a source change (race condition)\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Get the ads manager\\n      const settings = new google.ima.AdsRenderingSettings();\\n\\n      // Tell the SDK to save and restore content video state on our behalf\\n      settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n      settings.enablePreloading = true;\\n\\n      // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n      // so it can determine when to start the mid- and post-roll\\n      this.manager = event.getAdsManager(this.player, settings);\\n\\n      // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n      this.cuePoints = this.manager.getCuePoints();\\n\\n      // Add listeners to the required events\\n      // Advertisement error events\\n      this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n      // Advertisement regular events\\n      Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n        this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n      });\\n\\n      // Resolve our adsManager\\n      this.trigger('loaded');\\n    });\\n    _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n      // Add advertisement cue's within the time line if available\\n      if (!is.empty(this.cuePoints)) {\\n        this.cuePoints.forEach(cuePoint => {\\n          if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n            const seekElement = this.player.elements.progress;\\n            if (is.element(seekElement)) {\\n              const cuePercentage = 100 / this.player.duration * cuePoint;\\n              const cue = createElement('span', {\\n                class: this.player.config.classNames.cues\\n              });\\n              cue.style.left = `${cuePercentage.toString()}%`;\\n              seekElement.appendChild(cue);\\n            }\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n     * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n     * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n      // don't have ad object associated\\n      const ad = event.getAd();\\n      const adData = event.getAdData();\\n\\n      // Proxy event\\n      const dispatchEvent = type => {\\n        triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n      };\\n\\n      // Bubble the event\\n      dispatchEvent(event.type);\\n      switch (event.type) {\\n        case google.ima.AdEvent.Type.LOADED:\\n          // This is the first event sent for an ad - it is possible to determine whether the\\n          // ad is a video ad or an overlay\\n          this.trigger('loaded');\\n\\n          // Start countdown\\n          this.pollCountdown(true);\\n          if (!ad.isLinear()) {\\n            // Position AdDisplayContainer correctly for overlay\\n            ad.width = container.offsetWidth;\\n            ad.height = container.offsetHeight;\\n          }\\n\\n          // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n          // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n          break;\\n        case google.ima.AdEvent.Type.STARTED:\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n          break;\\n        case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n          // All ads for the current videos are done. We can now request new advertisements\\n          // in case the video is re-played\\n\\n          // TODO: Example for what happens when a next video in a playlist would be loaded.\\n          // So here we load a new video when all ads are done.\\n          // Then we load new ads within a new adsManager. When the video\\n          // Is started - after - the ads are loaded, then we get ads.\\n          // You can also easily test cancelling and reloading by running\\n          // player.ads.cancel() and player.ads.play from the console I guess.\\n          // this.player.source = {\\n          //     type: 'video',\\n          //     title: 'View From A Blue Moon',\\n          //     sources: [{\\n          //         src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n          // 'video/mp4', }], poster:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n          // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n          // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n          // };\\n\\n          // TODO: So there is still this thing where a video should only be allowed to start\\n          // playing when the IMA SDK is ready or has failed\\n\\n          if (this.player.ended) {\\n            this.loadAds();\\n          } else {\\n            // The SDK won't allow new ads to be called without receiving a contentComplete()\\n            this.loader.contentComplete();\\n          }\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n          // This event indicates the ad has started - the video player can adjust the UI,\\n          // for example display a pause button and remaining time. Fired when content should\\n          // be paused. This usually happens right before an ad is about to cover the content\\n\\n          this.pauseContent();\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n          // This event indicates the ad has finished - the video player can perform\\n          // appropriate UI actions, such as removing the timer for remaining time detection.\\n          // Fired when content should be resumed. This usually happens when an ad finishes\\n          // or collapses\\n\\n          this.pollCountdown();\\n          this.resumeContent();\\n          break;\\n        case google.ima.AdEvent.Type.LOG:\\n          if (adData.adError) {\\n            this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n          }\\n          break;\\n      }\\n    });\\n    /**\\n     * Any ad error handling comes through here\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdError\\\", event => {\\n      this.cancel();\\n      this.player.debug.warn('Ads error', event);\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events. This ensures\\n     * the mid- and post-roll launch at the correct time. And\\n     * resize the advertisement when the player resizes\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      let time;\\n      this.player.on('canplay', () => {\\n        this.addCuePoints();\\n      });\\n      this.player.on('ended', () => {\\n        this.loader.contentComplete();\\n      });\\n      this.player.on('timeupdate', () => {\\n        time = this.player.currentTime;\\n      });\\n      this.player.on('seeked', () => {\\n        const seekedTime = this.player.currentTime;\\n        if (is.empty(this.cuePoints)) {\\n          return;\\n        }\\n        this.cuePoints.forEach((cuePoint, index) => {\\n          if (time < cuePoint && cuePoint < seekedTime) {\\n            this.manager.discardAdBreak();\\n            this.cuePoints.splice(index, 1);\\n          }\\n        });\\n      });\\n\\n      // Listen to the resizing of the window. And resize ad accordingly\\n      // TODO: eventually implement ResizeObserver\\n      window.addEventListener('resize', () => {\\n        if (this.manager) {\\n          this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n        }\\n      });\\n    });\\n    /**\\n     * Initialize the adsManager and start playing advertisements\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      if (!this.managerPromise) {\\n        this.resumeContent();\\n      }\\n\\n      // Play the requested advertisement whenever the adsManager is ready\\n      this.managerPromise.then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Resume our video\\n     */\\n    _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n      // Hide the advertisement container\\n      this.elements.container.style.zIndex = '';\\n\\n      // Ad is stopped\\n      this.playing = false;\\n\\n      // Play video\\n      silencePromise(this.player.media.play());\\n    });\\n    /**\\n     * Pause our video\\n     */\\n    _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n      // Show the advertisement container\\n      this.elements.container.style.zIndex = 3;\\n\\n      // Ad is playing\\n      this.playing = true;\\n\\n      // Pause our video.\\n      this.player.media.pause();\\n    });\\n    /**\\n     * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n     * allowed to call new ads based on google policies, as they interpret this as an accidental\\n     * video requests. https://developers.google.com/interactive-\\n     * media-ads/docs/sdks/android/faq#8\\n     */\\n    _defineProperty$1(this, \\\"cancel\\\", () => {\\n      // Pause our video\\n      if (this.initialized) {\\n        this.resumeContent();\\n      }\\n\\n      // Tell our instance that we're done for now\\n      this.trigger('error');\\n\\n      // Re-create our adsManager\\n      this.loadAds();\\n    });\\n    /**\\n     * Re-create our adsManager\\n     */\\n    _defineProperty$1(this, \\\"loadAds\\\", () => {\\n      // Tell our adsManager to go bye bye\\n      this.managerPromise.then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise(resolve => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Handles callbacks after an ad event was invoked\\n     * @param {String} event - Event type\\n     * @param args\\n     */\\n    _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n      const handlers = this.events[event];\\n      if (is.array(handlers)) {\\n        handlers.forEach(handler => {\\n          if (is.function(handler)) {\\n            handler.apply(this, args);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     * @return {Ads}\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      if (!is.array(this.events[event])) {\\n        this.events[event] = [];\\n      }\\n      this.events[event].push(callback);\\n      return this;\\n    });\\n    /**\\n     * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n     * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n     * advertisement is playing, or when a user action is required to start, then we clear the\\n     * timer on ad ready\\n     * @param {Number} time\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n      this.player.debug.log(`Safety timer invoked from: ${from}`);\\n      this.safetyTimer = setTimeout(() => {\\n        this.cancel();\\n        this.clearSafetyTimer('startSafetyTimer()');\\n      }, time);\\n    });\\n    /**\\n     * Clear our safety timer(s)\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n      if (!is.nullOrUndefined(this.safetyTimer)) {\\n        this.player.debug.log(`Safety timer cleared from: ${from}`);\\n        clearTimeout(this.safetyTimer);\\n        this.safetyTimer = null;\\n      }\\n    });\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n    this.load();\\n  }\\n  get enabled() {\\n    const {\\n      config\\n    } = this;\\n    return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n  }\\n  // Build the tag URL\\n  get tagUrl() {\\n    const {\\n      config\\n    } = this;\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId\\n    };\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n}\\n\\n/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nfunction clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = vttDataString => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n  frames.forEach(frame => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n    lines.forEach(line => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n          result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = 1 / ratio * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n  return result;\\n};\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      // Toggle the regular seek tooltip\\n      if (this.player.elements.display.seekTooltip) {\\n        this.player.elements.display.seekTooltip.hidden = this.enabled;\\n      }\\n      if (!this.enabled) return;\\n      this.getThumbnails().then(() => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Render DOM elements\\n        this.render();\\n\\n        // Check to see if thumb container size was specified manually in CSS\\n        this.determineContainerAutoSizing();\\n\\n        // Set up listeners\\n        this.listeners();\\n        this.loaded = true;\\n      });\\n    });\\n    // Download VTT files and parse them\\n    _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n      return new Promise(resolve => {\\n        const {\\n          src\\n        } = this.player.config.previewThumbnails;\\n        if (is.empty(src)) {\\n          throw new Error('Missing previewThumbnails.src config attribute');\\n        }\\n\\n        // Resolve promise\\n        const sortAndResolve = () => {\\n          // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n          this.thumbnails.sort((x, y) => x.height - y.height);\\n          this.player.debug.log('Preview thumbnails', this.thumbnails);\\n          resolve();\\n        };\\n\\n        // Via callback()\\n        if (is.function(src)) {\\n          src(thumbnails => {\\n            this.thumbnails = thumbnails;\\n            sortAndResolve();\\n          });\\n        }\\n        // VTT urls\\n        else {\\n          // If string, convert into single-element list\\n          const urls = is.string(src) ? [src] : src;\\n          // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n          const promises = urls.map(u => this.getThumbnail(u));\\n          // Resolve\\n          Promise.all(promises).then(sortAndResolve);\\n        }\\n      });\\n    });\\n    // Process individual VTT file\\n    _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n      return new Promise(resolve => {\\n        fetch(url).then(response => {\\n          const thumbnail = {\\n            frames: parseVtt(response),\\n            height: null,\\n            urlPrefix: ''\\n          };\\n\\n          // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n          // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n          // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n          if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n            thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n          }\\n\\n          // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n          const tempImage = new Image();\\n          tempImage.onload = () => {\\n            thumbnail.height = tempImage.naturalHeight;\\n            thumbnail.width = tempImage.naturalWidth;\\n            this.thumbnails.push(thumbnail);\\n            resolve();\\n          };\\n          tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n        });\\n      });\\n    });\\n    _defineProperty$1(this, \\\"startMove\\\", event => {\\n      if (!this.loaded) return;\\n      if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n      // Wait until media has a duration\\n      if (!this.player.media.duration) return;\\n      if (event.type === 'touchmove') {\\n        // Calculate seek hover position as approx video seconds\\n        this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n      } else {\\n        var _this$player$config$m, _this$player$config$m2;\\n        // Calculate seek hover position as approx video seconds\\n        const clientRect = this.player.elements.progress.getBoundingClientRect();\\n        const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n        this.seekTime = this.player.media.duration * (percentage / 100);\\n        if (this.seekTime < 0) {\\n          // The mousemove fires for 10+px out to the left\\n          this.seekTime = 0;\\n        }\\n        if (this.seekTime > this.player.media.duration - 1) {\\n          // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n          this.seekTime = this.player.media.duration - 1;\\n        }\\n        this.mousePosX = event.pageX;\\n\\n        // Set time text inside image container\\n        this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n        // Get marker point for time\\n        const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n          time: t\\n        }) => t === Math.round(this.seekTime));\\n\\n        // Append the point label to the tooltip\\n        if (point) {\\n          // this.elements.thumb.time.innerText.concat('\\\\n');\\n          this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n        }\\n      }\\n\\n      // Download and show image\\n      this.showImageAtCurrentTime();\\n    });\\n    _defineProperty$1(this, \\\"endMove\\\", () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n    _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n      // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n      if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n        this.mouseDown = true;\\n\\n        // Wait until media has a duration\\n        if (this.player.media.duration) {\\n          this.toggleScrubbingContainer(true);\\n          this.toggleThumbContainer(false, true);\\n\\n          // Download and show image\\n          this.showImageAtCurrentTime();\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n      this.mouseDown = false;\\n\\n      // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n      if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n        // The video was already seeked/loaded at the chosen time - hide immediately\\n        this.toggleScrubbingContainer(false);\\n      } else {\\n        // The video hasn't seeked yet. Wait for that\\n        once.call(this.player, this.player.media, 'timeupdate', () => {\\n          // Re-check mousedown - we might have already started scrubbing again\\n          if (!this.mouseDown) {\\n            this.toggleScrubbingContainer(false);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n      this.player.on('play', () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      this.player.on('seeked', () => {\\n        this.toggleThumbContainer(false);\\n      });\\n      this.player.on('timeupdate', () => {\\n        this.lastTime = this.player.media.currentTime;\\n      });\\n    });\\n    /**\\n     * Create HTML elements for image containers\\n     */\\n    _defineProperty$1(this, \\\"render\\\", () => {\\n      // Create HTML element: plyr__preview-thumbnail-container\\n      this.elements.thumb.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.thumbContainer\\n      });\\n\\n      // Wrapper for the image for styling\\n      this.elements.thumb.imageContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.imageContainer\\n      });\\n      this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n      // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n      const timeContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.timeContainer\\n      });\\n      this.elements.thumb.time = createElement('span', {}, '00:00');\\n      timeContainer.appendChild(this.elements.thumb.time);\\n      this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n      // Inject the whole thumb\\n      if (is.element(this.player.elements.progress)) {\\n        this.player.elements.progress.appendChild(this.elements.thumb.container);\\n      }\\n\\n      // Create HTML element: plyr__preview-scrubbing-container\\n      this.elements.scrubbing.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n      });\\n      this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n    });\\n    _defineProperty$1(this, \\\"destroy\\\", () => {\\n      if (this.elements.thumb.container) {\\n        this.elements.thumb.container.remove();\\n      }\\n      if (this.elements.scrubbing.container) {\\n        this.elements.scrubbing.container.remove();\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n      if (this.mouseDown) {\\n        this.setScrubbingContainerSize();\\n      } else {\\n        this.setThumbContainerSizeAndPos();\\n      }\\n\\n      // Find the desired thumbnail index\\n      // TODO: Handle a video longer than the thumbs where thumbNum is null\\n      const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n      const hasThumb = thumbNum >= 0;\\n      let qualityIndex = 0;\\n\\n      // Show the thumb container if we're not scrubbing\\n      if (!this.mouseDown) {\\n        this.toggleThumbContainer(hasThumb);\\n      }\\n\\n      // No matching thumb found\\n      if (!hasThumb) {\\n        return;\\n      }\\n\\n      // Check to see if we've already downloaded higher quality versions of this image\\n      this.thumbnails.forEach((thumbnail, index) => {\\n        if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n          qualityIndex = index;\\n        }\\n      });\\n\\n      // Only proceed if either thumb num or thumbfilename has changed\\n      if (thumbNum !== this.showingThumb) {\\n        this.showingThumb = thumbNum;\\n        this.loadImage(qualityIndex);\\n      }\\n    });\\n    // Show the image that's currently specified in this.showingThumb\\n    _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n      const thumbNum = this.showingThumb;\\n      const thumbnail = this.thumbnails[qualityIndex];\\n      const {\\n        urlPrefix\\n      } = thumbnail;\\n      const frame = thumbnail.frames[thumbNum];\\n      const thumbFilename = thumbnail.frames[thumbNum].text;\\n      const thumbUrl = urlPrefix + thumbFilename;\\n      if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n        // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n        // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n        if (this.loadingImage && this.usingSprites) {\\n          this.loadingImage.onload = null;\\n        }\\n\\n        // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n        // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n        // images causes a flicker. Putting a new image over the top does not\\n        const previewImage = new Image();\\n        previewImage.src = thumbUrl;\\n        previewImage.dataset.index = thumbNum;\\n        previewImage.dataset.filename = thumbFilename;\\n        this.showingThumbFilename = thumbFilename;\\n        this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n        // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n        previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n        this.loadingImage = previewImage;\\n        this.removeOldImages(previewImage);\\n      } else {\\n        // Update the existing image\\n        this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n        this.currentImageElement.dataset.index = thumbNum;\\n        this.removeOldImages(this.currentImageElement);\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n      this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n      this.setImageSizeAndOffset(previewImage, frame);\\n      if (newImage) {\\n        this.currentImageContainer.appendChild(previewImage);\\n        this.currentImageElement = previewImage;\\n        if (!this.loadedImages.includes(thumbFilename)) {\\n          this.loadedImages.push(thumbFilename);\\n        }\\n      }\\n\\n      // Preload images before and after the current one\\n      // Show higher quality of the same frame\\n      // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n      this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n    });\\n    // Remove all preview images that aren't the designated current image\\n    _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n      // Get a list of all images, convert it from a DOM list to an array\\n      Array.from(this.currentImageContainer.children).forEach(image => {\\n        if (image.tagName.toLowerCase() !== 'img') {\\n          return;\\n        }\\n        const removeDelay = this.usingSprites ? 500 : 1000;\\n        if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n          // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n          // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n          // eslint-disable-next-line no-param-reassign\\n          image.dataset.deleting = true;\\n\\n          // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n          const {\\n            currentImageContainer\\n          } = this;\\n          setTimeout(() => {\\n            currentImageContainer.removeChild(image);\\n            this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n          }, removeDelay);\\n        }\\n      });\\n    });\\n    // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n    // This will only preload the lowest quality\\n    _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n      return new Promise(resolve => {\\n        setTimeout(() => {\\n          const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n          if (this.showingThumbFilename === oldThumbFilename) {\\n            // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n            let thumbnailsClone;\\n            if (forward) {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n            } else {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n            }\\n            let foundOne = false;\\n            thumbnailsClone.forEach(frame => {\\n              const newThumbFilename = frame.text;\\n              if (newThumbFilename !== oldThumbFilename) {\\n                // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                if (!this.loadedImages.includes(newThumbFilename)) {\\n                  foundOne = true;\\n                  this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                  const {\\n                    urlPrefix\\n                  } = this.thumbnails[0];\\n                  const thumbURL = urlPrefix + newThumbFilename;\\n                  const previewImage = new Image();\\n                  previewImage.src = thumbURL;\\n                  previewImage.onload = () => {\\n                    this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                    if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                    // We don't resolve until the thumb is loaded\\n                    resolve();\\n                  };\\n                }\\n              }\\n            });\\n\\n            // If there are none to preload then we want to resolve immediately\\n            if (!foundOne) {\\n              resolve();\\n            }\\n          }\\n        }, 300);\\n      });\\n    });\\n    // If user has been hovering current image for half a second, look for a higher quality one\\n    _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n      if (currentQualityIndex < this.thumbnails.length - 1) {\\n        // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n        let previewImageHeight = previewImage.naturalHeight;\\n        if (this.usingSprites) {\\n          previewImageHeight = frame.h;\\n        }\\n        if (previewImageHeight < this.thumbContainerHeight) {\\n          // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n          setTimeout(() => {\\n            // Make sure the mouse hasn't already moved on and started hovering at another image\\n            if (this.showingThumbFilename === thumbFilename) {\\n              this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n              this.loadImage(currentQualityIndex + 1);\\n            }\\n          }, 300);\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n      this.elements.thumb.container.classList.toggle(className, toggle);\\n      if (!toggle && clearShowing) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n      this.elements.scrubbing.container.classList.toggle(className, toggle);\\n      if (!toggle) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n      if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n        // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n        this.sizeSpecifiedInCSS = true;\\n      }\\n    });\\n    // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n    _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n      const {\\n        imageContainer\\n      } = this.elements.thumb;\\n      if (!this.sizeSpecifiedInCSS) {\\n        const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n        imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n        const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n        const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n        imageContainer.style.height = `${thumbHeight}px`;\\n      }\\n      this.setThumbContainerPos();\\n    });\\n    _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n      const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n      const containerRect = this.player.elements.container.getBoundingClientRect();\\n      const {\\n        container\\n      } = this.elements.thumb;\\n      // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n      const min = containerRect.left - scrubberRect.left + 10;\\n      const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n      // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n      const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n      const clamped = clamp(position, min, max);\\n\\n      // Move the popover position\\n      container.style.left = `${clamped}px`;\\n\\n      // The arrow can follow the cursor\\n      container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n    });\\n    // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n    _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n      const {\\n        width,\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      this.elements.scrubbing.container.style.width = `${width}px`;\\n      this.elements.scrubbing.container.style.height = `${height}px`;\\n    });\\n    // Sprites need to be offset to the correct location\\n    _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n      if (!this.usingSprites) return;\\n\\n      // Find difference between height and preview container height\\n      const multiplier = this.thumbContainerHeight / frame.h;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.left = `-${frame.x * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.top = `-${frame.y * multiplier}px`;\\n    });\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {}\\n    };\\n    this.load();\\n  }\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const {\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach(attribute => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(this, () => {\\n      // Reset quality options\\n      this.options.quality = [];\\n\\n      // Remove elements\\n      removeElement(this.media);\\n      this.media = null;\\n\\n      // Reset class name\\n      if (is.element(this.elements.container)) {\\n        this.elements.container.removeAttribute('class');\\n      }\\n\\n      // Set the type and provider\\n      const {\\n        sources,\\n        type\\n      } = input;\\n      const [{\\n        provider = providers.html5,\\n        src\\n      }] = sources;\\n      const tagName = provider === 'html5' ? type : 'div';\\n      const attributes = provider === 'html5' ? {} : {\\n        src\\n      };\\n      Object.assign(this, {\\n        provider,\\n        type,\\n        // Check for support\\n        supported: support.check(type, provider, this.config.playsinline),\\n        // Create new element\\n        media: createElement(tagName, attributes)\\n      });\\n\\n      // Inject the new element\\n      this.elements.container.appendChild(this.media);\\n\\n      // Autoplay the new source?\\n      if (is.boolean(input.autoplay)) {\\n        this.config.autoplay = input.autoplay;\\n      }\\n\\n      // Set attributes for audio and video\\n      if (this.isHTML5) {\\n        if (this.config.crossorigin) {\\n          this.media.setAttribute('crossorigin', '');\\n        }\\n        if (this.config.autoplay) {\\n          this.media.setAttribute('autoplay', '');\\n        }\\n        if (!is.empty(input.poster)) {\\n          this.poster = input.poster;\\n        }\\n        if (this.config.loop.active) {\\n          this.media.setAttribute('loop', '');\\n        }\\n        if (this.config.muted) {\\n          this.media.setAttribute('muted', '');\\n        }\\n        if (this.config.playsinline) {\\n          this.media.setAttribute('playsinline', '');\\n        }\\n      }\\n\\n      // Restore class hook\\n      ui.addStyleHook.call(this);\\n\\n      // Set new sources for html5\\n      if (this.isHTML5) {\\n        source.insertElements.call(this, 'source', sources);\\n      }\\n\\n      // Set video title\\n      this.config.title = input.title;\\n\\n      // Set up from scratch\\n      media.setup.call(this);\\n\\n      // HTML5 stuff\\n      if (this.isHTML5) {\\n        // Setup captions\\n        if (Object.keys(input).includes('tracks')) {\\n          source.insertElements.call(this, 'track', input.tracks);\\n        }\\n      }\\n\\n      // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        // Setup interface\\n        ui.build.call(this);\\n      }\\n\\n      // Load HTML5 sources\\n      if (this.isHTML5) {\\n        this.media.load();\\n      }\\n\\n      // Update previewThumbnails config & reload plugin\\n      if (!is.empty(input.previewThumbnails)) {\\n        Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n        // Cleanup previewThumbnails plugin if it was loaded\\n        if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n          this.previewThumbnails.destroy();\\n          this.previewThumbnails = null;\\n        }\\n\\n        // Create new instance if it is still enabled\\n        if (this.config.previewThumbnails.enabled) {\\n          this.previewThumbnails = new PreviewThumbnails(this);\\n        }\\n      }\\n\\n      // Update the fullscreen support\\n      this.fullscreen.update();\\n    }, true);\\n  }\\n};\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    /**\\n     * Play the media, or play the advertisement (if they are not blocked)\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      if (!is.function(this.media.play)) {\\n        return null;\\n      }\\n\\n      // Intecept play with ads\\n      if (this.ads && this.ads.enabled) {\\n        this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n      }\\n\\n      // Return the promise (for HTML5)\\n      return this.media.play();\\n    });\\n    /**\\n     * Pause the media\\n     */\\n    _defineProperty$1(this, \\\"pause\\\", () => {\\n      if (!this.playing || !is.function(this.media.pause)) {\\n        return null;\\n      }\\n      return this.media.pause();\\n    });\\n    /**\\n     * Toggle playback based on current status\\n     * @param {Boolean} input\\n     */\\n    _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n      // Toggle based on current state if nothing passed\\n      const toggle = is.boolean(input) ? input : !this.playing;\\n      if (toggle) {\\n        return this.play();\\n      }\\n      return this.pause();\\n    });\\n    /**\\n     * Stop playback\\n     */\\n    _defineProperty$1(this, \\\"stop\\\", () => {\\n      if (this.isHTML5) {\\n        this.pause();\\n        this.restart();\\n      } else if (is.function(this.media.stop)) {\\n        this.media.stop();\\n      }\\n    });\\n    /**\\n     * Restart playback\\n     */\\n    _defineProperty$1(this, \\\"restart\\\", () => {\\n      this.currentTime = 0;\\n    });\\n    /**\\n     * Rewind\\n     * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n      this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Fast forward\\n     * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n      this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Increase volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n      const volume = this.media.muted ? 0 : this.volume;\\n      this.volume = volume + (is.number(step) ? step : 0);\\n    });\\n    /**\\n     * Decrease volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n      this.increaseVolume(-step);\\n    });\\n    /**\\n     * Trigger the airplay dialog\\n     * TODO: update player with state, support, enabled\\n     */\\n    _defineProperty$1(this, \\\"airplay\\\", () => {\\n      // Show dialog if supported\\n      if (support.airplay) {\\n        this.media.webkitShowPlaybackTargetPicker();\\n      }\\n    });\\n    /**\\n     * Toggle the player controls\\n     * @param {Boolean} [toggle] - Whether to show the controls\\n     */\\n    _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n      // Don't toggle if missing UI support or if it's audio\\n      if (this.supported.ui && !this.isAudio) {\\n        // Get state before change\\n        const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n        // Negate the argument if not undefined since adding the class to hides the controls\\n        const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n        // Apply and get updated state\\n        const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n        // Close menu\\n        if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n          controls.toggleMenu.call(this, false);\\n        }\\n\\n        // Trigger event on change\\n        if (hiding !== isHidden) {\\n          const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n          triggerEvent.call(this, this.media, eventName);\\n        }\\n        return !hiding;\\n      }\\n      return false;\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      on.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Add event listeners once\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n      once.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Remove event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n      off(this.elements.container, event, callback);\\n    });\\n    /**\\n     * Destroy an instance\\n     * Event listeners are removed when elements are removed\\n     * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n     * @param {Function} callback - Callback for when destroy is complete\\n     * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n     */\\n    _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n      if (!this.ready) {\\n        return;\\n      }\\n      const done = () => {\\n        // Reset overflow (incase destroyed while in fullscreen)\\n        document.body.style.overflow = '';\\n\\n        // GC for embed\\n        this.embed = null;\\n\\n        // If it's a soft destroy, make minimal changes\\n        if (soft) {\\n          if (Object.keys(this.elements).length) {\\n            // Remove elements\\n            removeElement(this.elements.buttons.play);\\n            removeElement(this.elements.captions);\\n            removeElement(this.elements.controls);\\n            removeElement(this.elements.wrapper);\\n\\n            // Clear for GC\\n            this.elements.buttons.play = null;\\n            this.elements.captions = null;\\n            this.elements.controls = null;\\n            this.elements.wrapper = null;\\n          }\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n        } else {\\n          // Unbind listeners\\n          unbindListeners.call(this);\\n\\n          // Cancel current network requests\\n          html5.cancelRequests.call(this);\\n\\n          // Replace the container with the original element provided\\n          replaceElement(this.elements.original, this.elements.container);\\n\\n          // Event\\n          triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback.call(this.elements.original);\\n          }\\n\\n          // Reset state\\n          this.ready = false;\\n\\n          // Clear for garbage collection\\n          setTimeout(() => {\\n            this.elements = null;\\n            this.media = null;\\n          }, 200);\\n        }\\n      };\\n\\n      // Stop playback\\n      this.stop();\\n\\n      // Clear timeouts\\n      clearTimeout(this.timers.loading);\\n      clearTimeout(this.timers.controls);\\n      clearTimeout(this.timers.resized);\\n\\n      // Provider specific stuff\\n      if (this.isHTML5) {\\n        // Restore native video controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Clean up\\n        done();\\n      } else if (this.isYouTube) {\\n        // Clear timers\\n        clearInterval(this.timers.buffering);\\n        clearInterval(this.timers.playing);\\n\\n        // Destroy YouTube API\\n        if (this.embed !== null && is.function(this.embed.destroy)) {\\n          this.embed.destroy();\\n        }\\n\\n        // Clean up\\n        done();\\n      } else if (this.isVimeo) {\\n        // Destroy Vimeo API\\n        // then clean up (wait, to prevent postmessage errors)\\n        if (this.embed !== null) {\\n          this.embed.unload().then(done);\\n        }\\n\\n        // Vimeo does not always return\\n        setTimeout(done, 200);\\n      }\\n    });\\n    /**\\n     * Check for support for a mime type (HTML5 only)\\n     * @param {String} type - Mime type\\n     */\\n    _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n      try {\\n        return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n      } catch (_) {\\n        return {};\\n      }\\n    })());\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {}\\n      }\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap()\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: []\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const _type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (_type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n        break;\\n      case 'video':\\n      case 'audio':\\n        this.type = _type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n        break;\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const {\\n      buffered\\n    } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({\\n        volume\\n      } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const {\\n      minimumSpeed: min,\\n      maximumSpeed: max\\n    } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n    if (!options.length) {\\n      return;\\n    }\\n    let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n    let updateStorage = true;\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({\\n        quality\\n      });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n         switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n             case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n             case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n             case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n             default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const {\\n      download\\n    } = this.config.urls;\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n    this.config.urls.download = input;\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n    this.config.ratio = reduceAspectRatio(input);\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const {\\n      toggled,\\n      currentTrack\\n    } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n    return targets.map(t => new Plyr(t, options));\\n  }\\n}\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport { Plyr as default };\\n//# sourceMappingURL=plyr.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}',\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',\\n    },\\n  },\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\n+{\"version\":3,\"sources\":[\"plyr.mjs\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"_toPropertyKey\",\"Object\",\"defineProperty\",\"enumerable\",\"configurable\",\"writable\",\"_toPrimitive\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"call\",\"TypeError\",\"String\",\"Number\",\"arg\",\"_classCallCheck\",\"e\",\"t\",\"_defineProperties\",\"n\",\"length\",\"r\",\"_createClass\",\"prototype\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"matches\",\"Array\",\"from\",\"document\",\"querySelectorAll\",\"includes\",\"this\",\"trigger\",\"Event\",\"bubbles\",\"dispatchEvent\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isNumber\",\"isNaN\",\"isString\",\"isBoolean\",\"Boolean\",\"isFunction\",\"Function\",\"isArray\",\"isNodeList\",\"NodeList\",\"isElement\",\"Element\",\"isEvent\",\"isEmpty\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"string\",\"boolean\",\"function\",\"array\",\"nodeList\",\"element\",\"event\",\"empty\",\"getDecimalPlaces\",\"concat\",\"match\",\"Math\",\"max\",\"round\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"set\",\"target\",\"i\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"u\",\"c\",\"getBoundingClientRect\",\"a\",\"width\",\"clientX\",\"left\",\"disabled\",\"preventDefault\",\"get\",\"type\",\"MutationObserver\",\"addedNodes\",\"observe\",\"body\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isWeakMap\",\"WeakMap\",\"isTextNode\",\"Text\",\"isKeyboardEvent\",\"KeyboardEvent\",\"isCue\",\"window\",\"TextTrackCue\",\"VTTCue\",\"isTrack\",\"TextTrack\",\"kind\",\"isPromise\",\"Promise\",\"then\",\"nodeType\",\"ownerDocument\",\"isUrl\",\"URL\",\"startsWith\",\"hostname\",\"_\",\"weakMap\",\"textNode\",\"keyboardEvent\",\"cue\",\"track\",\"promise\",\"url\",\"transitionEndEvent\",\"createElement\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"isIE\",\"documentMode\",\"isEdge\",\"test\",\"navigator\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"browser\",\"cloneDeep\",\"JSON\",\"parse\",\"stringify\",\"getDeep\",\"path\",\"split\",\"reduce\",\"extend\",\"sources\",\"source\",\"shift\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"appendChild\",\"insertBefore\",\"setAttributes\",\"attributes\",\"entries\",\"setAttribute\",\"text\",\"innerText\",\"insertAfter\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"replace\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"method\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"closest\",\"el\",\"parentElement\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"callback\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"detail\",\"CustomEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"indexOf\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"getViewportSize\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"generateId\",\"prefix\",\"floor\",\"random\",\"format\",\"toString\",\"getPercentage\",\"current\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"slice\",\"toLowerCase\",\"toPascalCase\",\"toCamelCase\",\"stripHTML\",\"fragment\",\"createDocumentFragment\",\"innerHTML\",\"firstChild\",\"getHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"storage\",\"setItem\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"Error\",\"status\",\"open\",\"send\",\"error\",\"loadSprite\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"location\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"join\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sort\",\"b\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"values\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"href\",\"urls\",\"isEmbed\",\"inject\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"params\",\"URLSearchParams\",\"isYouTube\",\"protocol\",\"blob\",\"createObjectURL\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"has\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"global\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"inactive\",\"providers\",\"types\",\"getProviderByUrl\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"head\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"attribute\",\"hasAttribute\",\"done\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"commonjsGlobal\",\"globalThis\",\"self\",\"createCommonjsModule\",\"fn\",\"module\",\"exports\",\"loadjs_umd\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"doc\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathname\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"defaultPrevented\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"factory\",\"loadScript\",\"parseId\",\"$2\",\"parseHash\",\"found\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"hashParam\",\"sidedock\",\"gesture\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"setInterval\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"destroy\",\"manager\",\"displayContainer\",\"remove\",\"Ads\",\"google\",\"ima\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"Plyr\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"search\",\"truthy\",\"searchParams\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.min.mjs\",\"sourcesContent\":[\"function _defineProperty$1(obj, key, value) {\\n  key = _toPropertyKey(key);\\n  if (key in obj) {\\n    Object.defineProperty(obj, key, {\\n      value: value,\\n      enumerable: true,\\n      configurable: true,\\n      writable: true\\n    });\\n  } else {\\n    obj[key] = value;\\n  }\\n  return obj;\\n}\\nfunction _toPrimitive(input, hint) {\\n  if (typeof input !== \\\"object\\\" || input === null) return input;\\n  var prim = input[Symbol.toPrimitive];\\n  if (prim !== undefined) {\\n    var res = prim.call(input, hint || \\\"default\\\");\\n    if (typeof res !== \\\"object\\\") return res;\\n    throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n  }\\n  return (hint === \\\"string\\\" ? String : Number)(input);\\n}\\nfunction _toPropertyKey(arg) {\\n  var key = _toPrimitive(arg, \\\"string\\\");\\n  return typeof key === \\\"symbol\\\" ? key : String(key);\\n}\\n\\nfunction _classCallCheck(e, t) {\\n  if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n}\\nfunction _defineProperties(e, t) {\\n  for (var n = 0; n < t.length; n++) {\\n    var r = t[n];\\n    r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n  }\\n}\\nfunction _createClass(e, t, n) {\\n  return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n}\\nfunction _defineProperty(e, t, n) {\\n  return t in e ? Object.defineProperty(e, t, {\\n    value: n,\\n    enumerable: !0,\\n    configurable: !0,\\n    writable: !0\\n  }) : e[t] = n, e;\\n}\\nfunction ownKeys(e, t) {\\n  var n = Object.keys(e);\\n  if (Object.getOwnPropertySymbols) {\\n    var r = Object.getOwnPropertySymbols(e);\\n    t && (r = r.filter(function (t) {\\n      return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n    })), n.push.apply(n, r);\\n  }\\n  return n;\\n}\\nfunction _objectSpread2(e) {\\n  for (var t = 1; t < arguments.length; t++) {\\n    var n = null != arguments[t] ? arguments[t] : {};\\n    t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n      _defineProperty(e, t, n[t]);\\n    }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n      Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n    });\\n  }\\n  return e;\\n}\\nvar defaults$1 = {\\n  addCSS: !0,\\n  thumbWidth: 15,\\n  watch: !0\\n};\\nfunction matches$1(e, t) {\\n  return function () {\\n    return Array.from(document.querySelectorAll(t)).includes(this);\\n  }.call(e, t);\\n}\\nfunction trigger(e, t) {\\n  if (e && t) {\\n    var n = new Event(t, {\\n      bubbles: !0\\n    });\\n    e.dispatchEvent(n);\\n  }\\n}\\nvar getConstructor$1 = function (e) {\\n    return null != e ? e.constructor : null;\\n  },\\n  instanceOf$1 = function (e, t) {\\n    return !!(e && t && e instanceof t);\\n  },\\n  isNullOrUndefined$1 = function (e) {\\n    return null == e;\\n  },\\n  isObject$1 = function (e) {\\n    return getConstructor$1(e) === Object;\\n  },\\n  isNumber$1 = function (e) {\\n    return getConstructor$1(e) === Number && !Number.isNaN(e);\\n  },\\n  isString$1 = function (e) {\\n    return getConstructor$1(e) === String;\\n  },\\n  isBoolean$1 = function (e) {\\n    return getConstructor$1(e) === Boolean;\\n  },\\n  isFunction$1 = function (e) {\\n    return getConstructor$1(e) === Function;\\n  },\\n  isArray$1 = function (e) {\\n    return Array.isArray(e);\\n  },\\n  isNodeList$1 = function (e) {\\n    return instanceOf$1(e, NodeList);\\n  },\\n  isElement$1 = function (e) {\\n    return instanceOf$1(e, Element);\\n  },\\n  isEvent$1 = function (e) {\\n    return instanceOf$1(e, Event);\\n  },\\n  isEmpty$1 = function (e) {\\n    return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n  },\\n  is$1 = {\\n    nullOrUndefined: isNullOrUndefined$1,\\n    object: isObject$1,\\n    number: isNumber$1,\\n    string: isString$1,\\n    boolean: isBoolean$1,\\n    function: isFunction$1,\\n    array: isArray$1,\\n    nodeList: isNodeList$1,\\n    element: isElement$1,\\n    event: isEvent$1,\\n    empty: isEmpty$1\\n  };\\nfunction getDecimalPlaces(e) {\\n  var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n  return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n}\\nfunction round(e, t) {\\n  if (1 > t) {\\n    var n = getDecimalPlaces(t);\\n    return parseFloat(e.toFixed(n));\\n  }\\n  return Math.round(e / t) * t;\\n}\\nvar RangeTouch = function () {\\n  function e(t, n) {\\n    _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n  }\\n  return _createClass(e, [{\\n    key: \\\"init\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n    }\\n  }, {\\n    key: \\\"destroy\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n    }\\n  }, {\\n    key: \\\"listeners\\\",\\n    value: function (e) {\\n      var t = this,\\n        n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n      [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n        t.element[n](e, function (e) {\\n          return t.set(e);\\n        }, !1);\\n      });\\n    }\\n  }, {\\n    key: \\\"get\\\",\\n    value: function (t) {\\n      if (!e.enabled || !is$1.event(t)) return null;\\n      var n,\\n        r = t.target,\\n        i = t.changedTouches[0],\\n        o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n        s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n        u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n        c = r.getBoundingClientRect(),\\n        a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n      return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n    }\\n  }, {\\n    key: \\\"set\\\",\\n    value: function (t) {\\n      e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n    }\\n  }], [{\\n    key: \\\"setup\\\",\\n    value: function (t) {\\n      var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n        r = null;\\n      if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n      var i = _objectSpread2({}, defaults$1, {}, n);\\n      if (is$1.string(t) && i.watch) {\\n        var o = new MutationObserver(function (n) {\\n          Array.from(n).forEach(function (n) {\\n            Array.from(n.addedNodes).forEach(function (n) {\\n              is$1.element(n) && matches$1(n, t) && new e(n, i);\\n            });\\n          });\\n        });\\n        o.observe(document.body, {\\n          childList: !0,\\n          subtree: !0\\n        });\\n      }\\n      return r.map(function (t) {\\n        return new e(t, n);\\n      });\\n    }\\n  }, {\\n    key: \\\"enabled\\\",\\n    get: function () {\\n      return \\\"ontouchstart\\\" in document.documentElement;\\n    }\\n  }]), e;\\n}();\\n\\n// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = input => input === null || typeof input === 'undefined';\\nconst isObject = input => getConstructor(input) === Object;\\nconst isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = input => getConstructor(input) === String;\\nconst isBoolean = input => getConstructor(input) === Boolean;\\nconst isFunction = input => typeof input === 'function';\\nconst isArray = input => Array.isArray(input);\\nconst isWeakMap = input => instanceOf(input, WeakMap);\\nconst isNodeList = input => instanceOf(input, NodeList);\\nconst isTextNode = input => getConstructor(input) === Text;\\nconst isEvent = input => instanceOf(input, Event);\\nconst isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\nconst isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\nconst isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\nconst isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\nconst isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\nconst isUrl = input => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\nvar is = {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty\\n};\\n\\n// ==========================================================================\\nconst transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend'\\n  };\\n  const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nfunction repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\\n// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\nvar browser = {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos\\n};\\n\\n// ==========================================================================\\n\\n// Clone nested objects\\nfunction cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nfunction getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nfunction extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n  const source = sources.shift();\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n  Object.keys(source).forEach(key => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, {\\n          [key]: {}\\n        });\\n      }\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, {\\n        [key]: source[key]\\n      });\\n    }\\n  });\\n  return extend(target, ...sources);\\n}\\n\\n// ==========================================================================\\n\\n// Wrap an element\\nfunction wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets).reverse().forEach((element, index) => {\\n    const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n    // Cache the current parent and sibling.\\n    const parent = element.parentNode;\\n    const sibling = element.nextSibling;\\n\\n    // Wrap the element (is automatically removed from its current\\n    // parent).\\n    child.appendChild(element);\\n\\n    // If the element had a sibling, insert the wrapper before\\n    // the sibling to maintain the HTML structure; otherwise, just\\n    // append it to the parent.\\n    if (sibling) {\\n      parent.insertBefore(child, sibling);\\n    } else {\\n      parent.appendChild(child);\\n    }\\n  });\\n}\\n\\n// Set attributes\\nfunction setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nfunction createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nfunction insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nfunction insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nfunction removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nfunction emptyElement(element) {\\n  if (!is.element(element)) return;\\n  let {\\n    length\\n  } = element.childNodes;\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nfunction replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nfunction getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n  sel.split(',').forEach(s => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n        break;\\n    }\\n  });\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nfunction toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n  let hide = hidden;\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nfunction toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map(e => toggleClass(e, className, force));\\n  }\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n  return false;\\n}\\n\\n// Has class name\\nfunction hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nfunction matches(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n  const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nfunction closest$1(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n  const method = prototype.closest || closestElement;\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nfunction getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nfunction getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nfunction setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({\\n    preventScroll: true,\\n    focusVisible\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora'\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n    return {\\n      api,\\n      ui\\n    };\\n  },\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n    return false;\\n  })(),\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n};\\n\\n// ==========================================================================\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      }\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nfunction toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach(type => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({\\n        element,\\n        type,\\n        callback,\\n        options\\n      });\\n    }\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nfunction on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nfunction off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nfunction once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nfunction triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: {\\n      ...detail,\\n      plyr: this\\n    }\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nfunction unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach(item => {\\n      const {\\n        element,\\n        type,\\n        callback,\\n        options\\n      } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nfunction ready() {\\n  return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n}\\n\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nfunction silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Remove duplicates in an array\\nfunction dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nfunction closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n  return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n}\\n\\n// ==========================================================================\\n\\n// Check support for a CSS declaration\\nfunction supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n  ...out,\\n  [x / y]: [x, y]\\n}), {});\\n\\n// Validate an aspect ratio\\nfunction validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n  const ratio = is.array(input) ? input : input.split(':');\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nfunction reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n  const divider = getDivider(width, height);\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nfunction getAspectRatio(input) {\\n  const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({\\n      ratio\\n    } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const {\\n      videoWidth,\\n      videoHeight\\n    } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nfunction setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n  const {\\n    wrapper\\n  } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = 100 / x * y;\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n  return {\\n    padding,\\n    ratio\\n  };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nfunction roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nfunction getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\\n// ==========================================================================\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter(source => {\\n      const type = source.getAttribute('type');\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n      return support.mime.call(this, type);\\n    });\\n  },\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n  },\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const {\\n            currentTime,\\n            paused,\\n            preload,\\n            readyState,\\n            playbackRate\\n          } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input\\n        });\\n      }\\n    });\\n  },\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Generate a random ID\\nfunction generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nfunction format(input, ...args) {\\n  if (is.empty(input)) return input;\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nfunction getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n  return (current / max * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nconst replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nconst toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nfunction toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nfunction toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nfunction stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nfunction getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\\n// ==========================================================================\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube'\\n};\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n    let string = getDeep(config.i18n, key);\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n      return '';\\n    }\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title\\n    };\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n    return string;\\n  }\\n};\\n\\nclass Storage {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"get\\\", key => {\\n      if (!Storage.supported || !this.enabled) {\\n        return null;\\n      }\\n      const store = window.localStorage.getItem(this.key);\\n      if (is.empty(store)) {\\n        return null;\\n      }\\n      const json = JSON.parse(store);\\n      return is.string(key) && key.length ? json[key] : json;\\n    });\\n    _defineProperty$1(this, \\\"set\\\", object => {\\n      // Bail if we don't have localStorage support or it's disabled\\n      if (!Storage.supported || !this.enabled) {\\n        return;\\n      }\\n\\n      // Can only store objectst\\n      if (!is.object(object)) {\\n        return;\\n      }\\n\\n      // Get current storage\\n      let storage = this.get();\\n\\n      // Default to empty object\\n      if (is.empty(storage)) {\\n        storage = {};\\n      }\\n\\n      // Update the working copy of the values\\n      extend(storage, object);\\n\\n      // Update storage\\n      try {\\n        window.localStorage.setItem(this.key, JSON.stringify(storage));\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    });\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nfunction fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Load an external SVG sprite\\nfunction loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url).then(result => {\\n      if (is.empty(result)) {\\n        return;\\n      }\\n      if (useStorage) {\\n        try {\\n          window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n            content: result\\n          }));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      }\\n      update(container, result);\\n    }).catch(() => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Time helpers\\nconst getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\nconst getMinutes = value => Math.trunc(value / 60 % 60, 10);\\nconst getSeconds = value => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nfunction formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = value => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\\n// ==========================================================================\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n    return {\\n      url: this.config.iconUrl,\\n      cors\\n    };\\n  },\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume)\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration)\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n      return false;\\n    }\\n  },\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(icon, extend(attributes, {\\n      'aria-hidden': 'true',\\n      focusable: 'false'\\n    }));\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n    return icon;\\n  },\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = {\\n      ...attr,\\n      class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n    };\\n    return createElement('span', attributes, text);\\n  },\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value\\n    });\\n    badge.appendChild(createElement('span', {\\n      class: this.config.classNames.menu.badge\\n    }, text));\\n    return badge;\\n  },\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null\\n    };\\n    ['element', 'icon', 'label'].forEach(key => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n        class: 'icon--pressed'\\n      }));\\n      button.appendChild(controls.createIcon.call(this, props.icon, {\\n        class: 'icon--not-pressed'\\n      }));\\n\\n      // Label/Tooltip\\n      button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n        class: 'label--pressed'\\n      }));\\n      button.appendChild(controls.createLabel.call(this, props.label, {\\n        class: 'label--not-pressed'\\n      }));\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n    return button;\\n  },\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n      type: 'range',\\n      min: 0,\\n      max: 100,\\n      step: 0.01,\\n      value: 0,\\n      autocomplete: 'off',\\n      // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n      role: 'slider',\\n      'aria-label': i18n.get(type, this.config),\\n      'aria-valuemin': 0,\\n      'aria-valuemax': 100,\\n      'aria-valuenow': 0\\n    }, attributes));\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n    return input;\\n  },\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n      min: 0,\\n      max: 100,\\n      value: 0,\\n      role: 'progressbar',\\n      'aria-hidden': true\\n    }, attributes));\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered'\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n    this.elements.display[type] = progress;\\n    return progress;\\n  },\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n    const container = createElement('div', extend(attributes, {\\n      class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n      'aria-label': i18n.get(type, this.config),\\n      role: 'timer'\\n    }), '00:00');\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n    return container;\\n  },\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(this, menuItem, 'keydown keyup', event => {\\n      // We only care about space and ⬆️ ⬇️️ ➡️\\n      if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Prevent play / seek\\n      event.preventDefault();\\n      event.stopPropagation();\\n\\n      // We're just here to prevent the keydown bubbling\\n      if (event.type === 'keydown') {\\n        return;\\n      }\\n      const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n      // Show the respective menu\\n      if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n        controls.showMenuPanel.call(this, type, true);\\n      } else {\\n        let target;\\n        if (event.key !== ' ') {\\n          if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n            target = menuItem.nextElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.firstElementChild;\\n            }\\n          } else {\\n            target = menuItem.previousElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.lastElementChild;\\n            }\\n          }\\n          setFocus.call(this, target, true);\\n        }\\n      }\\n    }, false);\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', event => {\\n      if (event.key !== 'Return') return;\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n  // Create a settings menu item\\n  createMenuItem({\\n    value,\\n    list,\\n    type,\\n    title,\\n    badge = null,\\n    checked = false\\n  }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n    const menuItem = createElement('button', extend(attributes, {\\n      type: 'button',\\n      role: 'menuitemradio',\\n      class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n      'aria-checked': checked,\\n      value\\n    }));\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n        }\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      }\\n    });\\n    this.listeners.bind(menuItem, 'click keyup', event => {\\n      if (is.keyboardEvent(event) && event.key !== ' ') {\\n        return;\\n      }\\n      event.preventDefault();\\n      event.stopPropagation();\\n      menuItem.checked = true;\\n      switch (type) {\\n        case 'language':\\n          this.currentTrack = Number(value);\\n          break;\\n        case 'quality':\\n          this.quality = value;\\n          break;\\n        case 'speed':\\n          this.speed = parseFloat(value);\\n          break;\\n      }\\n      controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n    }, type, false);\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n    list.appendChild(menuItem);\\n  },\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n    return formatTime(time, forceHours, inverted);\\n  },\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n    let value = 0;\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n          break;\\n      }\\n    }\\n  },\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n  },\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    var _this$config$markers, _this$config$markers$;\\n    // Bail if setting not true\\n    if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n      return;\\n    }\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = show => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n    if (is.event(event)) {\\n      percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n    const time = this.duration / 100 * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n      time: t\\n    }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n          return label;\\n        }\\n        return toTitleCase(value);\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n      default:\\n        return null;\\n    }\\n  },\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = quality => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n      if (!label.length) {\\n        return null;\\n      }\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality.sort((a, b) => {\\n      const sorting = this.config.quality.options;\\n      return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n    }).forEach(quality => {\\n      controls.createMenuItem.call(this, {\\n        value: quality,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'quality', quality),\\n        badge: getBadge(quality)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n         const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n         // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n         // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n         // Empty the menu\\n        emptyElement(list);\\n         options.forEach(option => {\\n            const item = createElement('li');\\n             const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n             if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n             item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language'\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language'\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach(speed => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const {\\n      buttons\\n    } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n    let target = pane;\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n    }\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const {\\n      popup\\n    } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const {\\n      hidden\\n    } = popup;\\n    let show = hidden;\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n    return {\\n      width,\\n      height\\n    };\\n  },\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find(node => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = event => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = {\\n      class: 'plyr__controls__item'\\n    };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`\\n        });\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(createRange.call(this, 'seek', {\\n          id: `plyr-seek-${data.id}`\\n        }));\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement('span', {\\n            class: this.config.classNames.tooltip\\n          }, '00:00');\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let {\\n          volume\\n        } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__volume`.trim()\\n          }));\\n          this.elements.volume = volume;\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n            id: `plyr-volume-${data.id}`\\n          })));\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement('div', extend({}, defaultAttributes, {\\n          class: `${defaultAttributes.class} plyr__menu`.trim(),\\n          hidden: ''\\n        }));\\n        wrapper.appendChild(createButton.call(this, 'settings', {\\n          'aria-haspopup': true,\\n          'aria-controls': `plyr-settings-${data.id}`,\\n          'aria-expanded': false\\n        }));\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: ''\\n        });\\n        const inner = createElement('div');\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu'\\n        });\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach(type => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n            role: 'menuitem',\\n            'aria-haspopup': true,\\n            hidden: ''\\n          }));\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: ''\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(createElement('span', {\\n            'aria-hidden': true\\n          }, i18n.get(type, this.config)));\\n\\n          // Screen reader label\\n          backButton.appendChild(createElement('span', {\\n            class: this.config.classNames.hidden\\n          }, i18n.get('menuBack', this.config)));\\n\\n          // Go back via keyboard\\n          on.call(this, pane, 'keydown', event => {\\n            if (event.key !== 'ArrowLeft') return;\\n\\n            // Prevent seek\\n            event.preventDefault();\\n            event.stopPropagation();\\n\\n            // Show the respective menu\\n            showMenuPanel.call(this, 'home', true);\\n          }, false);\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(createElement('div', {\\n            role: 'menu'\\n          }));\\n          inner.appendChild(pane);\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank'\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n        const {\\n          download\\n        } = this.config.urls;\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider\\n          });\\n        }\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n    setSpeedMenu.call(this);\\n    return container;\\n  },\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this)\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = input => {\\n      let result = input;\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = button => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          }\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n        if (is.array(button) || is.nodeList(button)) {\\n          Array.from(button).filter(Boolean).forEach(addProperty);\\n        } else {\\n          addProperty(button);\\n        }\\n      });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const {\\n        classNames,\\n        selectors\\n      } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n      Array.from(labels).forEach(label => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n  // Add markers\\n  setMarkers() {\\n    var _this$config$markers2, _this$config$markers3;\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n      time\\n    }) => time > 0 && time < this.duration);\\n    if (!(points !== null && points !== void 0 && points.length)) return;\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach(point => {\\n      const markerElement = createElement('span', {\\n        class: this.config.classNames.marker\\n      }, '');\\n      const left = `${point.time / this.duration * 100}%`;\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement('span', {\\n        class: this.config.classNames.tooltip\\n      }, '');\\n      containerFragment.appendChild(tipElement);\\n    }\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement\\n    };\\n    this.elements.progress.appendChild(containerFragment);\\n  }\\n};\\n\\n// ==========================================================================\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nfunction parseUrl(input, safe = true) {\\n  let url = input;\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nfunction buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n  return params;\\n}\\n\\n// ==========================================================================\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n      // Clear menu and hide\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n      Array.from(elements).forEach(track => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n        if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n          fetch(src, 'blob').then(blob => {\\n            track.setAttribute('src', window.URL.createObjectURL(blob));\\n          }).catch(() => {\\n            removeElement(track);\\n          });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({\\n        active\\n      } = this.config.captions);\\n    }\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const {\\n      active,\\n      language,\\n      meta,\\n      currentTrackNode\\n    } = this.captions;\\n    const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks.filter(track => !meta.get(track)).forEach(track => {\\n        this.debug.log('Track added', track);\\n\\n        // Attempt to store if the original dom element was \\\"default\\\"\\n        meta.set(track, {\\n          default: track.mode === 'showing'\\n        });\\n\\n        // Turn off native caption rendering to avoid double captions\\n        // Note: mode='hidden' forces a track to download. To ensure every track\\n        // isn't downloaded at once, only 'showing' tracks should be reassigned\\n        // eslint-disable-next-line no-param-reassign\\n        if (track.mode === 'showing') {\\n          // eslint-disable-next-line no-param-reassign\\n          track.mode = 'hidden';\\n        }\\n\\n        // Add event listener for cue changes\\n        on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n      });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    const {\\n      toggled\\n    } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({\\n          captions: active\\n        });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const {\\n        language\\n      } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({\\n          language\\n        });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n    languages.every(language => {\\n      track = sorted.find(t => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n      return i18n.get('enabled', this.config);\\n    }\\n    return i18n.get('disabled', this.config);\\n  },\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n      cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n  // Custom media title\\n  title: '',\\n  // Logging to console\\n  debug: false,\\n  // Auto play (if supported)\\n  autoplay: false,\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n  // Pass a custom duration\\n  duration: null,\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n  // Auto hide the controls\\n  hideControls: true,\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null\\n  },\\n  // Set loops\\n  loop: {\\n    active: false\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n  },\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false\\n  },\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true\\n  },\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false\\n  },\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true,\\n    // Allow fullscreen?\\n    fallback: true,\\n    // Fallback using full viewport/window\\n    iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr'\\n  },\\n  // Default controls\\n  controls: ['play-large',\\n  // 'restart',\\n  // 'rewind',\\n  'play',\\n  // 'fast-forward',\\n  'progress', 'current-time',\\n  // 'duration',\\n  'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n  // 'download',\\n  'fullscreen'],\\n  settings: ['captions', 'quality', 'speed'],\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD'\\n    }\\n  },\\n  // URLs\\n  urls: null,\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null\\n  },\\n  // Events to watch and bubble\\n  events: [\\n  // Events to watch on HTML5 media elements and bubble\\n  // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n  'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n  // Custom events\\n  'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n  // YouTube\\n  'statechange',\\n  // Quality\\n  'qualitychange',\\n  // Ads\\n  'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls'\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]'\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]'\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop',\\n      // Used later\\n      volume: '.plyr__volume--display'\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption'\\n  },\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time'\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open'\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active'\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback'\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active'\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active'\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n    }\\n  },\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash'\\n    }\\n  },\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: ''\\n  },\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: ''\\n  },\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null,\\n    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false\\n  },\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0,\\n    // No related vids\\n    showinfo: 0,\\n    // Hide info\\n    iv_load_policy: 3,\\n    // Hide annotations\\n    modestbranding: 1,\\n    // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: []\\n  },\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: []\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nconst pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline'\\n};\\n\\n// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nconst providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo'\\n};\\nconst types = {\\n  audio: 'audio',\\n  video: 'video'\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nfunction getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n  return null;\\n}\\n\\n// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\nclass Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"onChange\\\", () => {\\n      if (!this.supported) return;\\n\\n      // Update toggle button\\n      const button = this.player.elements.buttons.fullscreen;\\n      if (is.element(button)) {\\n        button.pressed = this.active;\\n      }\\n\\n      // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n      const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n      // Trigger an event\\n      triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n    });\\n    _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n      // Store or restore scroll position\\n      if (toggle) {\\n        this.scrollPosition = {\\n          x: window.scrollX ?? 0,\\n          y: window.scrollY ?? 0\\n        };\\n      } else {\\n        window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n      }\\n\\n      // Toggle scroll\\n      document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n      // Toggle class hook\\n      toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n      // Force full viewport on iPhone X+\\n      if (browser.isIos) {\\n        let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n        const property = 'viewport-fit=cover';\\n\\n        // Inject the viewport meta if required\\n        if (!viewport) {\\n          viewport = document.createElement('meta');\\n          viewport.setAttribute('name', 'viewport');\\n        }\\n\\n        // Check if the property already exists\\n        const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n        if (toggle) {\\n          this.cleanupViewport = !hasProperty;\\n          if (!hasProperty) viewport.content += `,${property}`;\\n        } else if (this.cleanupViewport) {\\n          viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n        }\\n      }\\n\\n      // Toggle button and fire events\\n      this.onChange();\\n    });\\n    // Trap focus inside container\\n    _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n      // Bail if iOS/iPadOS, not active, not the tab key\\n      if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n      // Get the current focused element\\n      const focused = document.activeElement;\\n      const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n      const [first] = focusable;\\n      const last = focusable[focusable.length - 1];\\n      if (focused === last && !event.shiftKey) {\\n        // Move focus to first element that can be tabbed if Shift isn't used\\n        first.focus();\\n        event.preventDefault();\\n      } else if (focused === first && event.shiftKey) {\\n        // Move focus to last element that can be tabbed if Shift is used\\n        last.focus();\\n        event.preventDefault();\\n      }\\n    });\\n    // Update UI\\n    _defineProperty$1(this, \\\"update\\\", () => {\\n      if (this.supported) {\\n        let mode;\\n        if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n        this.player.debug.log(`${mode} fullscreen enabled`);\\n      } else {\\n        this.player.debug.log('Fullscreen not supported and fallback disabled');\\n      }\\n\\n      // Add styling hook to show button\\n      toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n    });\\n    // Make an element fullscreen\\n    _defineProperty$1(this, \\\"enter\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen doesn't need the request step\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.requestFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(true);\\n      } else if (!this.prefix) {\\n        this.target.requestFullscreen({\\n          navigationUI: 'hide'\\n        });\\n      } else if (!is.empty(this.prefix)) {\\n        this.target[`${this.prefix}Request${this.property}`]();\\n      }\\n    });\\n    // Bail from fullscreen\\n    _defineProperty$1(this, \\\"exit\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.exitFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n        silencePromise(this.player.play());\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(false);\\n      } else if (!this.prefix) {\\n        (document.cancelFullScreen || document.exitFullscreen).call(document);\\n      } else if (!is.empty(this.prefix)) {\\n        const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n        document[`${this.prefix}${action}${this.property}`]();\\n      }\\n    });\\n    // Toggle state\\n    _defineProperty$1(this, \\\"toggle\\\", () => {\\n      if (!this.active) this.enter();else this.exit();\\n    });\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = {\\n      x: 0,\\n      y: 0\\n    };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n      // TODO: Filter for target??\\n      this.onChange();\\n    });\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n    prefixes.some(pre => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n      return false;\\n    });\\n    return value;\\n  }\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n    // Fullscreen is enabled in config\\n    this.player.config.fullscreen.enabled,\\n    // Must be a video\\n    this.player.isVideo,\\n    // Either native is supported or fallback enabled\\n    Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n    // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n    // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n    !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n    const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n}\\n\\n// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nfunction loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n    Object.assign(image, {\\n      onload: handler,\\n      onerror: handler,\\n      src\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach(button => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return ready.call(this)\\n    // Load image\\n    .then(() => loadImage(poster)).catch(error => {\\n      // Hide poster on error unless it's been set by another call\\n      if (poster === this.poster) {\\n        ui.togglePoster.call(this, false);\\n      }\\n      // Rethrow\\n      throw error;\\n    }).then(() => {\\n      // Prevent race conditions\\n      if (poster !== this.poster) {\\n        throw new Error('setPoster cancelled by later call to setPoster');\\n      }\\n    }).then(() => {\\n      Object.assign(this.elements.poster.style, {\\n        backgroundImage: `url('${poster}')`,\\n        // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n        backgroundSize: ''\\n      });\\n      ui.togglePoster.call(this, true);\\n      return poster;\\n    });\\n  },\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach(target => {\\n      Object.assign(target, {\\n        pressed: this.playing\\n      });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(() => {\\n      // Update progress bar loading class state\\n      toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n      // Update controls visibility\\n      ui.toggleControls.call(this);\\n    }, this.loading ? 250 : 0);\\n  },\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const {\\n      controls: controlsElement\\n    } = this.elements;\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n    }\\n  },\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({\\n      ...this.media.style\\n    })\\n    // We're only fussed about Plyr specific properties\\n    .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n      // Set on the container\\n      this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n      // Clean up from media element\\n      this.media.style.removeProperty(key);\\n    });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  }\\n};\\n\\nclass Listeners {\\n  constructor(_player) {\\n    // Device is touch enabled\\n    _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      player.touch = true;\\n\\n      // Add touch class\\n      toggleClass(elements.container, player.config.classNames.isTouch, true);\\n    });\\n    // Global window & document listeners\\n    _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n      const {\\n        player\\n      } = this;\\n\\n      // Keyboard shortcuts\\n      if (player.config.keyboard.global) {\\n        toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n      }\\n\\n      // Click anywhere closes menu\\n      toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n      // Detect touch by events\\n      once.call(player, document.body, 'touchstart', this.firstTouch);\\n    });\\n    // Container listeners\\n    _defineProperty$1(this, \\\"container\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        config,\\n        elements,\\n        timers\\n      } = player;\\n\\n      // Keyboard shortcuts\\n      if (!config.keyboard.global && config.keyboard.focused) {\\n        on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n      }\\n\\n      // Toggle controls on mouse events and entering fullscreen\\n      on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n        const {\\n          controls: controlsElement\\n        } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Set a gutter for Vimeo\\n      const setGutter = () => {\\n        if (!player.isVimeo || player.config.vimeo.premium) {\\n          return;\\n        }\\n        const target = elements.wrapper;\\n        const {\\n          active\\n        } = player.fullscreen;\\n        const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n        const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n        // If not active, remove styles\\n        if (!active) {\\n          if (useNativeAspectRatio) {\\n            target.style.width = null;\\n            target.style.height = null;\\n          } else {\\n            target.style.maxWidth = null;\\n            target.style.margin = null;\\n          }\\n          return;\\n        }\\n\\n        // Determine which dimension will overflow and constrain view\\n        const [viewportWidth, viewportHeight] = getViewportSize();\\n        const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n        if (useNativeAspectRatio) {\\n          target.style.width = overflow ? 'auto' : '100%';\\n          target.style.height = overflow ? '100%' : 'auto';\\n        } else {\\n          target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n          target.style.margin = overflow ? '0 auto' : null;\\n        }\\n      };\\n\\n      // Handle resizing\\n      const resized = () => {\\n        clearTimeout(timers.resized);\\n        timers.resized = setTimeout(setGutter, 50);\\n      };\\n      on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n        const {\\n          target\\n        } = player.fullscreen;\\n\\n        // Ignore events not from target\\n        if (target !== elements.container) {\\n          return;\\n        }\\n\\n        // If it's not an embed and no ratio specified\\n        if (!player.isEmbed && is.empty(player.config.ratio)) {\\n          return;\\n        }\\n\\n        // Set Vimeo gutter\\n        setGutter();\\n\\n        // Watch for resizes\\n        const method = event.type === 'enterfullscreen' ? on : off;\\n        method.call(player, window, 'resize', resized);\\n      });\\n    });\\n    // Listen for media events\\n    _defineProperty$1(this, \\\"media\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n\\n      // Time change on media\\n      on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n      // Display duration\\n      on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n      // Handle the media finishing\\n      on.call(player, player.media, 'ended', () => {\\n        // Show poster on end\\n        if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n          // Restart\\n          player.restart();\\n\\n          // Call pause otherwise IE11 will start playing the video again\\n          player.pause();\\n        }\\n      });\\n\\n      // Check for buffer progress\\n      on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n      // Handle volume changes\\n      on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n      // Handle play/pause\\n      on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n      // Loading state\\n      on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n      // Click video\\n      if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n        // Re-fetch the wrapper\\n        const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n        // Bail if there's no wrapper (this should never happen)\\n        if (!is.element(wrapper)) {\\n          return;\\n        }\\n\\n        // On click play, pause or restart\\n        on.call(player, elements.container, 'click', event => {\\n          const targets = [elements.container, wrapper];\\n\\n          // Ignore if click if not container or in video wrapper\\n          if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n            return;\\n          }\\n\\n          // Touch devices will just show controls (if hidden)\\n          if (player.touch && player.config.hideControls) {\\n            return;\\n          }\\n          if (player.ended) {\\n            this.proxy(event, player.restart, 'restart');\\n            this.proxy(event, () => {\\n              silencePromise(player.play());\\n            }, 'play');\\n          } else {\\n            this.proxy(event, () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          }\\n        });\\n      }\\n\\n      // Disable right click\\n      if (player.supported.ui && player.config.disableContextMenu) {\\n        on.call(player, elements.wrapper, 'contextmenu', event => {\\n          event.preventDefault();\\n        }, false);\\n      }\\n\\n      // Volume change\\n      on.call(player, player.media, 'volumechange', () => {\\n        // Save to storage\\n        player.storage.set({\\n          volume: player.volume,\\n          muted: player.muted\\n        });\\n      });\\n\\n      // Speed change\\n      on.call(player, player.media, 'ratechange', () => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'speed');\\n\\n        // Save to storage\\n        player.storage.set({\\n          speed: player.speed\\n        });\\n      });\\n\\n      // Quality change\\n      on.call(player, player.media, 'qualitychange', event => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n      });\\n\\n      // Update download link when ready and if quality changes\\n      on.call(player, player.media, 'ready qualitychange', () => {\\n        controls.setDownloadUrl.call(player);\\n      });\\n\\n      // Proxy events to container\\n      // Bubble up key events for Edge\\n      const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n      on.call(player, player.media, proxyEvents, event => {\\n        let {\\n          detail = {}\\n        } = event;\\n\\n        // Get error details from media\\n        if (event.type === 'error') {\\n          detail = player.media.error;\\n        }\\n        triggerEvent.call(player, elements.container, event.type, true, detail);\\n      });\\n    });\\n    // Run default and custom handlers\\n    _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      let returned = true;\\n\\n      // Execute custom handler\\n      if (hasCustomHandler) {\\n        returned = customHandler.call(player, event);\\n      }\\n\\n      // Only call default handler if not prevented in custom handler\\n      if (returned !== false && is.function(defaultHandler)) {\\n        defaultHandler.call(player, event);\\n      }\\n    });\\n    // Trigger custom and default handlers\\n    _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n    });\\n    // Listen for control events\\n    _defineProperty$1(this, \\\"controls\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      // IE doesn't support input event, so we fallback to change\\n      const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n      // Play/pause toggle\\n      if (elements.buttons.play) {\\n        Array.from(elements.buttons.play).forEach(button => {\\n          this.bind(button, 'click', () => {\\n            silencePromise(player.togglePlay());\\n          }, 'play');\\n        });\\n      }\\n\\n      // Pause\\n      this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n      // Rewind\\n      this.bind(elements.buttons.rewind, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      }, 'rewind');\\n\\n      // Rewind\\n      this.bind(elements.buttons.fastForward, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      }, 'fastForward');\\n\\n      // Mute toggle\\n      this.bind(elements.buttons.mute, 'click', () => {\\n        player.muted = !player.muted;\\n      }, 'mute');\\n\\n      // Captions toggle\\n      this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n      // Download\\n      this.bind(elements.buttons.download, 'click', () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      }, 'download');\\n\\n      // Fullscreen toggle\\n      this.bind(elements.buttons.fullscreen, 'click', () => {\\n        player.fullscreen.toggle();\\n      }, 'fullscreen');\\n\\n      // Picture-in-Picture\\n      this.bind(elements.buttons.pip, 'click', () => {\\n        player.pip = 'toggle';\\n      }, 'pip');\\n\\n      // Airplay\\n      this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n      // Settings menu - click toggle\\n      this.bind(elements.buttons.settings, 'click', event => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n        controls.toggleMenu.call(player, event);\\n      }, null, false); // Can't be passive as we're preventing default\\n\\n      // Settings menu - keyboard toggle\\n      // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n      // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n      this.bind(elements.buttons.settings, 'keyup', event => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      }, null, false // Can't be passive as we're preventing default\\n      );\\n\\n      // Escape closes menu\\n      this.bind(elements.settings.menu, 'keydown', event => {\\n        if (event.key === 'Escape') {\\n          controls.toggleMenu.call(player, event);\\n        }\\n      });\\n\\n      // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n      this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n        const rect = elements.progress.getBoundingClientRect();\\n        const percent = 100 / rect.width * (event.pageX - rect.left);\\n        event.currentTarget.setAttribute('seek-value', percent);\\n      });\\n\\n      // Pause while seeking\\n      this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n        const seek = event.currentTarget;\\n        const attribute = 'play-on-seeked';\\n        if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Record seek time so we can prevent hiding controls for a few seconds after seek\\n        player.lastSeekTime = Date.now();\\n\\n        // Was playing before?\\n        const play = seek.hasAttribute(attribute);\\n        // Done seeking\\n        const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n        // If we're done seeking and it was playing, resume playback\\n        if (play && done) {\\n          seek.removeAttribute(attribute);\\n          silencePromise(player.play());\\n        } else if (!done && player.playing) {\\n          seek.setAttribute(attribute, '');\\n          player.pause();\\n        }\\n      });\\n\\n      // Fix range inputs on iOS\\n      // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n      // it takes over further interactions on the page. This is a hack\\n      if (browser.isIos) {\\n        const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n        Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n      }\\n\\n      // Seek\\n      this.bind(elements.inputs.seek, inputEvent, event => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n        seek.removeAttribute('seek-value');\\n        player.currentTime = seekTo / seek.max * player.duration;\\n      }, 'seek');\\n\\n      // Seek tooltip\\n      this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n      // Preview thumbnails plugin\\n      // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n      this.bind(elements.progress, 'mousemove touchmove', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startMove(event);\\n        }\\n      });\\n\\n      // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n      this.bind(elements.progress, 'mouseleave touchend click', () => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endMove(false, true);\\n        }\\n      });\\n\\n      // Show scrubbing preview\\n      this.bind(elements.progress, 'mousedown touchstart', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startScrubbing(event);\\n        }\\n      });\\n      this.bind(elements.progress, 'mouseup touchend', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endScrubbing(event);\\n        }\\n      });\\n\\n      // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n      if (browser.isWebKit) {\\n        Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n          this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n        });\\n      }\\n\\n      // Current time invert\\n      // Only if one time element is used for both currentTime and duration\\n      if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n        this.bind(elements.display.currentTime, 'click', () => {\\n          // Do nothing if we're at the start\\n          if (player.currentTime === 0) {\\n            return;\\n          }\\n          player.config.invertTime = !player.config.invertTime;\\n          controls.timeUpdate.call(player);\\n        });\\n      }\\n\\n      // Volume\\n      this.bind(elements.inputs.volume, inputEvent, event => {\\n        player.volume = event.target.value;\\n      }, 'volume');\\n\\n      // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n        elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n      });\\n\\n      // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n      if (elements.fullscreen) {\\n        Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n          this.bind(child, 'mouseenter mouseleave', event => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n      }\\n\\n      // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n      });\\n\\n      // Show controls when they receive focus (e.g., when using keyboard tab key)\\n      this.bind(elements.controls, 'focusin', () => {\\n        const {\\n          config,\\n          timers\\n        } = player;\\n\\n        // Skip transition to prevent focus from scrolling the parent element\\n        toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n        // Toggle\\n        ui.toggleControls.call(player, true);\\n\\n        // Restore transition\\n        setTimeout(() => {\\n          toggleClass(elements.controls, config.classNames.noTransition, false);\\n        }, 0);\\n\\n        // Delay a little more for mouse users\\n        const delay = this.touch ? 3000 : 4000;\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Hide again after delay\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Mouse wheel for volume\\n      this.bind(elements.inputs.volume, 'wheel', event => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const {\\n          volume\\n        } = player.media;\\n        if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n          event.preventDefault();\\n        }\\n      }, 'volume', false);\\n    });\\n    this.player = _player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const {\\n      player\\n    } = this;\\n    const {\\n      elements\\n    } = player;\\n    const {\\n      key,\\n      type,\\n      altKey,\\n      ctrlKey,\\n      metaKey,\\n      shiftKey\\n    } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = increment => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = player.duration / 10 * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const {\\n          editable\\n        } = player.config.selectors;\\n        const {\\n          seek\\n        } = elements.inputs;\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n}\\n\\nvar commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\nfunction createCommonjsModule(fn, module) {\\n\\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n}\\n\\nvar loadjs_umd = createCommonjsModule(function (module, exports) {\\n  (function (root, factory) {\\n    {\\n      module.exports = factory();\\n    }\\n  })(commonjsGlobal, function () {\\n    /**\\n     * Global dependencies.\\n     * @global {Object} document - DOM\\n     */\\n\\n    var devnull = function () {},\\n      bundleIdCache = {},\\n      bundleResultCache = {},\\n      bundleCallbackQueue = {};\\n\\n    /**\\n     * Subscribe to bundle load event.\\n     * @param {string[]} bundleIds - Bundle ids\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function subscribe(bundleIds, callbackFn) {\\n      // listify\\n      bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n      var depsNotFound = [],\\n        i = bundleIds.length,\\n        numWaiting = i,\\n        fn,\\n        bundleId,\\n        r,\\n        q;\\n\\n      // define callback function\\n      fn = function (bundleId, pathsNotFound) {\\n        if (pathsNotFound.length) depsNotFound.push(bundleId);\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(depsNotFound);\\n      };\\n\\n      // register callback\\n      while (i--) {\\n        bundleId = bundleIds[i];\\n\\n        // execute callback if in result cache\\n        r = bundleResultCache[bundleId];\\n        if (r) {\\n          fn(bundleId, r);\\n          continue;\\n        }\\n\\n        // add to callback queue\\n        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n        q.push(fn);\\n      }\\n    }\\n\\n    /**\\n     * Publish bundle load event.\\n     * @param {string} bundleId - Bundle id\\n     * @param {string[]} pathsNotFound - List of files not found\\n     */\\n    function publish(bundleId, pathsNotFound) {\\n      // exit if id isn't defined\\n      if (!bundleId) return;\\n      var q = bundleCallbackQueue[bundleId];\\n\\n      // cache result\\n      bundleResultCache[bundleId] = pathsNotFound;\\n\\n      // exit if queue is empty\\n      if (!q) return;\\n\\n      // empty callback queue\\n      while (q.length) {\\n        q[0](bundleId, pathsNotFound);\\n        q.splice(0, 1);\\n      }\\n    }\\n\\n    /**\\n     * Execute callbacks.\\n     * @param {Object or Function} args - The callback args\\n     * @param {string[]} depsNotFound - List of dependencies not found\\n     */\\n    function executeCallbacks(args, depsNotFound) {\\n      // accept function as argument\\n      if (args.call) args = {\\n        success: args\\n      };\\n\\n      // success and error callbacks\\n      if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n    }\\n\\n    /**\\n     * Load individual file.\\n     * @param {string} path - The file path\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFile(path, callbackFn, args, numTries) {\\n      var doc = document,\\n        async = args.async,\\n        maxTries = (args.numRetries || 0) + 1,\\n        beforeCallbackFn = args.before || devnull,\\n        pathname = path.replace(/[\\\\?|#].*$/, ''),\\n        pathStripped = path.replace(/^(css|img)!/, ''),\\n        isLegacyIECss,\\n        e;\\n      numTries = numTries || 0;\\n      if (/(^css!|\\\\.css$)/.test(pathname)) {\\n        // css\\n        e = doc.createElement('link');\\n        e.rel = 'stylesheet';\\n        e.href = pathStripped;\\n\\n        // tag IE9+\\n        isLegacyIECss = 'hideFocus' in e;\\n\\n        // use preload in IE Edge (to detect load errors)\\n        if (isLegacyIECss && e.relList) {\\n          isLegacyIECss = 0;\\n          e.rel = 'preload';\\n          e.as = 'style';\\n        }\\n      } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n        // image\\n        e = doc.createElement('img');\\n        e.src = pathStripped;\\n      } else {\\n        // javascript\\n        e = doc.createElement('script');\\n        e.src = path;\\n        e.async = async === undefined ? true : async;\\n      }\\n      e.onload = e.onerror = e.onbeforeload = function (ev) {\\n        var result = ev.type[0];\\n\\n        // treat empty stylesheets as failures to get around lack of onerror\\n        // support in IE9-11\\n        if (isLegacyIECss) {\\n          try {\\n            if (!e.sheet.cssText.length) result = 'e';\\n          } catch (x) {\\n            // sheets objects created from load errors don't allow access to\\n            // `cssText` (unless error is Code:18 SecurityError)\\n            if (x.code != 18) result = 'e';\\n          }\\n        }\\n\\n        // handle retries in case of load failure\\n        if (result == 'e') {\\n          // increment counter\\n          numTries += 1;\\n\\n          // exit function and try again\\n          if (numTries < maxTries) {\\n            return loadFile(path, callbackFn, args, numTries);\\n          }\\n        } else if (e.rel == 'preload' && e.as == 'style') {\\n          // activate preloaded stylesheets\\n          return e.rel = 'stylesheet'; // jshint ignore:line\\n        }\\n\\n        // execute callback\\n        callbackFn(path, result, ev.defaultPrevented);\\n      };\\n\\n      // add to document (unless callback returns `false`)\\n      if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n    }\\n\\n    /**\\n     * Load multiple files.\\n     * @param {string[]} paths - The file paths\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFiles(paths, callbackFn, args) {\\n      // listify paths\\n      paths = paths.push ? paths : [paths];\\n      var numWaiting = paths.length,\\n        x = numWaiting,\\n        pathsNotFound = [],\\n        fn,\\n        i;\\n\\n      // define callback function\\n      fn = function (path, result, defaultPrevented) {\\n        // handle error\\n        if (result == 'e') pathsNotFound.push(path);\\n\\n        // handle beforeload event. If defaultPrevented then that means the load\\n        // will be blocked (ex. Ghostery/ABP on Safari)\\n        if (result == 'b') {\\n          if (defaultPrevented) pathsNotFound.push(path);else return;\\n        }\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(pathsNotFound);\\n      };\\n\\n      // load scripts\\n      for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n    }\\n\\n    /**\\n     * Initiate script load and register bundle.\\n     * @param {(string|string[])} paths - The file paths\\n     * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n     *   callback or (3) object literal with success/error arguments, numRetries,\\n     *   etc.\\n     * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n     *   literal with success/error arguments, numRetries, etc.\\n     */\\n    function loadjs(paths, arg1, arg2) {\\n      var bundleId, args;\\n\\n      // bundleId (if string)\\n      if (arg1 && arg1.trim) bundleId = arg1;\\n\\n      // args (default is {})\\n      args = (bundleId ? arg2 : arg1) || {};\\n\\n      // throw error if bundle is already defined\\n      if (bundleId) {\\n        if (bundleId in bundleIdCache) {\\n          throw \\\"LoadJS\\\";\\n        } else {\\n          bundleIdCache[bundleId] = true;\\n        }\\n      }\\n      function loadFn(resolve, reject) {\\n        loadFiles(paths, function (pathsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, pathsNotFound);\\n\\n          // resolve Promise\\n          if (resolve) {\\n            executeCallbacks({\\n              success: resolve,\\n              error: reject\\n            }, pathsNotFound);\\n          }\\n\\n          // publish bundle load event\\n          publish(bundleId, pathsNotFound);\\n        }, args);\\n      }\\n      if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n    }\\n\\n    /**\\n     * Execute callbacks when dependencies have been satisfied.\\n     * @param {(string|string[])} deps - List of bundle ids\\n     * @param {Object} args - success/error arguments\\n     */\\n    loadjs.ready = function ready(deps, args) {\\n      // subscribe to bundle load event\\n      subscribe(deps, function (depsNotFound) {\\n        // execute callbacks\\n        executeCallbacks(args, depsNotFound);\\n      });\\n      return loadjs;\\n    };\\n\\n    /**\\n     * Manually satisfy bundle dependencies.\\n     * @param {string} bundleId - The bundle id\\n     */\\n    loadjs.done = function done(bundleId) {\\n      publish(bundleId, []);\\n    };\\n\\n    /**\\n     * Reset loadjs dependencies statuses\\n     */\\n    loadjs.reset = function reset() {\\n      bundleIdCache = {};\\n      bundleResultCache = {};\\n      bundleCallbackQueue = {};\\n    };\\n\\n    /**\\n     * Determine if bundle has already been defined\\n     * @param String} bundleId - The bundle id\\n     */\\n    loadjs.isDefined = function isDefined(bundleId) {\\n      return bundleId in bundleIdCache;\\n    };\\n\\n    // export\\n    return loadjs;\\n  });\\n});\\n\\n// ==========================================================================\\nfunction loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs_umd(url, {\\n      success: resolve,\\n      error: reject\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Parse Vimeo ID from URL\\nfunction parseId$1(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState$1(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk).then(() => {\\n        vimeo.ready.call(player);\\n      }).catch(error => {\\n        player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n      });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const {\\n      premium,\\n      referrerPolicy,\\n      ...frameParams\\n    } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? {\\n      h: hash\\n    } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams\\n    });\\n    const id = parseId$1(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted\\n    });\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState$1.call(player, true);\\n      return player.embed.play();\\n    };\\n    player.media.pause = () => {\\n      assurePlaybackState$1.call(player, false);\\n      return player.embed.pause();\\n    };\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let {\\n      currentTime\\n    } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const {\\n          embed,\\n          media,\\n          paused,\\n          volume\\n        } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n        // Seek\\n        .then(() => embed.setCurrentTime(time))\\n        // Restore paused\\n        .then(() => restorePause && embed.pause())\\n        // Restore volume\\n        .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n          // Do nothing\\n        });\\n      }\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed.setPlaybackRate(input).then(() => {\\n          speed = input;\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        }).catch(() => {\\n          // Cannot set Playback Rate, Video is probably not on Pro account\\n          player.options.speed = [1];\\n        });\\n      }\\n    });\\n\\n    // Volume\\n    let {\\n      volume\\n    } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Muted\\n    let {\\n      muted\\n    } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Loop\\n    let {\\n      loop\\n    } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      }\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed.getVideoUrl().then(value => {\\n      currentSrc = value;\\n      controls.setDownloadUrl.call(player);\\n    }).catch(error => {\\n      this.debug.warn(error);\\n    });\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      }\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      }\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then(state => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then(title => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then(value => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then(value => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then(tracks => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n    player.embed.on('cuechange', ({\\n      cues = []\\n    }) => {\\n      const strippedCues = cues.map(cue => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then(paused => {\\n        assurePlaybackState$1.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('play', () => {\\n      assurePlaybackState$1.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('pause', () => {\\n      assurePlaybackState$1.call(player, false);\\n    });\\n    player.embed.on('timeupdate', data => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n    player.embed.on('progress', data => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then(value => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n    player.embed.on('error', detail => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch(error => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n    fetch(url).then(data => {\\n      if (is.object(data)) {\\n        const {\\n          title,\\n          height,\\n          width\\n        } = data;\\n\\n        // Set title\\n        this.config.title = title;\\n        ui.setTitle.call(this);\\n\\n        // Set aspect ratio\\n        this.embed.ratio = roundAspectRatio(width, height);\\n      }\\n      setAspectRatio.call(this);\\n    }).catch(() => {\\n      // Set aspect ratio\\n      setAspectRatio.call(this);\\n    });\\n  },\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', {\\n      id,\\n      'data-poster': config.customControls ? player.poster : undefined\\n    });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n      .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n      .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n      .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n        // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n        if (!src.includes('maxres')) {\\n          player.elements.poster.style.backgroundSize = 'cover';\\n        }\\n      }).catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend({}, {\\n        // Autoplay\\n        autoplay: player.config.autoplay ? 1 : 0,\\n        // iframe interface language\\n        hl: player.config.hl,\\n        // Only show controls if not fully supported or opted out\\n        controls: player.supported.ui && config.customControls ? 0 : 1,\\n        // Disable keyboard as we handle it\\n        disablekb: 1,\\n        // Allow iOS inline playback\\n        playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n        // Captions are flaky on YouTube\\n        cc_load_policy: player.captions.active ? 1 : 0,\\n        cc_lang_pref: player.config.captions.language,\\n        // Tracking for stats\\n        widget_referrer: window ? window.location.href : null\\n      }, config),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message = {\\n              2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n              5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n              100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n              101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n            }[code] || 'An unknown error occurred';\\n            player.media.error = {\\n              code,\\n              message\\n            };\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            }\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            }\\n          });\\n\\n          // Volume\\n          let {\\n            volume\\n          } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Muted\\n          let {\\n            muted\\n          } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            }\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            }\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n              break;\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n              break;\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n              break;\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n              break;\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n              break;\\n          }\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data\\n          });\\n        }\\n      }\\n    });\\n  }\\n};\\n\\n// ==========================================================================\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster\\n      });\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  }\\n};\\n\\nconst destroy = instance => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n  instance.elements.container.remove();\\n};\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    /**\\n     * Load the IMA SDK\\n     */\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Check if the Google IMA3 SDK is loaded or load it ourselves\\n      if (!is.object(window.google) || !is.object(window.google.ima)) {\\n        loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n          this.ready();\\n        }).catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n      } else {\\n        this.ready();\\n      }\\n    });\\n    /**\\n     * Get the ads instance ready\\n     */\\n    _defineProperty$1(this, \\\"ready\\\", () => {\\n      // Double check we're enabled\\n      if (!this.enabled) {\\n        destroy(this);\\n      }\\n\\n      // Start ticking our safety timer. If the whole advertisement\\n      // thing doesn't resolve within our set time; we bail\\n      this.startSafetyTimer(12000, 'ready()');\\n\\n      // Clear the safety timer\\n      this.managerPromise.then(() => {\\n        this.clearSafetyTimer('onAdsManagerLoaded()');\\n      });\\n\\n      // Set listeners on the Plyr instance\\n      this.listeners();\\n\\n      // Setup the IMA SDK\\n      this.setupIMA();\\n    });\\n    /**\\n     * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n     * so here we define our ad container. This div is set up to render on top of the video player.\\n     * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n     * handle to the content video player - the SDK will poll the current time of our player to\\n     * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n     * mobile devices, this initialization is done as the result of a user action.\\n     */\\n    _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n      // Create the container for our advertisements\\n      this.elements.container = createElement('div', {\\n        class: this.player.config.classNames.ads\\n      });\\n      this.player.elements.container.appendChild(this.elements.container);\\n\\n      // So we can run VPAID2\\n      google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n      // Set language\\n      google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n      // Set playback for iOS10+\\n      google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n      // We assume the adContainer is the video container of the plyr element that will house the ads\\n      this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n      // Create ads loader\\n      this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n      // Listen and respond to ads loaded and error events\\n      this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n      this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n      // Request video ads to be pre-loaded\\n      this.requestAds();\\n    });\\n    /**\\n     * Request advertisements\\n     */\\n    _defineProperty$1(this, \\\"requestAds\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      try {\\n        // Request video ads\\n        const request = new google.ima.AdsRequest();\\n        request.adTagUrl = this.tagUrl;\\n\\n        // Specify the linear and nonlinear slot sizes. This helps the SDK\\n        // to select the correct creative if multiple are returned\\n        request.linearAdSlotWidth = container.offsetWidth;\\n        request.linearAdSlotHeight = container.offsetHeight;\\n        request.nonLinearAdSlotWidth = container.offsetWidth;\\n        request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n        // We only overlay ads as we only support video.\\n        request.forceNonLinearFullSlot = false;\\n\\n        // Mute based on current state\\n        request.setAdWillPlayMuted(!this.player.muted);\\n        this.loader.requestAds(request);\\n      } catch (error) {\\n        this.onAdError(error);\\n      }\\n    });\\n    /**\\n     * Update the ad countdown\\n     * @param {Boolean} start\\n     */\\n    _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n      if (!start) {\\n        clearInterval(this.countdownTimer);\\n        this.elements.container.removeAttribute('data-badge-text');\\n        return;\\n      }\\n      const update = () => {\\n        const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n        const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n        this.elements.container.setAttribute('data-badge-text', label);\\n      };\\n      this.countdownTimer = setInterval(update, 100);\\n    });\\n    /**\\n     * This method is called whenever the ads are ready inside the AdDisplayContainer\\n     * @param {Event} event - adsManagerLoadedEvent\\n     */\\n    _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n      // Load could occur after a source change (race condition)\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Get the ads manager\\n      const settings = new google.ima.AdsRenderingSettings();\\n\\n      // Tell the SDK to save and restore content video state on our behalf\\n      settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n      settings.enablePreloading = true;\\n\\n      // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n      // so it can determine when to start the mid- and post-roll\\n      this.manager = event.getAdsManager(this.player, settings);\\n\\n      // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n      this.cuePoints = this.manager.getCuePoints();\\n\\n      // Add listeners to the required events\\n      // Advertisement error events\\n      this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n      // Advertisement regular events\\n      Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n        this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n      });\\n\\n      // Resolve our adsManager\\n      this.trigger('loaded');\\n    });\\n    _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n      // Add advertisement cue's within the time line if available\\n      if (!is.empty(this.cuePoints)) {\\n        this.cuePoints.forEach(cuePoint => {\\n          if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n            const seekElement = this.player.elements.progress;\\n            if (is.element(seekElement)) {\\n              const cuePercentage = 100 / this.player.duration * cuePoint;\\n              const cue = createElement('span', {\\n                class: this.player.config.classNames.cues\\n              });\\n              cue.style.left = `${cuePercentage.toString()}%`;\\n              seekElement.appendChild(cue);\\n            }\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n     * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n     * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n      // don't have ad object associated\\n      const ad = event.getAd();\\n      const adData = event.getAdData();\\n\\n      // Proxy event\\n      const dispatchEvent = type => {\\n        triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n      };\\n\\n      // Bubble the event\\n      dispatchEvent(event.type);\\n      switch (event.type) {\\n        case google.ima.AdEvent.Type.LOADED:\\n          // This is the first event sent for an ad - it is possible to determine whether the\\n          // ad is a video ad or an overlay\\n          this.trigger('loaded');\\n\\n          // Start countdown\\n          this.pollCountdown(true);\\n          if (!ad.isLinear()) {\\n            // Position AdDisplayContainer correctly for overlay\\n            ad.width = container.offsetWidth;\\n            ad.height = container.offsetHeight;\\n          }\\n\\n          // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n          // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n          break;\\n        case google.ima.AdEvent.Type.STARTED:\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n          break;\\n        case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n          // All ads for the current videos are done. We can now request new advertisements\\n          // in case the video is re-played\\n\\n          // TODO: Example for what happens when a next video in a playlist would be loaded.\\n          // So here we load a new video when all ads are done.\\n          // Then we load new ads within a new adsManager. When the video\\n          // Is started - after - the ads are loaded, then we get ads.\\n          // You can also easily test cancelling and reloading by running\\n          // player.ads.cancel() and player.ads.play from the console I guess.\\n          // this.player.source = {\\n          //     type: 'video',\\n          //     title: 'View From A Blue Moon',\\n          //     sources: [{\\n          //         src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n          // 'video/mp4', }], poster:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n          // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n          // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n          // };\\n\\n          // TODO: So there is still this thing where a video should only be allowed to start\\n          // playing when the IMA SDK is ready or has failed\\n\\n          if (this.player.ended) {\\n            this.loadAds();\\n          } else {\\n            // The SDK won't allow new ads to be called without receiving a contentComplete()\\n            this.loader.contentComplete();\\n          }\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n          // This event indicates the ad has started - the video player can adjust the UI,\\n          // for example display a pause button and remaining time. Fired when content should\\n          // be paused. This usually happens right before an ad is about to cover the content\\n\\n          this.pauseContent();\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n          // This event indicates the ad has finished - the video player can perform\\n          // appropriate UI actions, such as removing the timer for remaining time detection.\\n          // Fired when content should be resumed. This usually happens when an ad finishes\\n          // or collapses\\n\\n          this.pollCountdown();\\n          this.resumeContent();\\n          break;\\n        case google.ima.AdEvent.Type.LOG:\\n          if (adData.adError) {\\n            this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n          }\\n          break;\\n      }\\n    });\\n    /**\\n     * Any ad error handling comes through here\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdError\\\", event => {\\n      this.cancel();\\n      this.player.debug.warn('Ads error', event);\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events. This ensures\\n     * the mid- and post-roll launch at the correct time. And\\n     * resize the advertisement when the player resizes\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      let time;\\n      this.player.on('canplay', () => {\\n        this.addCuePoints();\\n      });\\n      this.player.on('ended', () => {\\n        this.loader.contentComplete();\\n      });\\n      this.player.on('timeupdate', () => {\\n        time = this.player.currentTime;\\n      });\\n      this.player.on('seeked', () => {\\n        const seekedTime = this.player.currentTime;\\n        if (is.empty(this.cuePoints)) {\\n          return;\\n        }\\n        this.cuePoints.forEach((cuePoint, index) => {\\n          if (time < cuePoint && cuePoint < seekedTime) {\\n            this.manager.discardAdBreak();\\n            this.cuePoints.splice(index, 1);\\n          }\\n        });\\n      });\\n\\n      // Listen to the resizing of the window. And resize ad accordingly\\n      // TODO: eventually implement ResizeObserver\\n      window.addEventListener('resize', () => {\\n        if (this.manager) {\\n          this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n        }\\n      });\\n    });\\n    /**\\n     * Initialize the adsManager and start playing advertisements\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      if (!this.managerPromise) {\\n        this.resumeContent();\\n      }\\n\\n      // Play the requested advertisement whenever the adsManager is ready\\n      this.managerPromise.then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Resume our video\\n     */\\n    _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n      // Hide the advertisement container\\n      this.elements.container.style.zIndex = '';\\n\\n      // Ad is stopped\\n      this.playing = false;\\n\\n      // Play video\\n      silencePromise(this.player.media.play());\\n    });\\n    /**\\n     * Pause our video\\n     */\\n    _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n      // Show the advertisement container\\n      this.elements.container.style.zIndex = 3;\\n\\n      // Ad is playing\\n      this.playing = true;\\n\\n      // Pause our video.\\n      this.player.media.pause();\\n    });\\n    /**\\n     * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n     * allowed to call new ads based on google policies, as they interpret this as an accidental\\n     * video requests. https://developers.google.com/interactive-\\n     * media-ads/docs/sdks/android/faq#8\\n     */\\n    _defineProperty$1(this, \\\"cancel\\\", () => {\\n      // Pause our video\\n      if (this.initialized) {\\n        this.resumeContent();\\n      }\\n\\n      // Tell our instance that we're done for now\\n      this.trigger('error');\\n\\n      // Re-create our adsManager\\n      this.loadAds();\\n    });\\n    /**\\n     * Re-create our adsManager\\n     */\\n    _defineProperty$1(this, \\\"loadAds\\\", () => {\\n      // Tell our adsManager to go bye bye\\n      this.managerPromise.then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise(resolve => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Handles callbacks after an ad event was invoked\\n     * @param {String} event - Event type\\n     * @param args\\n     */\\n    _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n      const handlers = this.events[event];\\n      if (is.array(handlers)) {\\n        handlers.forEach(handler => {\\n          if (is.function(handler)) {\\n            handler.apply(this, args);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     * @return {Ads}\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      if (!is.array(this.events[event])) {\\n        this.events[event] = [];\\n      }\\n      this.events[event].push(callback);\\n      return this;\\n    });\\n    /**\\n     * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n     * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n     * advertisement is playing, or when a user action is required to start, then we clear the\\n     * timer on ad ready\\n     * @param {Number} time\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n      this.player.debug.log(`Safety timer invoked from: ${from}`);\\n      this.safetyTimer = setTimeout(() => {\\n        this.cancel();\\n        this.clearSafetyTimer('startSafetyTimer()');\\n      }, time);\\n    });\\n    /**\\n     * Clear our safety timer(s)\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n      if (!is.nullOrUndefined(this.safetyTimer)) {\\n        this.player.debug.log(`Safety timer cleared from: ${from}`);\\n        clearTimeout(this.safetyTimer);\\n        this.safetyTimer = null;\\n      }\\n    });\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n    this.load();\\n  }\\n  get enabled() {\\n    const {\\n      config\\n    } = this;\\n    return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n  }\\n  // Build the tag URL\\n  get tagUrl() {\\n    const {\\n      config\\n    } = this;\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId\\n    };\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n}\\n\\n/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nfunction clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = vttDataString => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n  frames.forEach(frame => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n    lines.forEach(line => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n          result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = 1 / ratio * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n  return result;\\n};\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      // Toggle the regular seek tooltip\\n      if (this.player.elements.display.seekTooltip) {\\n        this.player.elements.display.seekTooltip.hidden = this.enabled;\\n      }\\n      if (!this.enabled) return;\\n      this.getThumbnails().then(() => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Render DOM elements\\n        this.render();\\n\\n        // Check to see if thumb container size was specified manually in CSS\\n        this.determineContainerAutoSizing();\\n\\n        // Set up listeners\\n        this.listeners();\\n        this.loaded = true;\\n      });\\n    });\\n    // Download VTT files and parse them\\n    _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n      return new Promise(resolve => {\\n        const {\\n          src\\n        } = this.player.config.previewThumbnails;\\n        if (is.empty(src)) {\\n          throw new Error('Missing previewThumbnails.src config attribute');\\n        }\\n\\n        // Resolve promise\\n        const sortAndResolve = () => {\\n          // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n          this.thumbnails.sort((x, y) => x.height - y.height);\\n          this.player.debug.log('Preview thumbnails', this.thumbnails);\\n          resolve();\\n        };\\n\\n        // Via callback()\\n        if (is.function(src)) {\\n          src(thumbnails => {\\n            this.thumbnails = thumbnails;\\n            sortAndResolve();\\n          });\\n        }\\n        // VTT urls\\n        else {\\n          // If string, convert into single-element list\\n          const urls = is.string(src) ? [src] : src;\\n          // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n          const promises = urls.map(u => this.getThumbnail(u));\\n          // Resolve\\n          Promise.all(promises).then(sortAndResolve);\\n        }\\n      });\\n    });\\n    // Process individual VTT file\\n    _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n      return new Promise(resolve => {\\n        fetch(url).then(response => {\\n          const thumbnail = {\\n            frames: parseVtt(response),\\n            height: null,\\n            urlPrefix: ''\\n          };\\n\\n          // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n          // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n          // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n          if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n            thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n          }\\n\\n          // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n          const tempImage = new Image();\\n          tempImage.onload = () => {\\n            thumbnail.height = tempImage.naturalHeight;\\n            thumbnail.width = tempImage.naturalWidth;\\n            this.thumbnails.push(thumbnail);\\n            resolve();\\n          };\\n          tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n        });\\n      });\\n    });\\n    _defineProperty$1(this, \\\"startMove\\\", event => {\\n      if (!this.loaded) return;\\n      if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n      // Wait until media has a duration\\n      if (!this.player.media.duration) return;\\n      if (event.type === 'touchmove') {\\n        // Calculate seek hover position as approx video seconds\\n        this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n      } else {\\n        var _this$player$config$m, _this$player$config$m2;\\n        // Calculate seek hover position as approx video seconds\\n        const clientRect = this.player.elements.progress.getBoundingClientRect();\\n        const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n        this.seekTime = this.player.media.duration * (percentage / 100);\\n        if (this.seekTime < 0) {\\n          // The mousemove fires for 10+px out to the left\\n          this.seekTime = 0;\\n        }\\n        if (this.seekTime > this.player.media.duration - 1) {\\n          // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n          this.seekTime = this.player.media.duration - 1;\\n        }\\n        this.mousePosX = event.pageX;\\n\\n        // Set time text inside image container\\n        this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n        // Get marker point for time\\n        const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n          time: t\\n        }) => t === Math.round(this.seekTime));\\n\\n        // Append the point label to the tooltip\\n        if (point) {\\n          // this.elements.thumb.time.innerText.concat('\\\\n');\\n          this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n        }\\n      }\\n\\n      // Download and show image\\n      this.showImageAtCurrentTime();\\n    });\\n    _defineProperty$1(this, \\\"endMove\\\", () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n    _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n      // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n      if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n        this.mouseDown = true;\\n\\n        // Wait until media has a duration\\n        if (this.player.media.duration) {\\n          this.toggleScrubbingContainer(true);\\n          this.toggleThumbContainer(false, true);\\n\\n          // Download and show image\\n          this.showImageAtCurrentTime();\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n      this.mouseDown = false;\\n\\n      // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n      if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n        // The video was already seeked/loaded at the chosen time - hide immediately\\n        this.toggleScrubbingContainer(false);\\n      } else {\\n        // The video hasn't seeked yet. Wait for that\\n        once.call(this.player, this.player.media, 'timeupdate', () => {\\n          // Re-check mousedown - we might have already started scrubbing again\\n          if (!this.mouseDown) {\\n            this.toggleScrubbingContainer(false);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n      this.player.on('play', () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      this.player.on('seeked', () => {\\n        this.toggleThumbContainer(false);\\n      });\\n      this.player.on('timeupdate', () => {\\n        this.lastTime = this.player.media.currentTime;\\n      });\\n    });\\n    /**\\n     * Create HTML elements for image containers\\n     */\\n    _defineProperty$1(this, \\\"render\\\", () => {\\n      // Create HTML element: plyr__preview-thumbnail-container\\n      this.elements.thumb.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.thumbContainer\\n      });\\n\\n      // Wrapper for the image for styling\\n      this.elements.thumb.imageContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.imageContainer\\n      });\\n      this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n      // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n      const timeContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.timeContainer\\n      });\\n      this.elements.thumb.time = createElement('span', {}, '00:00');\\n      timeContainer.appendChild(this.elements.thumb.time);\\n      this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n      // Inject the whole thumb\\n      if (is.element(this.player.elements.progress)) {\\n        this.player.elements.progress.appendChild(this.elements.thumb.container);\\n      }\\n\\n      // Create HTML element: plyr__preview-scrubbing-container\\n      this.elements.scrubbing.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n      });\\n      this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n    });\\n    _defineProperty$1(this, \\\"destroy\\\", () => {\\n      if (this.elements.thumb.container) {\\n        this.elements.thumb.container.remove();\\n      }\\n      if (this.elements.scrubbing.container) {\\n        this.elements.scrubbing.container.remove();\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n      if (this.mouseDown) {\\n        this.setScrubbingContainerSize();\\n      } else {\\n        this.setThumbContainerSizeAndPos();\\n      }\\n\\n      // Find the desired thumbnail index\\n      // TODO: Handle a video longer than the thumbs where thumbNum is null\\n      const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n      const hasThumb = thumbNum >= 0;\\n      let qualityIndex = 0;\\n\\n      // Show the thumb container if we're not scrubbing\\n      if (!this.mouseDown) {\\n        this.toggleThumbContainer(hasThumb);\\n      }\\n\\n      // No matching thumb found\\n      if (!hasThumb) {\\n        return;\\n      }\\n\\n      // Check to see if we've already downloaded higher quality versions of this image\\n      this.thumbnails.forEach((thumbnail, index) => {\\n        if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n          qualityIndex = index;\\n        }\\n      });\\n\\n      // Only proceed if either thumb num or thumbfilename has changed\\n      if (thumbNum !== this.showingThumb) {\\n        this.showingThumb = thumbNum;\\n        this.loadImage(qualityIndex);\\n      }\\n    });\\n    // Show the image that's currently specified in this.showingThumb\\n    _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n      const thumbNum = this.showingThumb;\\n      const thumbnail = this.thumbnails[qualityIndex];\\n      const {\\n        urlPrefix\\n      } = thumbnail;\\n      const frame = thumbnail.frames[thumbNum];\\n      const thumbFilename = thumbnail.frames[thumbNum].text;\\n      const thumbUrl = urlPrefix + thumbFilename;\\n      if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n        // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n        // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n        if (this.loadingImage && this.usingSprites) {\\n          this.loadingImage.onload = null;\\n        }\\n\\n        // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n        // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n        // images causes a flicker. Putting a new image over the top does not\\n        const previewImage = new Image();\\n        previewImage.src = thumbUrl;\\n        previewImage.dataset.index = thumbNum;\\n        previewImage.dataset.filename = thumbFilename;\\n        this.showingThumbFilename = thumbFilename;\\n        this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n        // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n        previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n        this.loadingImage = previewImage;\\n        this.removeOldImages(previewImage);\\n      } else {\\n        // Update the existing image\\n        this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n        this.currentImageElement.dataset.index = thumbNum;\\n        this.removeOldImages(this.currentImageElement);\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n      this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n      this.setImageSizeAndOffset(previewImage, frame);\\n      if (newImage) {\\n        this.currentImageContainer.appendChild(previewImage);\\n        this.currentImageElement = previewImage;\\n        if (!this.loadedImages.includes(thumbFilename)) {\\n          this.loadedImages.push(thumbFilename);\\n        }\\n      }\\n\\n      // Preload images before and after the current one\\n      // Show higher quality of the same frame\\n      // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n      this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n    });\\n    // Remove all preview images that aren't the designated current image\\n    _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n      // Get a list of all images, convert it from a DOM list to an array\\n      Array.from(this.currentImageContainer.children).forEach(image => {\\n        if (image.tagName.toLowerCase() !== 'img') {\\n          return;\\n        }\\n        const removeDelay = this.usingSprites ? 500 : 1000;\\n        if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n          // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n          // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n          // eslint-disable-next-line no-param-reassign\\n          image.dataset.deleting = true;\\n\\n          // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n          const {\\n            currentImageContainer\\n          } = this;\\n          setTimeout(() => {\\n            currentImageContainer.removeChild(image);\\n            this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n          }, removeDelay);\\n        }\\n      });\\n    });\\n    // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n    // This will only preload the lowest quality\\n    _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n      return new Promise(resolve => {\\n        setTimeout(() => {\\n          const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n          if (this.showingThumbFilename === oldThumbFilename) {\\n            // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n            let thumbnailsClone;\\n            if (forward) {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n            } else {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n            }\\n            let foundOne = false;\\n            thumbnailsClone.forEach(frame => {\\n              const newThumbFilename = frame.text;\\n              if (newThumbFilename !== oldThumbFilename) {\\n                // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                if (!this.loadedImages.includes(newThumbFilename)) {\\n                  foundOne = true;\\n                  this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                  const {\\n                    urlPrefix\\n                  } = this.thumbnails[0];\\n                  const thumbURL = urlPrefix + newThumbFilename;\\n                  const previewImage = new Image();\\n                  previewImage.src = thumbURL;\\n                  previewImage.onload = () => {\\n                    this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                    if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                    // We don't resolve until the thumb is loaded\\n                    resolve();\\n                  };\\n                }\\n              }\\n            });\\n\\n            // If there are none to preload then we want to resolve immediately\\n            if (!foundOne) {\\n              resolve();\\n            }\\n          }\\n        }, 300);\\n      });\\n    });\\n    // If user has been hovering current image for half a second, look for a higher quality one\\n    _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n      if (currentQualityIndex < this.thumbnails.length - 1) {\\n        // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n        let previewImageHeight = previewImage.naturalHeight;\\n        if (this.usingSprites) {\\n          previewImageHeight = frame.h;\\n        }\\n        if (previewImageHeight < this.thumbContainerHeight) {\\n          // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n          setTimeout(() => {\\n            // Make sure the mouse hasn't already moved on and started hovering at another image\\n            if (this.showingThumbFilename === thumbFilename) {\\n              this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n              this.loadImage(currentQualityIndex + 1);\\n            }\\n          }, 300);\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n      this.elements.thumb.container.classList.toggle(className, toggle);\\n      if (!toggle && clearShowing) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n      this.elements.scrubbing.container.classList.toggle(className, toggle);\\n      if (!toggle) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n      if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n        // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n        this.sizeSpecifiedInCSS = true;\\n      }\\n    });\\n    // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n    _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n      const {\\n        imageContainer\\n      } = this.elements.thumb;\\n      if (!this.sizeSpecifiedInCSS) {\\n        const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n        imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n        const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n        const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n        imageContainer.style.height = `${thumbHeight}px`;\\n      }\\n      this.setThumbContainerPos();\\n    });\\n    _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n      const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n      const containerRect = this.player.elements.container.getBoundingClientRect();\\n      const {\\n        container\\n      } = this.elements.thumb;\\n      // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n      const min = containerRect.left - scrubberRect.left + 10;\\n      const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n      // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n      const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n      const clamped = clamp(position, min, max);\\n\\n      // Move the popover position\\n      container.style.left = `${clamped}px`;\\n\\n      // The arrow can follow the cursor\\n      container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n    });\\n    // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n    _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n      const {\\n        width,\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      this.elements.scrubbing.container.style.width = `${width}px`;\\n      this.elements.scrubbing.container.style.height = `${height}px`;\\n    });\\n    // Sprites need to be offset to the correct location\\n    _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n      if (!this.usingSprites) return;\\n\\n      // Find difference between height and preview container height\\n      const multiplier = this.thumbContainerHeight / frame.h;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.left = `-${frame.x * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.top = `-${frame.y * multiplier}px`;\\n    });\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {}\\n    };\\n    this.load();\\n  }\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const {\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach(attribute => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(this, () => {\\n      // Reset quality options\\n      this.options.quality = [];\\n\\n      // Remove elements\\n      removeElement(this.media);\\n      this.media = null;\\n\\n      // Reset class name\\n      if (is.element(this.elements.container)) {\\n        this.elements.container.removeAttribute('class');\\n      }\\n\\n      // Set the type and provider\\n      const {\\n        sources,\\n        type\\n      } = input;\\n      const [{\\n        provider = providers.html5,\\n        src\\n      }] = sources;\\n      const tagName = provider === 'html5' ? type : 'div';\\n      const attributes = provider === 'html5' ? {} : {\\n        src\\n      };\\n      Object.assign(this, {\\n        provider,\\n        type,\\n        // Check for support\\n        supported: support.check(type, provider, this.config.playsinline),\\n        // Create new element\\n        media: createElement(tagName, attributes)\\n      });\\n\\n      // Inject the new element\\n      this.elements.container.appendChild(this.media);\\n\\n      // Autoplay the new source?\\n      if (is.boolean(input.autoplay)) {\\n        this.config.autoplay = input.autoplay;\\n      }\\n\\n      // Set attributes for audio and video\\n      if (this.isHTML5) {\\n        if (this.config.crossorigin) {\\n          this.media.setAttribute('crossorigin', '');\\n        }\\n        if (this.config.autoplay) {\\n          this.media.setAttribute('autoplay', '');\\n        }\\n        if (!is.empty(input.poster)) {\\n          this.poster = input.poster;\\n        }\\n        if (this.config.loop.active) {\\n          this.media.setAttribute('loop', '');\\n        }\\n        if (this.config.muted) {\\n          this.media.setAttribute('muted', '');\\n        }\\n        if (this.config.playsinline) {\\n          this.media.setAttribute('playsinline', '');\\n        }\\n      }\\n\\n      // Restore class hook\\n      ui.addStyleHook.call(this);\\n\\n      // Set new sources for html5\\n      if (this.isHTML5) {\\n        source.insertElements.call(this, 'source', sources);\\n      }\\n\\n      // Set video title\\n      this.config.title = input.title;\\n\\n      // Set up from scratch\\n      media.setup.call(this);\\n\\n      // HTML5 stuff\\n      if (this.isHTML5) {\\n        // Setup captions\\n        if (Object.keys(input).includes('tracks')) {\\n          source.insertElements.call(this, 'track', input.tracks);\\n        }\\n      }\\n\\n      // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        // Setup interface\\n        ui.build.call(this);\\n      }\\n\\n      // Load HTML5 sources\\n      if (this.isHTML5) {\\n        this.media.load();\\n      }\\n\\n      // Update previewThumbnails config & reload plugin\\n      if (!is.empty(input.previewThumbnails)) {\\n        Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n        // Cleanup previewThumbnails plugin if it was loaded\\n        if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n          this.previewThumbnails.destroy();\\n          this.previewThumbnails = null;\\n        }\\n\\n        // Create new instance if it is still enabled\\n        if (this.config.previewThumbnails.enabled) {\\n          this.previewThumbnails = new PreviewThumbnails(this);\\n        }\\n      }\\n\\n      // Update the fullscreen support\\n      this.fullscreen.update();\\n    }, true);\\n  }\\n};\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    /**\\n     * Play the media, or play the advertisement (if they are not blocked)\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      if (!is.function(this.media.play)) {\\n        return null;\\n      }\\n\\n      // Intecept play with ads\\n      if (this.ads && this.ads.enabled) {\\n        this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n      }\\n\\n      // Return the promise (for HTML5)\\n      return this.media.play();\\n    });\\n    /**\\n     * Pause the media\\n     */\\n    _defineProperty$1(this, \\\"pause\\\", () => {\\n      if (!this.playing || !is.function(this.media.pause)) {\\n        return null;\\n      }\\n      return this.media.pause();\\n    });\\n    /**\\n     * Toggle playback based on current status\\n     * @param {Boolean} input\\n     */\\n    _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n      // Toggle based on current state if nothing passed\\n      const toggle = is.boolean(input) ? input : !this.playing;\\n      if (toggle) {\\n        return this.play();\\n      }\\n      return this.pause();\\n    });\\n    /**\\n     * Stop playback\\n     */\\n    _defineProperty$1(this, \\\"stop\\\", () => {\\n      if (this.isHTML5) {\\n        this.pause();\\n        this.restart();\\n      } else if (is.function(this.media.stop)) {\\n        this.media.stop();\\n      }\\n    });\\n    /**\\n     * Restart playback\\n     */\\n    _defineProperty$1(this, \\\"restart\\\", () => {\\n      this.currentTime = 0;\\n    });\\n    /**\\n     * Rewind\\n     * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n      this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Fast forward\\n     * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n      this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Increase volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n      const volume = this.media.muted ? 0 : this.volume;\\n      this.volume = volume + (is.number(step) ? step : 0);\\n    });\\n    /**\\n     * Decrease volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n      this.increaseVolume(-step);\\n    });\\n    /**\\n     * Trigger the airplay dialog\\n     * TODO: update player with state, support, enabled\\n     */\\n    _defineProperty$1(this, \\\"airplay\\\", () => {\\n      // Show dialog if supported\\n      if (support.airplay) {\\n        this.media.webkitShowPlaybackTargetPicker();\\n      }\\n    });\\n    /**\\n     * Toggle the player controls\\n     * @param {Boolean} [toggle] - Whether to show the controls\\n     */\\n    _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n      // Don't toggle if missing UI support or if it's audio\\n      if (this.supported.ui && !this.isAudio) {\\n        // Get state before change\\n        const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n        // Negate the argument if not undefined since adding the class to hides the controls\\n        const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n        // Apply and get updated state\\n        const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n        // Close menu\\n        if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n          controls.toggleMenu.call(this, false);\\n        }\\n\\n        // Trigger event on change\\n        if (hiding !== isHidden) {\\n          const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n          triggerEvent.call(this, this.media, eventName);\\n        }\\n        return !hiding;\\n      }\\n      return false;\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      on.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Add event listeners once\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n      once.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Remove event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n      off(this.elements.container, event, callback);\\n    });\\n    /**\\n     * Destroy an instance\\n     * Event listeners are removed when elements are removed\\n     * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n     * @param {Function} callback - Callback for when destroy is complete\\n     * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n     */\\n    _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n      if (!this.ready) {\\n        return;\\n      }\\n      const done = () => {\\n        // Reset overflow (incase destroyed while in fullscreen)\\n        document.body.style.overflow = '';\\n\\n        // GC for embed\\n        this.embed = null;\\n\\n        // If it's a soft destroy, make minimal changes\\n        if (soft) {\\n          if (Object.keys(this.elements).length) {\\n            // Remove elements\\n            removeElement(this.elements.buttons.play);\\n            removeElement(this.elements.captions);\\n            removeElement(this.elements.controls);\\n            removeElement(this.elements.wrapper);\\n\\n            // Clear for GC\\n            this.elements.buttons.play = null;\\n            this.elements.captions = null;\\n            this.elements.controls = null;\\n            this.elements.wrapper = null;\\n          }\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n        } else {\\n          // Unbind listeners\\n          unbindListeners.call(this);\\n\\n          // Cancel current network requests\\n          html5.cancelRequests.call(this);\\n\\n          // Replace the container with the original element provided\\n          replaceElement(this.elements.original, this.elements.container);\\n\\n          // Event\\n          triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback.call(this.elements.original);\\n          }\\n\\n          // Reset state\\n          this.ready = false;\\n\\n          // Clear for garbage collection\\n          setTimeout(() => {\\n            this.elements = null;\\n            this.media = null;\\n          }, 200);\\n        }\\n      };\\n\\n      // Stop playback\\n      this.stop();\\n\\n      // Clear timeouts\\n      clearTimeout(this.timers.loading);\\n      clearTimeout(this.timers.controls);\\n      clearTimeout(this.timers.resized);\\n\\n      // Provider specific stuff\\n      if (this.isHTML5) {\\n        // Restore native video controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Clean up\\n        done();\\n      } else if (this.isYouTube) {\\n        // Clear timers\\n        clearInterval(this.timers.buffering);\\n        clearInterval(this.timers.playing);\\n\\n        // Destroy YouTube API\\n        if (this.embed !== null && is.function(this.embed.destroy)) {\\n          this.embed.destroy();\\n        }\\n\\n        // Clean up\\n        done();\\n      } else if (this.isVimeo) {\\n        // Destroy Vimeo API\\n        // then clean up (wait, to prevent postmessage errors)\\n        if (this.embed !== null) {\\n          this.embed.unload().then(done);\\n        }\\n\\n        // Vimeo does not always return\\n        setTimeout(done, 200);\\n      }\\n    });\\n    /**\\n     * Check for support for a mime type (HTML5 only)\\n     * @param {String} type - Mime type\\n     */\\n    _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n      try {\\n        return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n      } catch (_) {\\n        return {};\\n      }\\n    })());\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {}\\n      }\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap()\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: []\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const _type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (_type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n        break;\\n      case 'video':\\n      case 'audio':\\n        this.type = _type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n        break;\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const {\\n      buffered\\n    } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({\\n        volume\\n      } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const {\\n      minimumSpeed: min,\\n      maximumSpeed: max\\n    } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n    if (!options.length) {\\n      return;\\n    }\\n    let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n    let updateStorage = true;\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({\\n        quality\\n      });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n         switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n             case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n             case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n             case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n             default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const {\\n      download\\n    } = this.config.urls;\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n    this.config.urls.download = input;\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n    this.config.ratio = reduceAspectRatio(input);\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const {\\n      toggled,\\n      currentTrack\\n    } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n    return targets.map(t => new Plyr(t, options));\\n  }\\n}\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport { Plyr as default };\\n//# sourceMappingURL=plyr.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: null,\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.mjs b/node_modules/plyr/dist/plyr.mjs\nindex 724e85d..53d8998 100644\n--- a/node_modules/plyr/dist/plyr.mjs\n+++ b/node_modules/plyr/dist/plyr.mjs\n@@ -3533,21 +3533,7 @@ const defaults = {\n     }\n   },\n   // URLs\n-  urls: {\n-    download: null,\n-    vimeo: {\n-      sdk: 'https://player.vimeo.com/api/player.js',\n-      iframe: 'https://player.vimeo.com/video/{0}?{1}',\n-      api: 'https://vimeo.com/api/oembed.json?url={0}'\n-    },\n-    youtube: {\n-      sdk: 'https://www.youtube.com/iframe_api',\n-      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\n-    },\n-    googleIMA: {\n-      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\n-    }\n-  },\n+  urls: null,\n   // Custom control listeners\n   listeners: {\n     seek: null,\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.js b/node_modules/plyr/dist/plyr.polyfilled.js\nindex d0ee402..16cff74 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.js\n+++ b/node_modules/plyr/dist/plyr.polyfilled.js\n@@ -4015,21 +4015,7 @@ typeof navigator === \"object\" && (function (global, factory) {\n       }\n     },\n     // URLs\n-    urls: {\n-      download: null,\n-      vimeo: {\n-        sdk: 'https://player.vimeo.com/api/player.js',\n-        iframe: 'https://player.vimeo.com/video/{0}?{1}',\n-        api: 'https://vimeo.com/api/oembed.json?url={0}'\n-      },\n-      youtube: {\n-        sdk: 'https://www.youtube.com/iframe_api',\n-        api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\n-      },\n-      googleIMA: {\n-        sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\n-      }\n-    },\n+    urls: null,\n     // Custom control listeners\n     listeners: {\n       seek: null,\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.min.js b/node_modules/plyr/dist/plyr.polyfilled.min.js\nindex a20aa09..1f4ae65 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.min.js\n+++ b/node_modules/plyr/dist/plyr.polyfilled.min.js\n@@ -1,2 +1,2 @@\n-\"object\"==typeof navigator&&function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(\"Plyr\",t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).Plyr=t()}(this,(function(){\"use strict\";!function(){if(\"undefined\"!=typeof window)try{var e=new window.CustomEvent(\"test\",{cancelable:!0});if(e.preventDefault(),!0!==e.defaultPrevented)throw new Error(\"Could not prevent default\")}catch(e){var t=function(e,t){var i,s;return(t=t||{}).bubbles=!!t.bubbles,t.cancelable=!!t.cancelable,(i=document.createEvent(\"CustomEvent\")).initCustomEvent(e,t.bubbles,t.cancelable,t.detail),s=i.preventDefault,i.preventDefault=function(){s.call(this);try{Object.defineProperty(this,\"defaultPrevented\",{get:function(){return!0}})}catch(e){this.defaultPrevented=!0}},i};t.prototype=window.Event.prototype,window.CustomEvent=t}}();var e=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function t(e,t,i){return(t=function(e){var t=function(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}(e,\"string\");return\"symbol\"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function i(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function s(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function n(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function a(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?n(Object(i),!0).forEach((function(t){s(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):n(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}!function(e){var t=function(){try{return!!Symbol.iterator}catch(e){return!1}}(),i=function(e){var i={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return t&&(i[Symbol.iterator]=function(){return i}),i},s=function(e){return encodeURIComponent(e).replace(/%20/g,\"+\")},n=function(e){return decodeURIComponent(String(e).replace(/\\+/g,\" \"))};(function(){try{var t=e.URLSearchParams;return\"a=1\"===new t(\"?a=1\").toString()&&\"function\"==typeof t.prototype.set&&\"function\"==typeof t.prototype.entries}catch(e){return!1}})()||function(){var n=function(e){Object.defineProperty(this,\"_entries\",{writable:!0,value:{}});var t=typeof e;if(\"undefined\"===t);else if(\"string\"===t)\"\"!==e&&this._fromString(e);else if(e instanceof n){var i=this;e.forEach((function(e,t){i.append(t,e)}))}else{if(null===e||\"object\"!==t)throw new TypeError(\"Unsupported input's type for URLSearchParams\");if(\"[object Array]\"===Object.prototype.toString.call(e))for(var s=0;s<e.length;s++){var a=e[s];if(\"[object Array]\"!==Object.prototype.toString.call(a)&&2===a.length)throw new TypeError(\"Expected [string, any] as entry at index \"+s+\" of URLSearchParams's input\");this.append(a[0],a[1])}else for(var r in e)e.hasOwnProperty(r)&&this.append(r,e[r])}},a=n.prototype;a.append=function(e,t){e in this._entries?this._entries[e].push(String(t)):this._entries[e]=[String(t)]},a.delete=function(e){delete this._entries[e]},a.get=function(e){return e in this._entries?this._entries[e][0]:null},a.getAll=function(e){return e in this._entries?this._entries[e].slice(0):[]},a.has=function(e){return e in this._entries},a.set=function(e,t){this._entries[e]=[String(t)]},a.forEach=function(e,t){var i;for(var s in this._entries)if(this._entries.hasOwnProperty(s)){i=this._entries[s];for(var n=0;n<i.length;n++)e.call(t,i[n],s,this)}},a.keys=function(){var e=[];return this.forEach((function(t,i){e.push(i)})),i(e)},a.values=function(){var e=[];return this.forEach((function(t){e.push(t)})),i(e)},a.entries=function(){var e=[];return this.forEach((function(t,i){e.push([i,t])})),i(e)},t&&(a[Symbol.iterator]=a.entries),a.toString=function(){var e=[];return this.forEach((function(t,i){e.push(s(i)+\"=\"+s(t))})),e.join(\"&\")},e.URLSearchParams=n}();var a=e.URLSearchParams.prototype;\"function\"!=typeof a.sort&&(a.sort=function(){var e=this,t=[];this.forEach((function(i,s){t.push([s,i]),e._entries||e.delete(s)})),t.sort((function(e,t){return e[0]<t[0]?-1:e[0]>t[0]?1:0})),e._entries&&(e._entries={});for(var i=0;i<t.length;i++)this.append(t[i][0],t[i][1])}),\"function\"!=typeof a._fromString&&Object.defineProperty(a,\"_fromString\",{enumerable:!1,configurable:!1,writable:!1,value:function(e){if(this._entries)this._entries={};else{var t=[];this.forEach((function(e,i){t.push(i)}));for(var i=0;i<t.length;i++)this.delete(t[i])}var s,a=(e=e.replace(/^\\?/,\"\")).split(\"&\");for(i=0;i<a.length;i++)s=a[i].split(\"=\"),this.append(n(s[0]),s.length>1?n(s[1]):\"\")}})}(void 0!==e?e:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:e),function(e){if(function(){try{var t=new e.URL(\"b\",\"http://a\");return t.pathname=\"c d\",\"http://a/c%20d\"===t.href&&t.searchParams}catch(e){return!1}}()||function(){var t=e.URL,i=function(t,i){\"string\"!=typeof t&&(t=String(t)),i&&\"string\"!=typeof i&&(i=String(i));var s,n=document;if(i&&(void 0===e.location||i!==e.location.href)){i=i.toLowerCase(),(s=(n=document.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=i,n.head.appendChild(s);try{if(0!==s.href.indexOf(i))throw new Error(s.href)}catch(e){throw new Error(\"URL unable to set base \"+i+\" due to \"+e)}}var a=n.createElement(\"a\");a.href=t,s&&(n.body.appendChild(a),a.href=a.href);var r=n.createElement(\"input\");if(r.type=\"url\",r.value=t,\":\"===a.protocol||!/:/.test(a.href)||!r.checkValidity()&&!i)throw new TypeError(\"Invalid URL\");Object.defineProperty(this,\"_anchorElement\",{value:a});var o=new e.URLSearchParams(this.search),l=!0,c=!0,u=this;[\"append\",\"delete\",\"set\"].forEach((function(e){var t=o[e];o[e]=function(){t.apply(o,arguments),l&&(c=!1,u.search=o.toString(),c=!0)}})),Object.defineProperty(this,\"searchParams\",{value:o,enumerable:!0});var h=void 0;Object.defineProperty(this,\"_updateSearchParams\",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==h&&(h=this.search,c&&(l=!1,this.searchParams._fromString(this.search),l=!0))}})},s=i.prototype;[\"hash\",\"host\",\"hostname\",\"port\",\"protocol\"].forEach((function(e){!function(e){Object.defineProperty(s,e,{get:function(){return this._anchorElement[e]},set:function(t){this._anchorElement[e]=t},enumerable:!0})}(e)})),Object.defineProperty(s,\"search\",{get:function(){return this._anchorElement.search},set:function(e){this._anchorElement.search=e,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\\?$/,\"\")},set:function(e){this._anchorElement.href=e,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\\/?)/,\"/\")},set:function(e){this._anchorElement.pathname=e},enumerable:!0},origin:{get:function(){var e={\"http:\":80,\"https:\":443,\"ftp:\":21}[this._anchorElement.protocol],t=this._anchorElement.port!=e&&\"\"!==this._anchorElement.port;return this._anchorElement.protocol+\"//\"+this._anchorElement.hostname+(t?\":\"+this._anchorElement.port:\"\")},enumerable:!0},password:{get:function(){return\"\"},set:function(e){},enumerable:!0},username:{get:function(){return\"\"},set:function(e){},enumerable:!0}}),i.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)},i.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)},e.URL=i}(),void 0!==e.location&&!(\"origin\"in e.location)){var t=function(){return e.location.protocol+\"//\"+e.location.hostname+(e.location.port?\":\"+e.location.port:\"\")};try{Object.defineProperty(e.location,\"origin\",{get:t,enumerable:!0})}catch(i){setInterval((function(){e.location.origin=t()}),100)}}}(void 0!==e?e:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:e);var r={addCSS:!0,thumbWidth:15,watch:!0};var o=function(e){return null!=e?e.constructor:null},l=function(e,t){return!!(e&&t&&e instanceof t)},c=function(e){return null==e},u=function(e){return o(e)===Object},h=function(e){return o(e)===String},d=function(e){return Array.isArray(e)},m=function(e){return l(e,NodeList)},p={nullOrUndefined:c,object:u,number:function(e){return o(e)===Number&&!Number.isNaN(e)},string:h,boolean:function(e){return o(e)===Boolean},function:function(e){return o(e)===Function},array:d,nodeList:m,element:function(e){return l(e,Element)},event:function(e){return l(e,Event)},empty:function(e){return c(e)||(h(e)||d(e)||m(e))&&!e.length||u(e)&&!Object.keys(e).length}};function g(e,t){if(1>t){var i=function(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var f=function(){function e(t,i){(function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")})(this,e),p.element(t)?this.element=t:p.string(t)&&(this.element=document.querySelector(t)),p.element(this.element)&&p.empty(this.element.rangeTouch)&&(this.config=a({},r,{},i),this.init())}return function(e,t,s){t&&i(e.prototype,t),s&&i(e,s)}(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!p.event(t))return null;var i,s=t.target,n=t.changedTouches[0],a=parseFloat(s.getAttribute(\"min\"))||0,r=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),a+g(i/100*(r-a),o)}},{key:\"set\",value:function(t){e.enabled&&p.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),function(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(p.empty(t)||p.string(t)?s=Array.from(document.querySelectorAll(p.string(t)?t:'input[type=\"range\"]')):p.element(t)?s=[t]:p.nodeList(t)?s=Array.from(t):p.array(t)&&(s=t.filter(p.element)),p.empty(s))return null;var n=a({},r,{},i);if(p.string(t)&&n.watch){var o=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){p.element(i)&&function(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}(i,t)&&new e(i,n)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const y=e=>null!=e?e.constructor:null,b=(e,t)=>Boolean(e&&t&&e instanceof t),v=e=>null==e,w=e=>y(e)===Object,T=e=>y(e)===String,k=e=>\"function\"==typeof e,E=e=>Array.isArray(e),C=e=>b(e,NodeList),S=e=>v(e)||(T(e)||E(e)||C(e))&&!e.length||w(e)&&!Object.keys(e).length;var A={nullOrUndefined:v,object:w,number:e=>y(e)===Number&&!Number.isNaN(e),string:T,boolean:e=>y(e)===Boolean,function:k,array:E,weakMap:e=>b(e,WeakMap),nodeList:C,element:e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,textNode:e=>y(e)===Text,event:e=>b(e,Event),keyboardEvent:e=>b(e,KeyboardEvent),cue:e=>b(e,window.TextTrackCue)||b(e,window.VTTCue),track:e=>b(e,TextTrack)||!v(e)&&T(e.kind),promise:e=>b(e,Promise)&&k(e.then),url:e=>{if(b(e,window.URL))return!0;if(!T(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!S(new URL(t).hostname)}catch(e){return!1}},empty:S};const P=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!A.string(i)&&t[i]})();function M(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}var x={isIE:Boolean(window.document.documentMode),isEdge:/Edge/g.test(navigator.userAgent),isWebKit:\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone:/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS:\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos:/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1};function L(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function N(e={},...t){if(!t.length)return e;const i=t.shift();return A.object(i)?(Object.keys(i).forEach((t=>{A.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),N(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),N(e,...t)):e}function _(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,a=e.nextSibling;s.appendChild(e),a?n.insertBefore(s,a):n.appendChild(s)}))}function I(e,t){A.element(e)&&!A.empty(t)&&Object.entries(t).filter((([,e])=>!A.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function O(e,t,i){const s=document.createElement(e);return A.object(t)&&I(s,t),A.string(i)&&(s.innerText=i),s}function $(e,t,i,s){A.element(t)&&t.appendChild(O(e,i,s))}function j(e){A.nodeList(e)||A.array(e)?Array.from(e).forEach(j):A.element(e)&&A.element(e.parentNode)&&e.parentNode.removeChild(e)}function R(e){if(!A.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function D(e,t){return A.element(t)&&A.element(t.parentNode)&&A.element(e)?(t.parentNode.replaceChild(e,t),e):null}function q(e,t){if(!A.string(e)||A.empty(e))return{};const i={},s=N({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),a=t.replace(/[[\\]]/g,\"\").split(\"=\"),[r]=a,o=a.length>1?a[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":A.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[r]=o}})),N(s,i)}function H(e,t){if(!A.element(e))return;let i=t;A.boolean(i)||(i=!e.hidden),e.hidden=i}function F(e,t,i){if(A.nodeList(e))return Array.from(e).map((e=>F(e,t,i)));if(A.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function U(e,t){return A.element(e)&&e.classList.contains(t)}function V(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function B(e){return this.elements.container.querySelectorAll(e)}function W(e){return this.elements.container.querySelector(e)}function z(e=null,t=!1){A.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const K={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},Y={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=Y[e]||\"html5\"!==t;return{api:i,ui:i&&Y.rangeInput}},pip:!(x.isIPhone||!A.function(O(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||O(\"video\").disablePictureInPicture)),airplay:A.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(A.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(K).includes(i)&&(i+=`; codecs=\"${K[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==P,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},Q=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function X(e,t,i,s=!1,n=!0,a=!1){if(!e||!(\"addEventListener\"in e)||A.empty(t)||!A.function(i))return;const r=t.split(\" \");let o=a;Q&&(o={passive:n,capture:a}),r.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function J(e,t=\"\",i,s=!0,n=!1){X.call(this,e,t,i,!0,s,n)}function G(e,t=\"\",i,s=!0,n=!1){X.call(this,e,t,i,!1,s,n)}function Z(e,t=\"\",i,s=!0,n=!1){const a=(...r)=>{G(e,t,a,s,n),i.apply(this,r)};X.call(this,e,t,a,!0,s,n)}function ee(e,t=\"\",i=!1,s={}){if(!A.element(e)||A.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function te(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ie(){return new Promise((e=>this.ready?setTimeout(e,0):J.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function se(e){A.promise(e)&&e.then(null,(()=>{}))}function ne(e){return A.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function ae(e,t){return A.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function re(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const oe=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function le(e){if(!(A.array(e)||A.string(e)&&e.includes(\":\")))return!1;return(A.array(e)?e:e.split(\":\")).map(Number).every(A.number)}function ce(e){if(!A.array(e)||!e.every(A.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function ue(e){const t=e=>le(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!A.empty(this.embed)&&A.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return ce(i)}function he(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=ue.call(this,e);if(!A.array(i))return{};const[s,n]=ce(i),a=100/s*n;if(re(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${a}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-a)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:a,ratio:i}}function de(e,t,i=.05){const s=e/t,n=ae(Object.keys(oe),s);return Math.abs(n-s)<=i?oe[n]:[e,t]}const me={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!A.empty(t)||Y.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:me.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,A.empty(this.config.ratio)||he.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=me.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&A.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=me.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:a,readyState:r,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==a||r)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||se(e.play())})),e.media.load())}ee.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(j(me.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function pe(e,...t){return A.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}const ge=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),fe=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function ye(e=\"\"){let t=e.toString();return t=function(e=\"\"){let t=e.toString();return t=ge(t,\"-\",\" \"),t=ge(t,\"_\",\" \"),t=fe(t),ge(t,\" \",\"\")}(t),t.charAt(0).toLowerCase()+t.slice(1)}function be(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const ve={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},we={get(e=\"\",t={}){if(A.empty(e)||A.empty(t))return\"\";let i=L(t.i18n,e);if(A.empty(i))return Object.keys(ve).includes(e)?ve[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=ge(i,e,t)})),i}};class Te{constructor(e){t(this,\"get\",(e=>{if(!Te.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(A.empty(t))return null;const i=JSON.parse(t);return A.string(e)&&e.length?i[e]:i})),t(this,\"set\",(e=>{if(!Te.supported||!this.enabled)return;if(!A.object(e))return;let t=this.get();A.empty(t)&&(t={}),N(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function ke(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function Ee(e,t){if(!A.string(e))return;const i=\"cache\",s=A.string(t);let n=!1;const a=()=>null!==document.getElementById(t),r=(e,t)=>{e.innerHTML=t,s&&a()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!a()){const a=Te.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),a){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);r(o,t.content)}}ke(e).then((e=>{if(!A.empty(e)){if(a)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}r(o,e)}})).catch((()=>{}))}}const Ce=e=>Math.trunc(e/60/60%60,10),Se=e=>Math.trunc(e/60%60,10),Ae=e=>Math.trunc(e%60,10);function Pe(e=0,t=!1,i=!1){if(!A.number(e))return Pe(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=Ce(e);const a=Se(e),r=Ae(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(a)}:${s(r)}`}const Me={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||x.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=W.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:B.call(this,this.config.selectors.buttons.play),pause:W.call(this,this.config.selectors.buttons.pause),restart:W.call(this,this.config.selectors.buttons.restart),rewind:W.call(this,this.config.selectors.buttons.rewind),fastForward:W.call(this,this.config.selectors.buttons.fastForward),mute:W.call(this,this.config.selectors.buttons.mute),pip:W.call(this,this.config.selectors.buttons.pip),airplay:W.call(this,this.config.selectors.buttons.airplay),settings:W.call(this,this.config.selectors.buttons.settings),captions:W.call(this,this.config.selectors.buttons.captions),fullscreen:W.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=W.call(this,this.config.selectors.progress),this.elements.inputs={seek:W.call(this,this.config.selectors.inputs.seek),volume:W.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:W.call(this,this.config.selectors.display.buffer),currentTime:W.call(this,this.config.selectors.display.currentTime),duration:W.call(this,this.config.selectors.display.duration)},A.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=Me.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,a=document.createElementNS(i,\"svg\");I(a,N(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const r=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in r&&r.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),r.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),a.appendChild(r),a},createLabel(e,t={}){const i=we.get(e,this.config);return O(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(A.empty(e))return null;const t=O(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(O(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=N({},t);let s=ye(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||N(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:A.empty(n.label)&&(n.label=s),A.empty(n.icon)&&(n.icon=e)}const a=O(n.element);return n.toggle?(a.appendChild(Me.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),a.appendChild(Me.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),a.appendChild(Me.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),a.appendChild(Me.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(a.appendChild(Me.createIcon.call(this,n.icon)),a.appendChild(Me.createLabel.call(this,n.label))),N(i,q(this.config.selectors.buttons[s],i)),I(a,i),\"play\"===s?(A.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(a)):this.elements.buttons[s]=a,a},createRange(e,t){const i=O(\"input\",N(q(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":we.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,Me.updateRangeFill.call(this,i),f.setup(i),i},createProgress(e,t){const i=O(\"progress\",N(q(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(O(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?we.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=q(this.config.selectors.display[e],t),s=O(\"div\",N(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":we.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){J.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=V(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))Me.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,A.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,A.element(t)||(t=e.parentNode.lastElementChild)),z.call(this,t,!0))}}),!1),J.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&Me.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:a=!1}){const r=q(this.config.selectors.inputs[i]),o=O(\"button\",N(r,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${r.class?r.class:\"\"}`.trim(),\"aria-checked\":a,value:e})),l=O(\"span\");l.innerHTML=s,A.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>V(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!A.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}Me.showMenuPanel.call(this,\"home\",A.keyboardEvent(t))}}),i,!1),Me.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!A.number(e))return e;return Pe(e,Ce(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){A.element(e)&&A.number(t)&&(e.innerText=Me.formatTime(t,i))},updateVolume(){this.supported.ui&&(A.element(this.elements.inputs.volume)&&Me.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),A.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){A.element(e)&&(e.value=t,Me.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!A.event(e))return;let t=0;const i=(e,t)=>{const i=A.number(t)?t:0,s=A.element(e)?e:this.elements.display.buffer;if(A.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];A.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":s=this.currentTime,n=this.duration,t=0===s||0===n||Number.isNaN(s)||Number.isNaN(n)?0:(s/n*100).toFixed(2),\"timeupdate\"===e.type&&Me.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}var s,n},updateRangeFill(e){const t=A.event(e)?e.target:e;if(A.element(t)&&\"range\"===t.getAttribute(\"type\")){if(V(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=Me.formatTime(this.currentTime),i=Me.formatTime(this.duration),s=we.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(V(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(x.isWebKit||x.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!A.element(this.elements.inputs.seek)||!A.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,a=e=>F(s,n,e);if(this.touch)return void a(!1);let r=0;const o=this.elements.progress.getBoundingClientRect();if(A.event(e))r=100/o.width*(e.pageX-o.left);else{if(!U(s,n))return;r=parseFloat(s.style.left,10)}r<0?r=0:r>100&&(r=100);const l=this.duration/100*r;s.innerText=Me.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${r}%`,A.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&a(\"mouseenter\"===e.type)},timeUpdate(e){const t=!A.element(this.elements.display.duration)&&this.config.invertTime;Me.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||Me.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return H(this.elements.display.currentTime,!0),void H(this.elements.progress,!0);A.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=A.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&Me.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&Me.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&Me.setMarkers.call(this),Me.updateSeekTooltip.call(this)},toggleMenuButton(e,t){H(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,a=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=A.empty(i)?this[e]:i,A.empty(n)&&(n=this.config[e].default),!A.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(A.element(a)||(a=s&&s.querySelector('[role=\"menu\"]')),!A.element(a))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=Me.getLabel.call(this,e,n);const r=a&&a.querySelector(`[value=\"${n}\"]`);A.element(r)&&(r.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?we.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(A.number(t)){const e=we.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return fe(t);case\"captions\":return Ne.getLabel.call(this);default:return null}},setQualityMenu(e){if(!A.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');A.array(e)&&(this.options.quality=ne(e).filter((e=>this.config.quality.options.includes(e))));const s=!A.empty(this.options.quality)&&this.options.quality.length>1;if(Me.toggleMenuButton.call(this,t,s),R(i),Me.checkMenu.call(this),!s)return;const n=e=>{const t=we.get(`qualityBadge.${e}`,this.config);return t.length?Me.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{Me.createMenuItem.call(this,{value:e,list:i,type:t,title:Me.getLabel.call(this,\"quality\",e),badge:n(e)})})),Me.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!A.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=Ne.getTracks.call(this),s=Boolean(i.length);if(Me.toggleMenuButton.call(this,e,s),R(t),Me.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:Ne.getLabel.call(this,e),badge:e.language&&Me.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:we.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(Me.createMenuItem.bind(this)),Me.updateSetting.call(this,e,t)},setSpeedMenu(){if(!A.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!A.empty(this.options.speed)&&this.options.speed.length>1;Me.toggleMenuButton.call(this,e,i),R(t),Me.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{Me.createMenuItem.call(this,{value:i,list:t,type:e,title:Me.getLabel.call(this,\"speed\",i)})})),Me.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!A.empty(e)&&Object.values(e).some((e=>!e.hidden));H(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;A.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');z.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!A.element(t)||!A.element(i))return;const{hidden:s}=t;let n=s;if(A.boolean(e))n=e;else if(A.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(A.event(e)){const s=A.function(e.composedPath)?e.composedPath()[0]:e.target,a=t.contains(s);if(a||!a&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),H(t,!n),F(this.elements.container,this.config.classNames.menu.open,n),n&&A.keyboardEvent(e)?Me.focusFirstMenuItem.call(this,null,!0):n||s||z.call(this,i,A.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return j(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!A.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(Y.transitions&&!Y.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=Me.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",G.call(this,s,P,t))};J.call(this,s,P,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}H(n,!0),H(i,!1),Me.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;A.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:a,setQualityMenu:r,setSpeedMenu:o,showMenuPanel:l}=Me;this.elements.controls=null,A.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=O(\"div\",q(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return ne(A.array(this.config.controls)?this.config.controls:[]).forEach((r=>{if(\"restart\"===r&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===r&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===r&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===r&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===r){const t=O(\"div\",{class:`${u.class} plyr__progress__container`}),i=O(\"div\",q(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=O(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===r&&c.appendChild(a.call(this,\"currentTime\",u)),\"duration\"===r&&c.appendChild(a.call(this,\"duration\",u)),\"mute\"===r||\"volume\"===r){let{volume:t}=this.elements;if(A.element(t)&&c.contains(t)||(t=O(\"div\",N({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===r&&t.appendChild(i.call(this,\"mute\")),\"volume\"===r&&!x.isIos&&!x.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",N(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===r&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===r&&!A.empty(this.config.settings)){const s=O(\"div\",N({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=O(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),a=O(\"div\"),r=O(\"div\",{id:`plyr-settings-${e.id}-home`}),o=O(\"div\",{role:\"menu\"});r.appendChild(o),a.appendChild(r),this.elements.settings.panels.home=r,this.config.settings.forEach((i=>{const s=O(\"button\",N(q(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),J.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=O(\"span\",null,we.get(i,this.config)),r=O(\"span\",{class:this.config.classNames.menu.value});r.innerHTML=e[i],n.appendChild(r),s.appendChild(n),o.appendChild(s);const c=O(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=O(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(O(\"span\",{\"aria-hidden\":!0},we.get(i,this.config))),u.appendChild(O(\"span\",{class:this.config.classNames.hidden},we.get(\"menuBack\",this.config))),J.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),J.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(O(\"div\",{role:\"menu\"})),a.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(a),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===r&&Y.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===r&&Y.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===r){const e=N({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!A.url(t)&&this.isEmbed&&N(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===r&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&r.call(this,me.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=Me.getIconUrl.call(this);e.cors&&Ee(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;A.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),A.element(this.config.controls)||A.string(this.config.controls)?e=this.config.controls:(e=Me.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:Ne.getLabel.call(this)}),i=!1);let s;i&&A.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=ge(i,`{${e}}`,t)})),i})(e)),A.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),A.element(s)||(s=this.elements.container);if(s[A.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),A.element(this.elements.controls)||Me.findElements.call(this),!A.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>U(e,t),set(i=!1){F(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{A.array(t)||A.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(x.isEdge&&M(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=B.call(this,i);Array.from(s).forEach((e=>{F(e,this.config.classNames.hidden,!1),F(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let a=null;const r=`${this.config.classNames.tooltip}--visible`,o=e=>F(a,r,e);i.forEach((e=>{const t=O(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";a&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(a.style.left=i,a.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(a=O(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(a)),this.elements.markers={points:n,tip:a},this.elements.progress.appendChild(s)}};function xe(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function Le(e){const t=new URLSearchParams;return A.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const Ne={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!Y.textTracks)return void(A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Me.setCaptionsMenu.call(this));var e,t;if(A.element(this.elements.captions)||(this.elements.captions=O(\"div\",q(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),e=this.elements.captions,t=this.elements.wrapper,A.element(e)&&A.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)),x.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=xe(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&ke(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{j(e)}))}))}const i=ne((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let s=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===s&&([s]=i);let n=this.storage.get(\"captions\");if(A.boolean(n)||({active:n}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:n,language:s,languages:i}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";J.call(this,this.media.textTracks,e,Ne.update.bind(this))}setTimeout(Ne.update.bind(this),0)},update(){const e=Ne.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,a=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),J.call(this,e,\"cuechange\",(()=>Ne.updateCues.call(this)))})),(a&&this.language!==i||!e.includes(n))&&(Ne.setLanguage.call(this,i),Ne.toggle.call(this,t&&a)),this.elements&&F(this.elements.container,this.config.classNames.captions.enabled,!A.empty(e)),A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Me.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=A.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=Ne.getTracks.call(this),t=Ne.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void Ne.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),F(this.elements.container,s,n),this.captions.toggled=n,Me.updateSetting.call(this,\"captions\"),ee.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=Ne.getTracks.call(this);if(-1!==e)if(A.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,Me.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),ee.call(this,this.media,\"languagechange\")}Ne.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&Ne.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else Ne.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!A.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=Ne.getTracks.call(this),n=Ne.findTrack.call(this,[i]);Ne.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=Ne.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let a;return e.every((e=>(a=n.find((t=>t.language===e)),!a))),a||(t?n[0]:void 0)},getCurrentTrack(){return Ne.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!A.track(t)&&Y.textTracks&&this.captions.toggled&&(t=Ne.getCurrentTrack.call(this)),A.track(t)?A.empty(t.label)?A.empty(t.language)?we.get(\"enabled\",this.config):e.language.toUpperCase():t.label:we.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!A.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!A.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=Ne.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(be)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){R(this.elements.captions);const e=O(\"span\",q(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),ee.call(this,this.media,\"cuechange\")}}},_e={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:{download:null,vimeo:{sdk:\"https://player.vimeo.com/api/player.js\",iframe:\"https://player.vimeo.com/video/{0}?{1}\",api:\"https://vimeo.com/api/oembed.json?url={0}\"},youtube:{sdk:\"https://www.youtube.com/iframe_api\",api:\"https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}\"},googleIMA:{sdk:\"https://imasdk.googleapis.com/js/sdkloader/ima3.js\"}},listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},Ie=\"picture-in-picture\",Oe=\"inline\",$e={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},je=\"audio\",Re=\"video\";const De=()=>{};class qe{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):De}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):De}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):De}}class He{constructor(e){t(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;A.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;ee.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),t(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",F(this.target,this.player.config.classNames.fullscreen.fallback,e),x.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=A.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),t(this,\"trapFocus\",(e=>{if(x.isIos||x.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=B.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),t(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":He.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");F(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),t(this,\"enter\",(()=>{this.supported&&(x.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!He.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?A.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),t(this,\"exit\",(()=>{if(this.supported)if(x.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),se(this.player.play());else if(!He.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!A.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),t(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=He.prefix,this.property=He.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&function(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(V.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}(this.player.elements.container,e.config.fullscreen.container),J.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),J.call(this.player,this.player.elements.container,\"dblclick\",(e=>{A.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),J.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return He.nativeSupported&&!this.forceFallback}static get prefix(){if(A.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!A.function(document[`${t}ExitFullscreen`])&&!A.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,He.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||He.nativeSupported||!x.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!He.nativeSupported||this.forceFallback)return U(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return x.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function Fe(e,t=1){return new Promise(((i,s)=>{const n=new Image,a=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:a,onerror:a,src:e})}))}const Ue={addStyleHook(){F(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),F(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void Ue.toggleNativeControls.call(this,!0);A.element(this.elements.controls)||(Me.inject.call(this),this.listeners.controls()),Ue.toggleNativeControls.call(this),this.isHTML5&&Ne.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,Me.updateVolume.call(this),Me.timeUpdate.call(this),Me.durationUpdate.call(this),Ue.checkPlaying.call(this),F(this.elements.container,this.config.classNames.pip.supported,Y.pip&&this.isHTML5&&this.isVideo),F(this.elements.container,this.config.classNames.airplay.supported,Y.airplay&&this.isHTML5),F(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{ee.call(this,this.media,\"ready\")}),0),Ue.setTitle.call(this),this.poster&&Ue.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&Me.durationUpdate.call(this),this.config.mediaMetadata&&Me.setMediaMetadata.call(this)},setTitle(){let e=we.get(\"play\",this.config);if(A.string(this.config.title)&&!A.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=W.call(this,\"iframe\");if(!A.element(e))return;const t=A.empty(this.config.title)?\"video\":this.config.title,i=we.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){F(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ie.call(this).then((()=>Fe(e))).catch((t=>{throw e===this.poster&&Ue.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),Ue.togglePoster.call(this,!0),e))))},checkPlaying(e){F(this.elements.container,this.config.classNames.playing,this.playing),F(this.elements.container,this.config.classNames.paused,this.paused),F(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",we.get(this.playing?\"pause\":\"play\",this.config))})),A.event(e)&&\"timeupdate\"===e.type||Ue.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{F(this.elements.container,this.config.classNames.loading,this.loading),Ue.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!A.empty(e)&&A.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),A.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Ve{constructor(e){t(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,F(t.container,e.config.classNames.isTouch,!0)})),t(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&X.call(t,window,\"keydown keyup\",this.handleKey,e,!1),X.call(t,document.body,\"click\",this.toggleMenu,e),Z.call(t,document.body,\"touchstart\",this.firstTouch)})),t(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&J.call(e,i.container,\"keydown keyup\",this.handleKey,!1),J.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let a=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(Ue.toggleControls.call(e,!0),a=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>Ue.toggleControls.call(e,!1)),a)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,a]=ue.call(e),r=re(`aspect-ratio: ${n} / ${a}`);if(!s)return void(r?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)],c=o/l>n/a;r?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/a*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},a=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};J.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&A.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?J:G).call(e,window,\"resize\",a)}))})),t(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(J.call(e,e.media,\"timeupdate seeking seeked\",(t=>Me.timeUpdate.call(e,t))),J.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>Me.durationUpdate.call(e,t))),J.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),J.call(e,e.media,\"progress playing seeking seeked\",(t=>Me.updateProgress.call(e,t))),J.call(e,e.media,\"volumechange\",(t=>Me.updateVolume.call(e,t))),J.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>Ue.checkPlaying.call(e,t))),J.call(e,e.media,\"waiting canplay seeked playing\",(t=>Ue.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=W.call(e,`.${e.config.classNames.video}`);if(!A.element(i))return;J.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{se(e.play())}),\"play\")):this.proxy(s,(()=>{se(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&J.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),J.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),J.call(e,e.media,\"ratechange\",(()=>{Me.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),J.call(e,e.media,\"qualitychange\",(t=>{Me.updateSetting.call(e,\"quality\",null,t.detail.quality)})),J.call(e,e.media,\"ready qualitychange\",(()=>{Me.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");J.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),ee.call(e,t.container,i.type,!0,s)}))})),t(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let a=!0;A.function(n)&&(a=n.call(s,e)),!1!==a&&A.function(t)&&t.call(s,e)})),t(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:a}=this,r=a.config.listeners[s],o=A.function(r);J.call(a,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),t(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=x.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{se(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{ee.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),Me.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),Me.toggleMenu.call(e,t)):Me.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&Me.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(A.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),a=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&a?(i.removeAttribute(s),se(e.play())):!a&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),x.isIos){const t=B.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>M(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");A.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>Me.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),x.isWebKit&&Array.from(B.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>Me.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!A.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,Me.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;F(t.controls,i.classNames.noTransition,!0),Ue.toggleControls.call(e,!0),setTimeout((()=>{F(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>Ue.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),a=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(a/50);const{volume:r}=e.media;(1===a&&r<1||-1===a&&r>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:a,ctrlKey:r,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(a||r||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(A.element(n)){const{editable:s}=t.config.selectors,{seek:a}=i.inputs;if(n!==a&&V(n,s))return;if(\" \"===e.key&&V(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||se(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){Me.toggleMenu.call(this.player,e)}}var Be=function(e,t){return e(t={exports:{}},t.exports),t.exports}((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,a,r,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)a=e[l],(r=i[a])?n(a,r):(s[a]=s[a]||[]).push(n)}function a(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function r(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var a,r,l=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((r=l.createElement(\"link\")).rel=\"stylesheet\",r.href=m,(a=\"hideFocus\"in r)&&r.relList&&(a=0,r.rel=\"preload\",r.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(r=l.createElement(\"img\")).src=m:((r=l.createElement(\"script\")).src=t,r.async=void 0===c||c),r.onload=r.onerror=r.onbeforeload=function(e){var l=e.type[0];if(a)try{r.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==r.rel&&\"style\"==r.as)return r.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==h(t,r)&&l.head.appendChild(r)}function l(e,t,i){var s,n,a=(e=e.push?e:[e]).length,r=a,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--a||t(l)},n=0;n<r;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){r(o,e),t&&r({success:t,error:i},e),a(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){r(t,e)})),c},c.done=function(e){a(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function We(e){return new Promise(((t,i)=>{Be(e,{success:t,error:i})}))}function ze(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,ee.call(this,this.media,e?\"play\":\"pause\"))}const Ke={setup(){const e=this;F(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,he.call(e),A.object(window.Vimeo)?Ke.ready.call(e):We(e.config.urls.vimeo.sdk).then((()=>{Ke.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let a=e.media.getAttribute(\"src\"),r=\"\";A.empty(a)?(a=e.media.getAttribute(e.config.attributes.embed.id),r=e.media.getAttribute(e.config.attributes.embed.hash)):r=function(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}(a);const o=r?{h:r}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=Le({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=(u=a,A.empty(u)?null:A.number(Number(u))?u:u.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:u);var u;const h=O(\"iframe\"),d=pe(e.config.urls.vimeo.iframe,c,l);if(h.setAttribute(\"src\",d),h.setAttribute(\"allowfullscreen\",\"\"),h.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),A.empty(s)||h.setAttribute(\"referrerPolicy\",s),i||!t.customControls)h.setAttribute(\"data-poster\",e.poster),e.media=D(h,e.media);else{const t=O(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(h),e.media=D(t,e.media)}t.customControls||ke(pe(e.config.urls.vimeo.api,d)).then((t=>{!A.empty(t)&&t.thumbnail_url&&Ue.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(h,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(ze.call(e,!0),e.embed.play()),e.media.pause=()=>(ze.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:m}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>m,set(t){const{embed:i,media:s,paused:n,volume:a}=e,r=n&&!i.hasPlayed;s.seeking=!0,ee.call(e,s,\"seeking\"),Promise.resolve(r&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>r&&i.pause())).then((()=>r&&i.setVolume(a))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,ee.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:g}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>g,set(t){e.embed.setVolume(t).then((()=>{g=t,ee.call(e,e.media,\"volumechange\")}))}});let{muted:f}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>f,set(t){const i=!!A.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{f=i,ee.call(e,e.media,\"volumechange\")}))}});let y,{loop:b}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>b,set(t){const i=A.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{b=i}))}}),e.embed.getVideoUrl().then((t=>{y=t,Me.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>y}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=de(i,s),he.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,Ue.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{m=t,ee.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,ee.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,Ne.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>function(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}(e.text)));Ne.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{ze.call(e,!t),t||ee.call(e,e.media,\"playing\")})),A.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{ee.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{ee.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{ze.call(e,!0),ee.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{ze.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,m=t.seconds,ee.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,ee.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&ee.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,ee.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,ee.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,ee.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,ee.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>Ue.build.call(e)),0)}};function Ye(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,ee.call(this,this.media,e?\"play\":\"pause\"))}function Qe(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const Xe={setup(){if(F(this.elements.wrapper,this.config.classNames.embed,!0),A.object(window.YT)&&A.function(window.YT.Player))Xe.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{A.function(e)&&e(),Xe.ready.call(this)},We(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){ke(pe(this.config.urls.youtube.api,e)).then((e=>{if(A.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,Ue.setTitle.call(this),this.embed.ratio=de(s,i)}he.call(this)})).catch((()=>{he.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!A.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");A.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=(a=s,A.empty(a)?null:a.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:a);var a;const r=O(\"div\",{id:`${e.provider}-${Math.floor(1e4*Math.random())}`,\"data-poster\":t.customControls?e.poster:void 0});if(e.media=D(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;Fe(t(\"maxres\"),121).catch((()=>Fe(t(\"sd\"),121))).catch((()=>Fe(t(\"hq\")))).then((t=>Ue.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:Qe(t),playerVars:N({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},ee.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),ee.call(e,e.media,\"ratechange\")},onReady(i){if(A.function(e.media.play))return;const s=i.target;Xe.getTitle.call(e,n),e.media.play=()=>{Ye.call(e,!0),s.playVideo()},e.media.pause=()=>{Ye.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,ee.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:a}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>a,set(t){a=t,s.setVolume(100*a),ee.call(e,e.media,\"volumechange\")}});let{muted:r}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>r,set(t){const i=A.boolean(t)?t:r;r=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*a),ee.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),ee.call(e,e.media,\"timeupdate\"),ee.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&ee.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),ee.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>Ue.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,ee.call(e,e.media,\"seeked\")),i.data){case-1:ee.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),ee.call(e,e.media,\"progress\");break;case 0:Ye.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):ee.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(Ye.call(e,!0),ee.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{ee.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),ee.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),Ye.call(e,!1);break;case 3:ee.call(e,e.media,\"waiting\")}ee.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},Je={setup(){this.media?(F(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),F(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&F(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=O(\"div\",{class:this.config.classNames.video}),_(this.media,this.elements.wrapper),this.elements.poster=O(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?me.setup.call(this):this.isYouTube?Xe.setup.call(this):this.isVimeo&&Ke.setup.call(this)):this.debug.warn(\"No media element found!\")}};class Ge{constructor(e){t(this,\"load\",(()=>{this.enabled&&(A.object(window.google)&&A.object(window.google.ima)?this.ready():We(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),t(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),t(this,\"setupIMA\",(()=>{this.elements.container=O(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),t(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),t(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=Pe(Math.max(this.manager.getRemainingTime(),0)),t=`${we.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),t(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),t(this,\"addCuePoints\",(()=>{A.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(A.element(t)){const i=100/this.player.duration*e,s=O(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),t(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{ee.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),t(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),t(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;A.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),t(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),t(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,se(this.player.media.play())})),t(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),t(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),t(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),t(this,\"trigger\",((e,...t)=>{const i=this.events[e];A.array(i)&&i.forEach((e=>{A.function(e)&&e.apply(this,t)}))})),t(this,\"on\",((e,t)=>(A.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),t(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),t(this,\"clearSafetyTimer\",(e=>{A.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!A.empty(e.publisherId)||A.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(A.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${Le({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function Ze(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const et=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(A.number(i.startTime)){if(!A.empty(e.trim())&&A.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},tt=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class it{constructor(e){t(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),t(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(A.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(A.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(A.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),t(this,\"getThumbnail\",(e=>new Promise((t=>{ke(e).then((i=>{const s={frames:et(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),t(this,\"startMove\",(e=>{if(this.loaded&&A.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=Pe(this.seekTime);const a=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));a&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${a.label}<br>`)}this.showImageAtCurrentTime()}})),t(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),t(this,\"startScrubbing\",(e=>{(A.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),t(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):Z.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),t(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),t(this,\"render\",(()=>{this.elements.thumb.container=O(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=O(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=O(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=O(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),A.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=O(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),t(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),t(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),t(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],a=i.frames[t].text,r=s+a;if(this.currentImageElement&&this.currentImageElement.dataset.filename===a)this.showImage(this.currentImageElement,n,e,t,a,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=r,i.dataset.index=t,i.dataset.filename=a,this.showingThumbFilename=a,this.player.debug.log(`Loading image: ${r}`),i.onload=()=>this.showImage(i,n,e,t,a,!0),this.loadingImage=i,this.removeOldImages(i)}})),t(this,\"showImage\",((e,t,i,s,n,a=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${a}`),this.setImageSizeAndOffset(e,t),a&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),t(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),t(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let a=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){a=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),a||i()}}),300)})))),t(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),t(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),t(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),t(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),t(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),t(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,a=this.mousePosX-e.left-i.clientWidth/2,r=Ze(a,s,n);i.style.left=`${r}px`,i.style.setProperty(\"--preview-arrow-offset\",a-r+\"px\")})),t(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=tt(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),t(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=tt(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const st={insertElements(e,t){A.string(t)?$(e,this.media,{src:t}):A.array(t)&&t.forEach((t=>{$(e,this.media,t)}))},change(e){L(e,\"sources.length\")?(me.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],j(this.media),this.media=null,A.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=$e.html5,src:n}]=t,a=\"html5\"===s?i:\"div\",r=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:Y.check(i,s,this.config.playsinline),media:O(a,r)}),this.elements.container.appendChild(this.media),A.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),A.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),Ue.addStyleHook.call(this),this.isHTML5&&st.insertElements.call(this,\"source\",t),this.config.title=e.title,Je.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&st.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Ue.build.call(this),this.isHTML5&&this.media.load(),A.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class nt{constructor(e,i){if(t(this,\"play\",(()=>A.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>se(this.media.play()))),this.media.play()):null)),t(this,\"pause\",(()=>this.playing&&A.function(this.media.pause)?this.media.pause():null)),t(this,\"togglePlay\",(e=>(A.boolean(e)?e:!this.playing)?this.play():this.pause())),t(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):A.function(this.media.stop)&&this.media.stop()})),t(this,\"restart\",(()=>{this.currentTime=0})),t(this,\"rewind\",(e=>{this.currentTime-=A.number(e)?e:this.config.seekTime})),t(this,\"forward\",(e=>{this.currentTime+=A.number(e)?e:this.config.seekTime})),t(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(A.number(e)?e:0)})),t(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),t(this,\"airplay\",(()=>{Y.airplay&&this.media.webkitShowPlaybackTargetPicker()})),t(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=U(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=F(this.elements.container,this.config.classNames.hideControls,i);if(s&&A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!A.empty(this.config.settings)&&Me.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";ee.call(this,this.media,e)}return!s}return!1})),t(this,\"on\",((e,t)=>{J.call(this,this.elements.container,e,t)})),t(this,\"once\",((e,t)=>{Z.call(this,this.elements.container,e,t)})),t(this,\"off\",((e,t)=>{G(this.elements.container,e,t)})),t(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(j(this.elements.buttons.play),j(this.elements.captions),j(this.elements.controls),j(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),A.function(e)&&e()):(te.call(this),me.cancelRequests.call(this),D(this.elements.original,this.elements.container),ee.call(this,this.elements.original,\"destroyed\",!0),A.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(Ue.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&A.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),t(this,\"supports\",(e=>Y.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=Y.touch,this.media=e,A.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||A.nodeList(this.media)||A.array(this.media))&&(this.media=this.media[0]),this.config=N({},_e,nt.defaults,i||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new qe(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",Y),A.nullOrUndefined(this.media)||!A.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!Y.check().api)return void this.debug.error(\"Setup failed: no support\");const s=this.media.cloneNode(!0);s.autoplay=!1,this.elements.original=s;const n=this.media.tagName.toLowerCase();let a=null,r=null;switch(n){case\"div\":if(a=this.media.querySelector(\"iframe\"),A.element(a)){if(r=xe(a.getAttribute(\"src\")),this.provider=function(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?$e.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?$e.vimeo:null}(r.toString()),this.elements.container=this.media,this.media=a,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(A.empty(this.provider)||!Object.values($e).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=Re;break;case\"video\":case\"audio\":this.type=n,this.provider=$e.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=Y.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Ve(this),this.storage=new Te(this),this.media.plyr=this,A.element(this.elements.container)||(this.elements.container=O(\"div\"),_(this.media,this.elements.container)),Ue.migrateStyles.call(this),Ue.addStyleHook.call(this),Je.setup.call(this),this.config.debug&&J.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new He(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Ue.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ge(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>se(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===$e.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===$e.youtube}get isVimeo(){return this.provider===$e.vimeo}get isVideo(){return this.type===Re}get isAudio(){return this.type===je}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=A.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return A.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=A.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;A.string(t)&&(t=Number(t)),A.number(t)||(t=this.storage.get(\"volume\")),A.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!A.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;A.boolean(t)||(t=this.storage.get(\"muted\")),A.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;A.number(e)&&(t=e),A.number(t)||(t=this.storage.get(\"speed\")),A.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=Ze(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!A.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(A.number),n=!0;if(!i.includes(s)){const e=ae(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=A.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){st.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return A.url(e)?e:this.source}set download(e){A.url(e)&&(this.config.urls.download=e,Me.setDownloadUrl.call(this))}set poster(e){this.isVideo?Ue.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=ce(ue.call(this));return A.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?A.string(e)&&le(e)?(this.config.ratio=ce(e),he.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=A.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){Ne.toggle.call(this,e,!1)}set currentTrack(e){Ne.set.call(this,e,!1),Ne.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){Ne.setLanguage.call(this,e,!1)}get language(){return(Ne.getCurrentTrack.call(this)||{}).language}set pip(e){if(!Y.pip)return;const t=A.boolean(e)?e:!this.pip;A.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?Ie:Oe),A.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return Y.pip?A.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===Ie:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))}static supported(e,t){return Y.check(e,t)}static loadSprite(e,t){return Ee(e,t)}static setup(e,t={}){let i=null;return A.string(e)?i=Array.from(document.querySelectorAll(e)):A.nodeList(e)?i=Array.from(e):A.array(e)&&(i=e.filter(A.element)),A.empty(i)?null:i.map((e=>new nt(e,t)))}}var at;return nt.defaults=(at=_e,JSON.parse(JSON.stringify(at))),nt}));\n+\"object\"==typeof navigator&&function(e,t){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=t():\"function\"==typeof define&&define.amd?define(\"Plyr\",t):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).Plyr=t()}(this,(function(){\"use strict\";!function(){if(\"undefined\"!=typeof window)try{var e=new window.CustomEvent(\"test\",{cancelable:!0});if(e.preventDefault(),!0!==e.defaultPrevented)throw new Error(\"Could not prevent default\")}catch(e){var t=function(e,t){var i,s;return(t=t||{}).bubbles=!!t.bubbles,t.cancelable=!!t.cancelable,(i=document.createEvent(\"CustomEvent\")).initCustomEvent(e,t.bubbles,t.cancelable,t.detail),s=i.preventDefault,i.preventDefault=function(){s.call(this);try{Object.defineProperty(this,\"defaultPrevented\",{get:function(){return!0}})}catch(e){this.defaultPrevented=!0}},i};t.prototype=window.Event.prototype,window.CustomEvent=t}}();var e=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function t(e,t,i){return(t=function(e){var t=function(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}(e,\"string\");return\"symbol\"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function i(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function s(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function n(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function a(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?n(Object(i),!0).forEach((function(t){s(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):n(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}!function(e){var t=function(){try{return!!Symbol.iterator}catch(e){return!1}}(),i=function(e){var i={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return t&&(i[Symbol.iterator]=function(){return i}),i},s=function(e){return encodeURIComponent(e).replace(/%20/g,\"+\")},n=function(e){return decodeURIComponent(String(e).replace(/\\+/g,\" \"))};(function(){try{var t=e.URLSearchParams;return\"a=1\"===new t(\"?a=1\").toString()&&\"function\"==typeof t.prototype.set&&\"function\"==typeof t.prototype.entries}catch(e){return!1}})()||function(){var n=function(e){Object.defineProperty(this,\"_entries\",{writable:!0,value:{}});var t=typeof e;if(\"undefined\"===t);else if(\"string\"===t)\"\"!==e&&this._fromString(e);else if(e instanceof n){var i=this;e.forEach((function(e,t){i.append(t,e)}))}else{if(null===e||\"object\"!==t)throw new TypeError(\"Unsupported input's type for URLSearchParams\");if(\"[object Array]\"===Object.prototype.toString.call(e))for(var s=0;s<e.length;s++){var a=e[s];if(\"[object Array]\"!==Object.prototype.toString.call(a)&&2===a.length)throw new TypeError(\"Expected [string, any] as entry at index \"+s+\" of URLSearchParams's input\");this.append(a[0],a[1])}else for(var r in e)e.hasOwnProperty(r)&&this.append(r,e[r])}},a=n.prototype;a.append=function(e,t){e in this._entries?this._entries[e].push(String(t)):this._entries[e]=[String(t)]},a.delete=function(e){delete this._entries[e]},a.get=function(e){return e in this._entries?this._entries[e][0]:null},a.getAll=function(e){return e in this._entries?this._entries[e].slice(0):[]},a.has=function(e){return e in this._entries},a.set=function(e,t){this._entries[e]=[String(t)]},a.forEach=function(e,t){var i;for(var s in this._entries)if(this._entries.hasOwnProperty(s)){i=this._entries[s];for(var n=0;n<i.length;n++)e.call(t,i[n],s,this)}},a.keys=function(){var e=[];return this.forEach((function(t,i){e.push(i)})),i(e)},a.values=function(){var e=[];return this.forEach((function(t){e.push(t)})),i(e)},a.entries=function(){var e=[];return this.forEach((function(t,i){e.push([i,t])})),i(e)},t&&(a[Symbol.iterator]=a.entries),a.toString=function(){var e=[];return this.forEach((function(t,i){e.push(s(i)+\"=\"+s(t))})),e.join(\"&\")},e.URLSearchParams=n}();var a=e.URLSearchParams.prototype;\"function\"!=typeof a.sort&&(a.sort=function(){var e=this,t=[];this.forEach((function(i,s){t.push([s,i]),e._entries||e.delete(s)})),t.sort((function(e,t){return e[0]<t[0]?-1:e[0]>t[0]?1:0})),e._entries&&(e._entries={});for(var i=0;i<t.length;i++)this.append(t[i][0],t[i][1])}),\"function\"!=typeof a._fromString&&Object.defineProperty(a,\"_fromString\",{enumerable:!1,configurable:!1,writable:!1,value:function(e){if(this._entries)this._entries={};else{var t=[];this.forEach((function(e,i){t.push(i)}));for(var i=0;i<t.length;i++)this.delete(t[i])}var s,a=(e=e.replace(/^\\?/,\"\")).split(\"&\");for(i=0;i<a.length;i++)s=a[i].split(\"=\"),this.append(n(s[0]),s.length>1?n(s[1]):\"\")}})}(void 0!==e?e:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:e),function(e){if(function(){try{var t=new e.URL(\"b\",\"http://a\");return t.pathname=\"c d\",\"http://a/c%20d\"===t.href&&t.searchParams}catch(e){return!1}}()||function(){var t=e.URL,i=function(t,i){\"string\"!=typeof t&&(t=String(t)),i&&\"string\"!=typeof i&&(i=String(i));var s,n=document;if(i&&(void 0===e.location||i!==e.location.href)){i=i.toLowerCase(),(s=(n=document.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=i,n.head.appendChild(s);try{if(0!==s.href.indexOf(i))throw new Error(s.href)}catch(e){throw new Error(\"URL unable to set base \"+i+\" due to \"+e)}}var a=n.createElement(\"a\");a.href=t,s&&(n.body.appendChild(a),a.href=a.href);var r=n.createElement(\"input\");if(r.type=\"url\",r.value=t,\":\"===a.protocol||!/:/.test(a.href)||!r.checkValidity()&&!i)throw new TypeError(\"Invalid URL\");Object.defineProperty(this,\"_anchorElement\",{value:a});var o=new e.URLSearchParams(this.search),l=!0,c=!0,u=this;[\"append\",\"delete\",\"set\"].forEach((function(e){var t=o[e];o[e]=function(){t.apply(o,arguments),l&&(c=!1,u.search=o.toString(),c=!0)}})),Object.defineProperty(this,\"searchParams\",{value:o,enumerable:!0});var h=void 0;Object.defineProperty(this,\"_updateSearchParams\",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==h&&(h=this.search,c&&(l=!1,this.searchParams._fromString(this.search),l=!0))}})},s=i.prototype;[\"hash\",\"host\",\"hostname\",\"port\",\"protocol\"].forEach((function(e){!function(e){Object.defineProperty(s,e,{get:function(){return this._anchorElement[e]},set:function(t){this._anchorElement[e]=t},enumerable:!0})}(e)})),Object.defineProperty(s,\"search\",{get:function(){return this._anchorElement.search},set:function(e){this._anchorElement.search=e,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\\?$/,\"\")},set:function(e){this._anchorElement.href=e,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\\/?)/,\"/\")},set:function(e){this._anchorElement.pathname=e},enumerable:!0},origin:{get:function(){var e={\"http:\":80,\"https:\":443,\"ftp:\":21}[this._anchorElement.protocol],t=this._anchorElement.port!=e&&\"\"!==this._anchorElement.port;return this._anchorElement.protocol+\"//\"+this._anchorElement.hostname+(t?\":\"+this._anchorElement.port:\"\")},enumerable:!0},password:{get:function(){return\"\"},set:function(e){},enumerable:!0},username:{get:function(){return\"\"},set:function(e){},enumerable:!0}}),i.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)},i.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)},e.URL=i}(),void 0!==e.location&&!(\"origin\"in e.location)){var t=function(){return e.location.protocol+\"//\"+e.location.hostname+(e.location.port?\":\"+e.location.port:\"\")};try{Object.defineProperty(e.location,\"origin\",{get:t,enumerable:!0})}catch(i){setInterval((function(){e.location.origin=t()}),100)}}}(void 0!==e?e:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:e);var r={addCSS:!0,thumbWidth:15,watch:!0};var o=function(e){return null!=e?e.constructor:null},l=function(e,t){return!!(e&&t&&e instanceof t)},c=function(e){return null==e},u=function(e){return o(e)===Object},h=function(e){return o(e)===String},d=function(e){return Array.isArray(e)},m=function(e){return l(e,NodeList)},p={nullOrUndefined:c,object:u,number:function(e){return o(e)===Number&&!Number.isNaN(e)},string:h,boolean:function(e){return o(e)===Boolean},function:function(e){return o(e)===Function},array:d,nodeList:m,element:function(e){return l(e,Element)},event:function(e){return l(e,Event)},empty:function(e){return c(e)||(h(e)||d(e)||m(e))&&!e.length||u(e)&&!Object.keys(e).length}};function g(e,t){if(1>t){var i=function(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var f=function(){function e(t,i){(function(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")})(this,e),p.element(t)?this.element=t:p.string(t)&&(this.element=document.querySelector(t)),p.element(this.element)&&p.empty(this.element.rangeTouch)&&(this.config=a({},r,{},i),this.init())}return function(e,t,s){t&&i(e.prototype,t),s&&i(e,s)}(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!p.event(t))return null;var i,s=t.target,n=t.changedTouches[0],a=parseFloat(s.getAttribute(\"min\"))||0,r=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),a+g(i/100*(r-a),o)}},{key:\"set\",value:function(t){e.enabled&&p.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),function(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(p.empty(t)||p.string(t)?s=Array.from(document.querySelectorAll(p.string(t)?t:'input[type=\"range\"]')):p.element(t)?s=[t]:p.nodeList(t)?s=Array.from(t):p.array(t)&&(s=t.filter(p.element)),p.empty(s))return null;var n=a({},r,{},i);if(p.string(t)&&n.watch){var o=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){p.element(i)&&function(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}(i,t)&&new e(i,n)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const y=e=>null!=e?e.constructor:null,b=(e,t)=>Boolean(e&&t&&e instanceof t),v=e=>null==e,w=e=>y(e)===Object,T=e=>y(e)===String,k=e=>\"function\"==typeof e,E=e=>Array.isArray(e),C=e=>b(e,NodeList),S=e=>v(e)||(T(e)||E(e)||C(e))&&!e.length||w(e)&&!Object.keys(e).length;var A={nullOrUndefined:v,object:w,number:e=>y(e)===Number&&!Number.isNaN(e),string:T,boolean:e=>y(e)===Boolean,function:k,array:E,weakMap:e=>b(e,WeakMap),nodeList:C,element:e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,textNode:e=>y(e)===Text,event:e=>b(e,Event),keyboardEvent:e=>b(e,KeyboardEvent),cue:e=>b(e,window.TextTrackCue)||b(e,window.VTTCue),track:e=>b(e,TextTrack)||!v(e)&&T(e.kind),promise:e=>b(e,Promise)&&k(e.then),url:e=>{if(b(e,window.URL))return!0;if(!T(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!S(new URL(t).hostname)}catch(e){return!1}},empty:S};const P=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!A.string(i)&&t[i]})();function M(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}var x={isIE:Boolean(window.document.documentMode),isEdge:/Edge/g.test(navigator.userAgent),isWebKit:\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone:/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS:\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos:/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1};function L(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function N(e={},...t){if(!t.length)return e;const i=t.shift();return A.object(i)?(Object.keys(i).forEach((t=>{A.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),N(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),N(e,...t)):e}function _(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,a=e.nextSibling;s.appendChild(e),a?n.insertBefore(s,a):n.appendChild(s)}))}function I(e,t){A.element(e)&&!A.empty(t)&&Object.entries(t).filter((([,e])=>!A.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function O(e,t,i){const s=document.createElement(e);return A.object(t)&&I(s,t),A.string(i)&&(s.innerText=i),s}function $(e,t,i,s){A.element(t)&&t.appendChild(O(e,i,s))}function j(e){A.nodeList(e)||A.array(e)?Array.from(e).forEach(j):A.element(e)&&A.element(e.parentNode)&&e.parentNode.removeChild(e)}function R(e){if(!A.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function D(e,t){return A.element(t)&&A.element(t.parentNode)&&A.element(e)?(t.parentNode.replaceChild(e,t),e):null}function q(e,t){if(!A.string(e)||A.empty(e))return{};const i={},s=N({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),a=t.replace(/[[\\]]/g,\"\").split(\"=\"),[r]=a,o=a.length>1?a[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":A.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[r]=o}})),N(s,i)}function H(e,t){if(!A.element(e))return;let i=t;A.boolean(i)||(i=!e.hidden),e.hidden=i}function F(e,t,i){if(A.nodeList(e))return Array.from(e).map((e=>F(e,t,i)));if(A.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function U(e,t){return A.element(e)&&e.classList.contains(t)}function V(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function B(e){return this.elements.container.querySelectorAll(e)}function W(e){return this.elements.container.querySelector(e)}function z(e=null,t=!1){A.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const K={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},Y={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=Y[e]||\"html5\"!==t;return{api:i,ui:i&&Y.rangeInput}},pip:!(x.isIPhone||!A.function(O(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||O(\"video\").disablePictureInPicture)),airplay:A.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(A.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(K).includes(i)&&(i+=`; codecs=\"${K[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==P,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},Q=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function X(e,t,i,s=!1,n=!0,a=!1){if(!e||!(\"addEventListener\"in e)||A.empty(t)||!A.function(i))return;const r=t.split(\" \");let o=a;Q&&(o={passive:n,capture:a}),r.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function J(e,t=\"\",i,s=!0,n=!1){X.call(this,e,t,i,!0,s,n)}function G(e,t=\"\",i,s=!0,n=!1){X.call(this,e,t,i,!1,s,n)}function Z(e,t=\"\",i,s=!0,n=!1){const a=(...r)=>{G(e,t,a,s,n),i.apply(this,r)};X.call(this,e,t,a,!0,s,n)}function ee(e,t=\"\",i=!1,s={}){if(!A.element(e)||A.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function te(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ie(){return new Promise((e=>this.ready?setTimeout(e,0):J.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function se(e){A.promise(e)&&e.then(null,(()=>{}))}function ne(e){return A.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function ae(e,t){return A.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function re(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const oe=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function le(e){if(!(A.array(e)||A.string(e)&&e.includes(\":\")))return!1;return(A.array(e)?e:e.split(\":\")).map(Number).every(A.number)}function ce(e){if(!A.array(e)||!e.every(A.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function ue(e){const t=e=>le(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!A.empty(this.embed)&&A.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return ce(i)}function he(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=ue.call(this,e);if(!A.array(i))return{};const[s,n]=ce(i),a=100/s*n;if(re(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${a}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-a)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:a,ratio:i}}function de(e,t,i=.05){const s=e/t,n=ae(Object.keys(oe),s);return Math.abs(n-s)<=i?oe[n]:[e,t]}const me={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!A.empty(t)||Y.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:me.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,A.empty(this.config.ratio)||he.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=me.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&A.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=me.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:a,readyState:r,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==a||r)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||se(e.play())})),e.media.load())}ee.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(j(me.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function pe(e,...t){return A.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}const ge=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),fe=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function ye(e=\"\"){let t=e.toString();return t=function(e=\"\"){let t=e.toString();return t=ge(t,\"-\",\" \"),t=ge(t,\"_\",\" \"),t=fe(t),ge(t,\" \",\"\")}(t),t.charAt(0).toLowerCase()+t.slice(1)}function be(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const ve={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},we={get(e=\"\",t={}){if(A.empty(e)||A.empty(t))return\"\";let i=L(t.i18n,e);if(A.empty(i))return Object.keys(ve).includes(e)?ve[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=ge(i,e,t)})),i}};class Te{constructor(e){t(this,\"get\",(e=>{if(!Te.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(A.empty(t))return null;const i=JSON.parse(t);return A.string(e)&&e.length?i[e]:i})),t(this,\"set\",(e=>{if(!Te.supported||!this.enabled)return;if(!A.object(e))return;let t=this.get();A.empty(t)&&(t={}),N(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function ke(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function Ee(e,t){if(!A.string(e))return;const i=\"cache\",s=A.string(t);let n=!1;const a=()=>null!==document.getElementById(t),r=(e,t)=>{e.innerHTML=t,s&&a()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!a()){const a=Te.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),a){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);r(o,t.content)}}ke(e).then((e=>{if(!A.empty(e)){if(a)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}r(o,e)}})).catch((()=>{}))}}const Ce=e=>Math.trunc(e/60/60%60,10),Se=e=>Math.trunc(e/60%60,10),Ae=e=>Math.trunc(e%60,10);function Pe(e=0,t=!1,i=!1){if(!A.number(e))return Pe(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=Ce(e);const a=Se(e),r=Ae(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(a)}:${s(r)}`}const Me={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||x.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=W.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:B.call(this,this.config.selectors.buttons.play),pause:W.call(this,this.config.selectors.buttons.pause),restart:W.call(this,this.config.selectors.buttons.restart),rewind:W.call(this,this.config.selectors.buttons.rewind),fastForward:W.call(this,this.config.selectors.buttons.fastForward),mute:W.call(this,this.config.selectors.buttons.mute),pip:W.call(this,this.config.selectors.buttons.pip),airplay:W.call(this,this.config.selectors.buttons.airplay),settings:W.call(this,this.config.selectors.buttons.settings),captions:W.call(this,this.config.selectors.buttons.captions),fullscreen:W.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=W.call(this,this.config.selectors.progress),this.elements.inputs={seek:W.call(this,this.config.selectors.inputs.seek),volume:W.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:W.call(this,this.config.selectors.display.buffer),currentTime:W.call(this,this.config.selectors.display.currentTime),duration:W.call(this,this.config.selectors.display.duration)},A.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=Me.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,a=document.createElementNS(i,\"svg\");I(a,N(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const r=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in r&&r.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),r.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),a.appendChild(r),a},createLabel(e,t={}){const i=we.get(e,this.config);return O(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(A.empty(e))return null;const t=O(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(O(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=N({},t);let s=ye(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||N(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:A.empty(n.label)&&(n.label=s),A.empty(n.icon)&&(n.icon=e)}const a=O(n.element);return n.toggle?(a.appendChild(Me.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),a.appendChild(Me.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),a.appendChild(Me.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),a.appendChild(Me.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(a.appendChild(Me.createIcon.call(this,n.icon)),a.appendChild(Me.createLabel.call(this,n.label))),N(i,q(this.config.selectors.buttons[s],i)),I(a,i),\"play\"===s?(A.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(a)):this.elements.buttons[s]=a,a},createRange(e,t){const i=O(\"input\",N(q(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":we.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,Me.updateRangeFill.call(this,i),f.setup(i),i},createProgress(e,t){const i=O(\"progress\",N(q(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(O(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?we.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=q(this.config.selectors.display[e],t),s=O(\"div\",N(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":we.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){J.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=V(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))Me.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,A.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,A.element(t)||(t=e.parentNode.lastElementChild)),z.call(this,t,!0))}}),!1),J.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&Me.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:a=!1}){const r=q(this.config.selectors.inputs[i]),o=O(\"button\",N(r,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${r.class?r.class:\"\"}`.trim(),\"aria-checked\":a,value:e})),l=O(\"span\");l.innerHTML=s,A.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>V(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!A.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}Me.showMenuPanel.call(this,\"home\",A.keyboardEvent(t))}}),i,!1),Me.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!A.number(e))return e;return Pe(e,Ce(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){A.element(e)&&A.number(t)&&(e.innerText=Me.formatTime(t,i))},updateVolume(){this.supported.ui&&(A.element(this.elements.inputs.volume)&&Me.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),A.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){A.element(e)&&(e.value=t,Me.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!A.event(e))return;let t=0;const i=(e,t)=>{const i=A.number(t)?t:0,s=A.element(e)?e:this.elements.display.buffer;if(A.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];A.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":s=this.currentTime,n=this.duration,t=0===s||0===n||Number.isNaN(s)||Number.isNaN(n)?0:(s/n*100).toFixed(2),\"timeupdate\"===e.type&&Me.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}var s,n},updateRangeFill(e){const t=A.event(e)?e.target:e;if(A.element(t)&&\"range\"===t.getAttribute(\"type\")){if(V(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=Me.formatTime(this.currentTime),i=Me.formatTime(this.duration),s=we.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(V(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(x.isWebKit||x.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!A.element(this.elements.inputs.seek)||!A.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,a=e=>F(s,n,e);if(this.touch)return void a(!1);let r=0;const o=this.elements.progress.getBoundingClientRect();if(A.event(e))r=100/o.width*(e.pageX-o.left);else{if(!U(s,n))return;r=parseFloat(s.style.left,10)}r<0?r=0:r>100&&(r=100);const l=this.duration/100*r;s.innerText=Me.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${r}%`,A.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&a(\"mouseenter\"===e.type)},timeUpdate(e){const t=!A.element(this.elements.display.duration)&&this.config.invertTime;Me.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||Me.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return H(this.elements.display.currentTime,!0),void H(this.elements.progress,!0);A.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=A.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&Me.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&Me.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&Me.setMarkers.call(this),Me.updateSeekTooltip.call(this)},toggleMenuButton(e,t){H(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,a=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=A.empty(i)?this[e]:i,A.empty(n)&&(n=this.config[e].default),!A.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(A.element(a)||(a=s&&s.querySelector('[role=\"menu\"]')),!A.element(a))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=Me.getLabel.call(this,e,n);const r=a&&a.querySelector(`[value=\"${n}\"]`);A.element(r)&&(r.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?we.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(A.number(t)){const e=we.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return fe(t);case\"captions\":return Ne.getLabel.call(this);default:return null}},setQualityMenu(e){if(!A.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');A.array(e)&&(this.options.quality=ne(e).filter((e=>this.config.quality.options.includes(e))));const s=!A.empty(this.options.quality)&&this.options.quality.length>1;if(Me.toggleMenuButton.call(this,t,s),R(i),Me.checkMenu.call(this),!s)return;const n=e=>{const t=we.get(`qualityBadge.${e}`,this.config);return t.length?Me.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{Me.createMenuItem.call(this,{value:e,list:i,type:t,title:Me.getLabel.call(this,\"quality\",e),badge:n(e)})})),Me.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!A.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=Ne.getTracks.call(this),s=Boolean(i.length);if(Me.toggleMenuButton.call(this,e,s),R(t),Me.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:Ne.getLabel.call(this,e),badge:e.language&&Me.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:we.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(Me.createMenuItem.bind(this)),Me.updateSetting.call(this,e,t)},setSpeedMenu(){if(!A.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!A.empty(this.options.speed)&&this.options.speed.length>1;Me.toggleMenuButton.call(this,e,i),R(t),Me.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{Me.createMenuItem.call(this,{value:i,list:t,type:e,title:Me.getLabel.call(this,\"speed\",i)})})),Me.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!A.empty(e)&&Object.values(e).some((e=>!e.hidden));H(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;A.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');z.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!A.element(t)||!A.element(i))return;const{hidden:s}=t;let n=s;if(A.boolean(e))n=e;else if(A.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(A.event(e)){const s=A.function(e.composedPath)?e.composedPath()[0]:e.target,a=t.contains(s);if(a||!a&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),H(t,!n),F(this.elements.container,this.config.classNames.menu.open,n),n&&A.keyboardEvent(e)?Me.focusFirstMenuItem.call(this,null,!0):n||s||z.call(this,i,A.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return j(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!A.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(Y.transitions&&!Y.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=Me.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",G.call(this,s,P,t))};J.call(this,s,P,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}H(n,!0),H(i,!1),Me.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;A.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:a,setQualityMenu:r,setSpeedMenu:o,showMenuPanel:l}=Me;this.elements.controls=null,A.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=O(\"div\",q(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return ne(A.array(this.config.controls)?this.config.controls:[]).forEach((r=>{if(\"restart\"===r&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===r&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===r&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===r&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===r){const t=O(\"div\",{class:`${u.class} plyr__progress__container`}),i=O(\"div\",q(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=O(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===r&&c.appendChild(a.call(this,\"currentTime\",u)),\"duration\"===r&&c.appendChild(a.call(this,\"duration\",u)),\"mute\"===r||\"volume\"===r){let{volume:t}=this.elements;if(A.element(t)&&c.contains(t)||(t=O(\"div\",N({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===r&&t.appendChild(i.call(this,\"mute\")),\"volume\"===r&&!x.isIos&&!x.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",N(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===r&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===r&&!A.empty(this.config.settings)){const s=O(\"div\",N({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=O(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),a=O(\"div\"),r=O(\"div\",{id:`plyr-settings-${e.id}-home`}),o=O(\"div\",{role:\"menu\"});r.appendChild(o),a.appendChild(r),this.elements.settings.panels.home=r,this.config.settings.forEach((i=>{const s=O(\"button\",N(q(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),J.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=O(\"span\",null,we.get(i,this.config)),r=O(\"span\",{class:this.config.classNames.menu.value});r.innerHTML=e[i],n.appendChild(r),s.appendChild(n),o.appendChild(s);const c=O(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=O(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(O(\"span\",{\"aria-hidden\":!0},we.get(i,this.config))),u.appendChild(O(\"span\",{class:this.config.classNames.hidden},we.get(\"menuBack\",this.config))),J.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),J.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(O(\"div\",{role:\"menu\"})),a.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(a),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===r&&Y.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===r&&Y.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===r){const e=N({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!A.url(t)&&this.isEmbed&&N(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===r&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&r.call(this,me.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=Me.getIconUrl.call(this);e.cors&&Ee(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;A.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),A.element(this.config.controls)||A.string(this.config.controls)?e=this.config.controls:(e=Me.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:Ne.getLabel.call(this)}),i=!1);let s;i&&A.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=ge(i,`{${e}}`,t)})),i})(e)),A.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),A.element(s)||(s=this.elements.container);if(s[A.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),A.element(this.elements.controls)||Me.findElements.call(this),!A.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>U(e,t),set(i=!1){F(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{A.array(t)||A.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(x.isEdge&&M(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=B.call(this,i);Array.from(s).forEach((e=>{F(e,this.config.classNames.hidden,!1),F(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let a=null;const r=`${this.config.classNames.tooltip}--visible`,o=e=>F(a,r,e);i.forEach((e=>{const t=O(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";a&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(a.style.left=i,a.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(a=O(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(a)),this.elements.markers={points:n,tip:a},this.elements.progress.appendChild(s)}};function xe(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function Le(e){const t=new URLSearchParams;return A.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const Ne={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!Y.textTracks)return void(A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Me.setCaptionsMenu.call(this));var e,t;if(A.element(this.elements.captions)||(this.elements.captions=O(\"div\",q(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),e=this.elements.captions,t=this.elements.wrapper,A.element(e)&&A.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)),x.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=xe(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&ke(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{j(e)}))}))}const i=ne((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let s=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===s&&([s]=i);let n=this.storage.get(\"captions\");if(A.boolean(n)||({active:n}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:n,language:s,languages:i}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";J.call(this,this.media.textTracks,e,Ne.update.bind(this))}setTimeout(Ne.update.bind(this),0)},update(){const e=Ne.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,a=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),J.call(this,e,\"cuechange\",(()=>Ne.updateCues.call(this)))})),(a&&this.language!==i||!e.includes(n))&&(Ne.setLanguage.call(this,i),Ne.toggle.call(this,t&&a)),this.elements&&F(this.elements.container,this.config.classNames.captions.enabled,!A.empty(e)),A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&Me.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=A.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=Ne.getTracks.call(this),t=Ne.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void Ne.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),F(this.elements.container,s,n),this.captions.toggled=n,Me.updateSetting.call(this,\"captions\"),ee.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=Ne.getTracks.call(this);if(-1!==e)if(A.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,Me.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),ee.call(this,this.media,\"languagechange\")}Ne.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&Ne.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else Ne.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!A.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=Ne.getTracks.call(this),n=Ne.findTrack.call(this,[i]);Ne.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=Ne.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let a;return e.every((e=>(a=n.find((t=>t.language===e)),!a))),a||(t?n[0]:void 0)},getCurrentTrack(){return Ne.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!A.track(t)&&Y.textTracks&&this.captions.toggled&&(t=Ne.getCurrentTrack.call(this)),A.track(t)?A.empty(t.label)?A.empty(t.language)?we.get(\"enabled\",this.config):e.language.toUpperCase():t.label:we.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!A.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!A.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=Ne.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(be)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){R(this.elements.captions);const e=O(\"span\",q(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),ee.call(this,this.media,\"cuechange\")}}},_e={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:null,listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},Ie=\"picture-in-picture\",Oe=\"inline\",$e={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},je=\"audio\",Re=\"video\";const De=()=>{};class qe{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):De}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):De}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):De}}class He{constructor(e){t(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;A.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;ee.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),t(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",F(this.target,this.player.config.classNames.fullscreen.fallback,e),x.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=A.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),t(this,\"trapFocus\",(e=>{if(x.isIos||x.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=B.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),t(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":He.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");F(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),t(this,\"enter\",(()=>{this.supported&&(x.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!He.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?A.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),t(this,\"exit\",(()=>{if(this.supported)if(x.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),se(this.player.play());else if(!He.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!A.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),t(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=He.prefix,this.property=He.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&function(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(V.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}(this.player.elements.container,e.config.fullscreen.container),J.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),J.call(this.player,this.player.elements.container,\"dblclick\",(e=>{A.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),J.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return He.nativeSupported&&!this.forceFallback}static get prefix(){if(A.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!A.function(document[`${t}ExitFullscreen`])&&!A.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,He.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||He.nativeSupported||!x.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!He.nativeSupported||this.forceFallback)return U(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return x.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function Fe(e,t=1){return new Promise(((i,s)=>{const n=new Image,a=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:a,onerror:a,src:e})}))}const Ue={addStyleHook(){F(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),F(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void Ue.toggleNativeControls.call(this,!0);A.element(this.elements.controls)||(Me.inject.call(this),this.listeners.controls()),Ue.toggleNativeControls.call(this),this.isHTML5&&Ne.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,Me.updateVolume.call(this),Me.timeUpdate.call(this),Me.durationUpdate.call(this),Ue.checkPlaying.call(this),F(this.elements.container,this.config.classNames.pip.supported,Y.pip&&this.isHTML5&&this.isVideo),F(this.elements.container,this.config.classNames.airplay.supported,Y.airplay&&this.isHTML5),F(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{ee.call(this,this.media,\"ready\")}),0),Ue.setTitle.call(this),this.poster&&Ue.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&Me.durationUpdate.call(this),this.config.mediaMetadata&&Me.setMediaMetadata.call(this)},setTitle(){let e=we.get(\"play\",this.config);if(A.string(this.config.title)&&!A.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=W.call(this,\"iframe\");if(!A.element(e))return;const t=A.empty(this.config.title)?\"video\":this.config.title,i=we.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){F(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ie.call(this).then((()=>Fe(e))).catch((t=>{throw e===this.poster&&Ue.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),Ue.togglePoster.call(this,!0),e))))},checkPlaying(e){F(this.elements.container,this.config.classNames.playing,this.playing),F(this.elements.container,this.config.classNames.paused,this.paused),F(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",we.get(this.playing?\"pause\":\"play\",this.config))})),A.event(e)&&\"timeupdate\"===e.type||Ue.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{F(this.elements.container,this.config.classNames.loading,this.loading),Ue.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!A.empty(e)&&A.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),A.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Ve{constructor(e){t(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,F(t.container,e.config.classNames.isTouch,!0)})),t(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&X.call(t,window,\"keydown keyup\",this.handleKey,e,!1),X.call(t,document.body,\"click\",this.toggleMenu,e),Z.call(t,document.body,\"touchstart\",this.firstTouch)})),t(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&J.call(e,i.container,\"keydown keyup\",this.handleKey,!1),J.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let a=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(Ue.toggleControls.call(e,!0),a=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>Ue.toggleControls.call(e,!1)),a)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,a]=ue.call(e),r=re(`aspect-ratio: ${n} / ${a}`);if(!s)return void(r?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)],c=o/l>n/a;r?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/a*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},a=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};J.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&A.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?J:G).call(e,window,\"resize\",a)}))})),t(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(J.call(e,e.media,\"timeupdate seeking seeked\",(t=>Me.timeUpdate.call(e,t))),J.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>Me.durationUpdate.call(e,t))),J.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),J.call(e,e.media,\"progress playing seeking seeked\",(t=>Me.updateProgress.call(e,t))),J.call(e,e.media,\"volumechange\",(t=>Me.updateVolume.call(e,t))),J.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>Ue.checkPlaying.call(e,t))),J.call(e,e.media,\"waiting canplay seeked playing\",(t=>Ue.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=W.call(e,`.${e.config.classNames.video}`);if(!A.element(i))return;J.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{se(e.play())}),\"play\")):this.proxy(s,(()=>{se(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&J.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),J.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),J.call(e,e.media,\"ratechange\",(()=>{Me.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),J.call(e,e.media,\"qualitychange\",(t=>{Me.updateSetting.call(e,\"quality\",null,t.detail.quality)})),J.call(e,e.media,\"ready qualitychange\",(()=>{Me.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");J.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),ee.call(e,t.container,i.type,!0,s)}))})),t(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let a=!0;A.function(n)&&(a=n.call(s,e)),!1!==a&&A.function(t)&&t.call(s,e)})),t(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:a}=this,r=a.config.listeners[s],o=A.function(r);J.call(a,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),t(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=x.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{se(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{ee.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),Me.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),Me.toggleMenu.call(e,t)):Me.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&Me.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(A.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),a=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&a?(i.removeAttribute(s),se(e.play())):!a&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),x.isIos){const t=B.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>M(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");A.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>Me.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),x.isWebKit&&Array.from(B.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>Me.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!A.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,Me.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;F(t.controls,i.classNames.noTransition,!0),Ue.toggleControls.call(e,!0),setTimeout((()=>{F(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>Ue.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),a=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(a/50);const{volume:r}=e.media;(1===a&&r<1||-1===a&&r>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:a,ctrlKey:r,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(a||r||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(A.element(n)){const{editable:s}=t.config.selectors,{seek:a}=i.inputs;if(n!==a&&V(n,s))return;if(\" \"===e.key&&V(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||se(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){Me.toggleMenu.call(this.player,e)}}var Be=function(e,t){return e(t={exports:{}},t.exports),t.exports}((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,a,r,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)a=e[l],(r=i[a])?n(a,r):(s[a]=s[a]||[]).push(n)}function a(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function r(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var a,r,l=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),m=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((r=l.createElement(\"link\")).rel=\"stylesheet\",r.href=m,(a=\"hideFocus\"in r)&&r.relList&&(a=0,r.rel=\"preload\",r.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(r=l.createElement(\"img\")).src=m:((r=l.createElement(\"script\")).src=t,r.async=void 0===c||c),r.onload=r.onerror=r.onbeforeload=function(e){var l=e.type[0];if(a)try{r.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==r.rel&&\"style\"==r.as)return r.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==h(t,r)&&l.head.appendChild(r)}function l(e,t,i){var s,n,a=(e=e.push?e:[e]).length,r=a,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--a||t(l)},n=0;n<r;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){r(o,e),t&&r({success:t,error:i},e),a(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){r(t,e)})),c},c.done=function(e){a(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function We(e){return new Promise(((t,i)=>{Be(e,{success:t,error:i})}))}function ze(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,ee.call(this,this.media,e?\"play\":\"pause\"))}const Ke={setup(){const e=this;F(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,he.call(e),A.object(window.Vimeo)?Ke.ready.call(e):We(e.config.urls.vimeo.sdk).then((()=>{Ke.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let a=e.media.getAttribute(\"src\"),r=\"\";A.empty(a)?(a=e.media.getAttribute(e.config.attributes.embed.id),r=e.media.getAttribute(e.config.attributes.embed.hash)):r=function(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}(a);const o=r?{h:r}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=Le({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=(u=a,A.empty(u)?null:A.number(Number(u))?u:u.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:u);var u;const h=O(\"iframe\"),d=pe(e.config.urls.vimeo.iframe,c,l);if(h.setAttribute(\"src\",d),h.setAttribute(\"allowfullscreen\",\"\"),h.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),A.empty(s)||h.setAttribute(\"referrerPolicy\",s),i||!t.customControls)h.setAttribute(\"data-poster\",e.poster),e.media=D(h,e.media);else{const t=O(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(h),e.media=D(t,e.media)}t.customControls||ke(pe(e.config.urls.vimeo.api,d)).then((t=>{!A.empty(t)&&t.thumbnail_url&&Ue.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(h,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(ze.call(e,!0),e.embed.play()),e.media.pause=()=>(ze.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:m}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>m,set(t){const{embed:i,media:s,paused:n,volume:a}=e,r=n&&!i.hasPlayed;s.seeking=!0,ee.call(e,s,\"seeking\"),Promise.resolve(r&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>r&&i.pause())).then((()=>r&&i.setVolume(a))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,ee.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:g}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>g,set(t){e.embed.setVolume(t).then((()=>{g=t,ee.call(e,e.media,\"volumechange\")}))}});let{muted:f}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>f,set(t){const i=!!A.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{f=i,ee.call(e,e.media,\"volumechange\")}))}});let y,{loop:b}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>b,set(t){const i=A.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{b=i}))}}),e.embed.getVideoUrl().then((t=>{y=t,Me.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>y}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=de(i,s),he.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,Ue.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{m=t,ee.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,ee.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,Ne.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>function(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}(e.text)));Ne.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{ze.call(e,!t),t||ee.call(e,e.media,\"playing\")})),A.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{ee.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{ee.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{ze.call(e,!0),ee.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{ze.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,m=t.seconds,ee.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,ee.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&ee.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,ee.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,ee.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,ee.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,ee.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>Ue.build.call(e)),0)}};function Ye(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,ee.call(this,this.media,e?\"play\":\"pause\"))}function Qe(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const Xe={setup(){if(F(this.elements.wrapper,this.config.classNames.embed,!0),A.object(window.YT)&&A.function(window.YT.Player))Xe.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{A.function(e)&&e(),Xe.ready.call(this)},We(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){ke(pe(this.config.urls.youtube.api,e)).then((e=>{if(A.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,Ue.setTitle.call(this),this.embed.ratio=de(s,i)}he.call(this)})).catch((()=>{he.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!A.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");A.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=(a=s,A.empty(a)?null:a.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:a);var a;const r=O(\"div\",{id:`${e.provider}-${Math.floor(1e4*Math.random())}`,\"data-poster\":t.customControls?e.poster:void 0});if(e.media=D(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;Fe(t(\"maxres\"),121).catch((()=>Fe(t(\"sd\"),121))).catch((()=>Fe(t(\"hq\")))).then((t=>Ue.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:Qe(t),playerVars:N({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},ee.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),ee.call(e,e.media,\"ratechange\")},onReady(i){if(A.function(e.media.play))return;const s=i.target;Xe.getTitle.call(e,n),e.media.play=()=>{Ye.call(e,!0),s.playVideo()},e.media.pause=()=>{Ye.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,ee.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:a}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>a,set(t){a=t,s.setVolume(100*a),ee.call(e,e.media,\"volumechange\")}});let{muted:r}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>r,set(t){const i=A.boolean(t)?t:r;r=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*a),ee.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),ee.call(e,e.media,\"timeupdate\"),ee.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&ee.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),ee.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>Ue.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,ee.call(e,e.media,\"seeked\")),i.data){case-1:ee.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),ee.call(e,e.media,\"progress\");break;case 0:Ye.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):ee.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(Ye.call(e,!0),ee.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{ee.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),ee.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),Ye.call(e,!1);break;case 3:ee.call(e,e.media,\"waiting\")}ee.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},Je={setup(){this.media?(F(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),F(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&F(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=O(\"div\",{class:this.config.classNames.video}),_(this.media,this.elements.wrapper),this.elements.poster=O(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?me.setup.call(this):this.isYouTube?Xe.setup.call(this):this.isVimeo&&Ke.setup.call(this)):this.debug.warn(\"No media element found!\")}};class Ge{constructor(e){t(this,\"load\",(()=>{this.enabled&&(A.object(window.google)&&A.object(window.google.ima)?this.ready():We(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),t(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),t(this,\"setupIMA\",(()=>{this.elements.container=O(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),t(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),t(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=Pe(Math.max(this.manager.getRemainingTime(),0)),t=`${we.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),t(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),t(this,\"addCuePoints\",(()=>{A.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(A.element(t)){const i=100/this.player.duration*e,s=O(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),t(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{ee.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),t(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),t(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;A.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),t(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),t(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,se(this.player.media.play())})),t(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),t(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),t(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),t(this,\"trigger\",((e,...t)=>{const i=this.events[e];A.array(i)&&i.forEach((e=>{A.function(e)&&e.apply(this,t)}))})),t(this,\"on\",((e,t)=>(A.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),t(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),t(this,\"clearSafetyTimer\",(e=>{A.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!A.empty(e.publisherId)||A.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(A.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${Le({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function Ze(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const et=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(A.number(i.startTime)){if(!A.empty(e.trim())&&A.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},tt=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class it{constructor(e){t(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),t(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(A.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(A.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(A.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),t(this,\"getThumbnail\",(e=>new Promise((t=>{ke(e).then((i=>{const s={frames:et(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),t(this,\"startMove\",(e=>{if(this.loaded&&A.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=Pe(this.seekTime);const a=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));a&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${a.label}<br>`)}this.showImageAtCurrentTime()}})),t(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),t(this,\"startScrubbing\",(e=>{(A.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),t(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):Z.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),t(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),t(this,\"render\",(()=>{this.elements.thumb.container=O(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=O(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=O(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=O(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),A.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=O(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),t(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),t(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),t(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],a=i.frames[t].text,r=s+a;if(this.currentImageElement&&this.currentImageElement.dataset.filename===a)this.showImage(this.currentImageElement,n,e,t,a,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=r,i.dataset.index=t,i.dataset.filename=a,this.showingThumbFilename=a,this.player.debug.log(`Loading image: ${r}`),i.onload=()=>this.showImage(i,n,e,t,a,!0),this.loadingImage=i,this.removeOldImages(i)}})),t(this,\"showImage\",((e,t,i,s,n,a=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${a}`),this.setImageSizeAndOffset(e,t),a&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),t(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),t(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let a=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){a=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),a||i()}}),300)})))),t(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),t(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),t(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),t(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),t(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),t(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,a=this.mousePosX-e.left-i.clientWidth/2,r=Ze(a,s,n);i.style.left=`${r}px`,i.style.setProperty(\"--preview-arrow-offset\",a-r+\"px\")})),t(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=tt(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),t(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=tt(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const st={insertElements(e,t){A.string(t)?$(e,this.media,{src:t}):A.array(t)&&t.forEach((t=>{$(e,this.media,t)}))},change(e){L(e,\"sources.length\")?(me.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],j(this.media),this.media=null,A.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=$e.html5,src:n}]=t,a=\"html5\"===s?i:\"div\",r=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:Y.check(i,s,this.config.playsinline),media:O(a,r)}),this.elements.container.appendChild(this.media),A.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),A.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),Ue.addStyleHook.call(this),this.isHTML5&&st.insertElements.call(this,\"source\",t),this.config.title=e.title,Je.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&st.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Ue.build.call(this),this.isHTML5&&this.media.load(),A.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class nt{constructor(e,i){if(t(this,\"play\",(()=>A.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>se(this.media.play()))),this.media.play()):null)),t(this,\"pause\",(()=>this.playing&&A.function(this.media.pause)?this.media.pause():null)),t(this,\"togglePlay\",(e=>(A.boolean(e)?e:!this.playing)?this.play():this.pause())),t(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):A.function(this.media.stop)&&this.media.stop()})),t(this,\"restart\",(()=>{this.currentTime=0})),t(this,\"rewind\",(e=>{this.currentTime-=A.number(e)?e:this.config.seekTime})),t(this,\"forward\",(e=>{this.currentTime+=A.number(e)?e:this.config.seekTime})),t(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(A.number(e)?e:0)})),t(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),t(this,\"airplay\",(()=>{Y.airplay&&this.media.webkitShowPlaybackTargetPicker()})),t(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=U(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=F(this.elements.container,this.config.classNames.hideControls,i);if(s&&A.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!A.empty(this.config.settings)&&Me.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";ee.call(this,this.media,e)}return!s}return!1})),t(this,\"on\",((e,t)=>{J.call(this,this.elements.container,e,t)})),t(this,\"once\",((e,t)=>{Z.call(this,this.elements.container,e,t)})),t(this,\"off\",((e,t)=>{G(this.elements.container,e,t)})),t(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(j(this.elements.buttons.play),j(this.elements.captions),j(this.elements.controls),j(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),A.function(e)&&e()):(te.call(this),me.cancelRequests.call(this),D(this.elements.original,this.elements.container),ee.call(this,this.elements.original,\"destroyed\",!0),A.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(Ue.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&A.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),t(this,\"supports\",(e=>Y.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=Y.touch,this.media=e,A.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||A.nodeList(this.media)||A.array(this.media))&&(this.media=this.media[0]),this.config=N({},_e,nt.defaults,i||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new qe(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",Y),A.nullOrUndefined(this.media)||!A.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!Y.check().api)return void this.debug.error(\"Setup failed: no support\");const s=this.media.cloneNode(!0);s.autoplay=!1,this.elements.original=s;const n=this.media.tagName.toLowerCase();let a=null,r=null;switch(n){case\"div\":if(a=this.media.querySelector(\"iframe\"),A.element(a)){if(r=xe(a.getAttribute(\"src\")),this.provider=function(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?$e.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?$e.vimeo:null}(r.toString()),this.elements.container=this.media,this.media=a,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(A.empty(this.provider)||!Object.values($e).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=Re;break;case\"video\":case\"audio\":this.type=n,this.provider=$e.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=Y.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Ve(this),this.storage=new Te(this),this.media.plyr=this,A.element(this.elements.container)||(this.elements.container=O(\"div\"),_(this.media,this.elements.container)),Ue.migrateStyles.call(this),Ue.addStyleHook.call(this),Je.setup.call(this),this.config.debug&&J.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new He(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&Ue.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ge(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>se(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===$e.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===$e.youtube}get isVimeo(){return this.provider===$e.vimeo}get isVideo(){return this.type===Re}get isAudio(){return this.type===je}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=A.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return A.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=A.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;A.string(t)&&(t=Number(t)),A.number(t)||(t=this.storage.get(\"volume\")),A.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!A.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;A.boolean(t)||(t=this.storage.get(\"muted\")),A.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;A.number(e)&&(t=e),A.number(t)||(t=this.storage.get(\"speed\")),A.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=Ze(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!A.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(A.number),n=!0;if(!i.includes(s)){const e=ae(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=A.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){st.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return A.url(e)?e:this.source}set download(e){A.url(e)&&(this.config.urls.download=e,Me.setDownloadUrl.call(this))}set poster(e){this.isVideo?Ue.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=ce(ue.call(this));return A.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?A.string(e)&&le(e)?(this.config.ratio=ce(e),he.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=A.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){Ne.toggle.call(this,e,!1)}set currentTrack(e){Ne.set.call(this,e,!1),Ne.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){Ne.setLanguage.call(this,e,!1)}get language(){return(Ne.getCurrentTrack.call(this)||{}).language}set pip(e){if(!Y.pip)return;const t=A.boolean(e)?e:!this.pip;A.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?Ie:Oe),A.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return Y.pip?A.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===Ie:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new it(this))}static supported(e,t){return Y.check(e,t)}static loadSprite(e,t){return Ee(e,t)}static setup(e,t={}){let i=null;return A.string(e)?i=Array.from(document.querySelectorAll(e)):A.nodeList(e)?i=Array.from(e):A.array(e)&&(i=e.filter(A.element)),A.empty(i)?null:i.map((e=>new nt(e,t)))}}var at;return nt.defaults=(at=_e,JSON.parse(JSON.stringify(at))),nt}));\n //# sourceMappingURL=plyr.polyfilled.min.js.map\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.min.js.map b/node_modules/plyr/dist/plyr.polyfilled.min.js.map\nindex 2f0ec23..4ab06f1 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.min.js.map\n+++ b/node_modules/plyr/dist/plyr.polyfilled.min.js.map\n@@ -1 +1 @@\n-{\"version\":3,\"sources\":[\"plyr.polyfilled.js\",\"node_modules/.pnpm/custom-event-polyfill@1.0.7/node_modules/custom-event-polyfill/polyfill.js\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"node_modules/.pnpm/url-polyfill@1.1.12/node_modules/url-polyfill/url-polyfill.js\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"navigator\",\"global\",\"factory\",\"exports\",\"module\",\"define\",\"amd\",\"globalThis\",\"self\",\"Plyr\",\"this\",\"window\",\"ce\",\"CustomEvent\",\"cancelable\",\"preventDefault\",\"defaultPrevented\",\"Error\",\"e\",\"event\",\"params\",\"evt\",\"origPrevent\",\"bubbles\",\"document\",\"createEvent\",\"initCustomEvent\",\"detail\",\"call\",\"Object\",\"defineProperty\",\"get\",\"prototype\",\"Event\",\"commonjsGlobal\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"arg\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"TypeError\",\"String\",\"Number\",\"_toPrimitive\",\"_toPropertyKey\",\"enumerable\",\"configurable\",\"writable\",\"_defineProperties\",\"t\",\"n\",\"length\",\"r\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"iteratorSupported\",\"iterator\",\"error\",\"checkIfIteratorIsSupported\",\"createIterator\",\"items\",\"next\",\"shift\",\"done\",\"serializeParam\",\"encodeURIComponent\",\"replace\",\"deserializeParam\",\"decodeURIComponent\",\"URLSearchParams\",\"toString\",\"set\",\"entries\",\"checkIfURLSearchParamsSupported\",\"searchString\",\"typeofSearchString\",\"_fromString\",\"_this\",\"name\",\"append\",\"i\",\"entry\",\"hasOwnProperty\",\"proto\",\"_entries\",\"delete\",\"getAll\",\"slice\",\"has\",\"callback\",\"thisArg\",\"values\",\"searchArray\",\"join\",\"polyfillURLSearchParams\",\"sort\",\"a\",\"b\",\"attribute\",\"attributes\",\"split\",\"u\",\"URL\",\"pathname\",\"href\",\"searchParams\",\"checkIfURLIsSupported\",\"_URL\",\"url\",\"base\",\"baseElement\",\"doc\",\"location\",\"toLowerCase\",\"implementation\",\"createHTMLDocument\",\"createElement\",\"head\",\"appendChild\",\"indexOf\",\"err\",\"anchorElement\",\"body\",\"inputElement\",\"type\",\"protocol\",\"test\",\"checkValidity\",\"search\",\"enableSearchUpdate\",\"enableSearchParamsUpdate\",\"methodName\",\"method\",\"attributeName\",\"_anchorElement\",\"linkURLWithAnchorAttribute\",\"_updateSearchParams\",\"origin\",\"expectedPort\",\"addPortToOrigin\",\"port\",\"hostname\",\"password\",\"username\",\"createObjectURL\",\"blob\",\"revokeObjectURL\",\"polyfillURL\",\"getOrigin\",\"setInterval\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isString\",\"isArray\",\"Array\",\"isNodeList\",\"NodeList\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"isNaN\",\"string\",\"boolean\",\"Boolean\",\"function\",\"Function\",\"array\",\"nodeList\",\"element\",\"Element\",\"empty\",\"round\",\"concat\",\"match\",\"Math\",\"max\",\"getDecimalPlaces\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"_classCallCheck\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"_createClass\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"target\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"c\",\"getBoundingClientRect\",\"width\",\"clientX\",\"left\",\"disabled\",\"dispatchEvent\",\"trigger\",\"from\",\"querySelectorAll\",\"MutationObserver\",\"addedNodes\",\"includes\",\"matches\",\"observe\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isFunction\",\"isEmpty\",\"weakMap\",\"WeakMap\",\"nodeType\",\"ownerDocument\",\"textNode\",\"Text\",\"keyboardEvent\",\"KeyboardEvent\",\"cue\",\"TextTrackCue\",\"VTTCue\",\"track\",\"TextTrack\",\"kind\",\"promise\",\"Promise\",\"then\",\"startsWith\",\"_\",\"transitionEndEvent\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"browser\",\"isIE\",\"documentMode\",\"isEdge\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"getDeep\",\"path\",\"reduce\",\"extend\",\"sources\",\"source\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"insertBefore\",\"setAttributes\",\"setAttribute\",\"text\",\"innerText\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"closest\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"parse\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"format\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"toCamelCase\",\"toPascalCase\",\"getHTML\",\"innerHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"JSON\",\"storage\",\"setItem\",\"stringify\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"status\",\"open\",\"send\",\"loadSprite\",\"prefix\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"current\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"urls\",\"isEmbed\",\"inject\",\"floor\",\"random\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"createDocumentFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"isYouTube\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"providers\",\"types\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"el\",\"parentElement\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"hasAttribute\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"fn\",\"createCommonjsModule\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"loadScript\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"found\",\"parseHash\",\"hashParam\",\"sidedock\",\"gesture\",\"$2\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"fragment\",\"firstChild\",\"stripHTML\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"Ads\",\"google\",\"ima\",\"manager\",\"destroy\",\"displayContainer\",\"remove\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"getProviderByUrl\",\"truthy\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.polyfilled.min.js\",\"sourcesContent\":[\"typeof navigator === \\\"object\\\" && (function (global, factory) {\\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\\n  typeof define === 'function' && define.amd ? define('Plyr', factory) :\\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());\\n})(this, (function () { 'use strict';\\n\\n  // Polyfill for creating CustomEvents on IE9/10/11\\n\\n  // code pulled from:\\n  // https://github.com/d4tocchini/customevent-polyfill\\n  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n  (function () {\\n    if (typeof window === 'undefined') {\\n      return;\\n    }\\n    try {\\n      var ce = new window.CustomEvent('test', {\\n        cancelable: true\\n      });\\n      ce.preventDefault();\\n      if (ce.defaultPrevented !== true) {\\n        // IE has problems with .preventDefault() on custom events\\n        // http://stackoverflow.com/questions/23349191\\n        throw new Error('Could not prevent default');\\n      }\\n    } catch (e) {\\n      var CustomEvent = function (event, params) {\\n        var evt, origPrevent;\\n        params = params || {};\\n        params.bubbles = !!params.bubbles;\\n        params.cancelable = !!params.cancelable;\\n        evt = document.createEvent('CustomEvent');\\n        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);\\n        origPrevent = evt.preventDefault;\\n        evt.preventDefault = function () {\\n          origPrevent.call(this);\\n          try {\\n            Object.defineProperty(this, 'defaultPrevented', {\\n              get: function () {\\n                return true;\\n              }\\n            });\\n          } catch (e) {\\n            this.defaultPrevented = true;\\n          }\\n        };\\n        return evt;\\n      };\\n      CustomEvent.prototype = window.Event.prototype;\\n      window.CustomEvent = CustomEvent; // expose definition to window\\n    }\\n  })();\\n\\n  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\n  function createCommonjsModule(fn, module) {\\n  \\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n  }\\n\\n  (function (global) {\\n    /**\\r\\n     * Polyfill URLSearchParams\\r\\n     *\\r\\n     * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n     */\\n\\n    var checkIfIteratorIsSupported = function () {\\n      try {\\n        return !!Symbol.iterator;\\n      } catch (error) {\\n        return false;\\n      }\\n    };\\n    var iteratorSupported = checkIfIteratorIsSupported();\\n    var createIterator = function (items) {\\n      var iterator = {\\n        next: function () {\\n          var value = items.shift();\\n          return {\\n            done: value === void 0,\\n            value: value\\n          };\\n        }\\n      };\\n      if (iteratorSupported) {\\n        iterator[Symbol.iterator] = function () {\\n          return iterator;\\n        };\\n      }\\n      return iterator;\\n    };\\n\\n    /**\\r\\n     * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n     * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n     */\\n    var serializeParam = function (value) {\\n      return encodeURIComponent(value).replace(/%20/g, '+');\\n    };\\n    var deserializeParam = function (value) {\\n      return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\n    };\\n    var polyfillURLSearchParams = function () {\\n      var URLSearchParams = function (searchString) {\\n        Object.defineProperty(this, '_entries', {\\n          writable: true,\\n          value: {}\\n        });\\n        var typeofSearchString = typeof searchString;\\n        if (typeofSearchString === 'undefined') ; else if (typeofSearchString === 'string') {\\n          if (searchString !== '') {\\n            this._fromString(searchString);\\n          }\\n        } else if (searchString instanceof URLSearchParams) {\\n          var _this = this;\\n          searchString.forEach(function (value, name) {\\n            _this.append(name, value);\\n          });\\n        } else if (searchString !== null && typeofSearchString === 'object') {\\n          if (Object.prototype.toString.call(searchString) === '[object Array]') {\\n            for (var i = 0; i < searchString.length; i++) {\\n              var entry = searchString[i];\\n              if (Object.prototype.toString.call(entry) === '[object Array]' || entry.length !== 2) {\\n                this.append(entry[0], entry[1]);\\n              } else {\\n                throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\n              }\\n            }\\n          } else {\\n            for (var key in searchString) {\\n              if (searchString.hasOwnProperty(key)) {\\n                this.append(key, searchString[key]);\\n              }\\n            }\\n          }\\n        } else {\\n          throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\n        }\\n      };\\n      var proto = URLSearchParams.prototype;\\n      proto.append = function (name, value) {\\n        if (name in this._entries) {\\n          this._entries[name].push(String(value));\\n        } else {\\n          this._entries[name] = [String(value)];\\n        }\\n      };\\n      proto.delete = function (name) {\\n        delete this._entries[name];\\n      };\\n      proto.get = function (name) {\\n        return name in this._entries ? this._entries[name][0] : null;\\n      };\\n      proto.getAll = function (name) {\\n        return name in this._entries ? this._entries[name].slice(0) : [];\\n      };\\n      proto.has = function (name) {\\n        return name in this._entries;\\n      };\\n      proto.set = function (name, value) {\\n        this._entries[name] = [String(value)];\\n      };\\n      proto.forEach = function (callback, thisArg) {\\n        var entries;\\n        for (var name in this._entries) {\\n          if (this._entries.hasOwnProperty(name)) {\\n            entries = this._entries[name];\\n            for (var i = 0; i < entries.length; i++) {\\n              callback.call(thisArg, entries[i], name, this);\\n            }\\n          }\\n        }\\n      };\\n      proto.keys = function () {\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push(name);\\n        });\\n        return createIterator(items);\\n      };\\n      proto.values = function () {\\n        var items = [];\\n        this.forEach(function (value) {\\n          items.push(value);\\n        });\\n        return createIterator(items);\\n      };\\n      proto.entries = function () {\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push([name, value]);\\n        });\\n        return createIterator(items);\\n      };\\n      if (iteratorSupported) {\\n        proto[Symbol.iterator] = proto.entries;\\n      }\\n      proto.toString = function () {\\n        var searchArray = [];\\n        this.forEach(function (value, name) {\\n          searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\n        });\\n        return searchArray.join('&');\\n      };\\n      global.URLSearchParams = URLSearchParams;\\n    };\\n    var checkIfURLSearchParamsSupported = function () {\\n      try {\\n        var URLSearchParams = global.URLSearchParams;\\n        return new URLSearchParams('?a=1').toString() === 'a=1' && typeof URLSearchParams.prototype.set === 'function' && typeof URLSearchParams.prototype.entries === 'function';\\n      } catch (e) {\\n        return false;\\n      }\\n    };\\n    if (!checkIfURLSearchParamsSupported()) {\\n      polyfillURLSearchParams();\\n    }\\n    var proto = global.URLSearchParams.prototype;\\n    if (typeof proto.sort !== 'function') {\\n      proto.sort = function () {\\n        var _this = this;\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push([name, value]);\\n          if (!_this._entries) {\\n            _this.delete(name);\\n          }\\n        });\\n        items.sort(function (a, b) {\\n          if (a[0] < b[0]) {\\n            return -1;\\n          } else if (a[0] > b[0]) {\\n            return +1;\\n          } else {\\n            return 0;\\n          }\\n        });\\n        if (_this._entries) {\\n          // force reset because IE keeps keys index\\n          _this._entries = {};\\n        }\\n        for (var i = 0; i < items.length; i++) {\\n          this.append(items[i][0], items[i][1]);\\n        }\\n      };\\n    }\\n    if (typeof proto._fromString !== 'function') {\\n      Object.defineProperty(proto, '_fromString', {\\n        enumerable: false,\\n        configurable: false,\\n        writable: false,\\n        value: function (searchString) {\\n          if (this._entries) {\\n            this._entries = {};\\n          } else {\\n            var keys = [];\\n            this.forEach(function (value, name) {\\n              keys.push(name);\\n            });\\n            for (var i = 0; i < keys.length; i++) {\\n              this.delete(keys[i]);\\n            }\\n          }\\n          searchString = searchString.replace(/^\\\\?/, '');\\n          var attributes = searchString.split('&');\\n          var attribute;\\n          for (var i = 0; i < attributes.length; i++) {\\n            attribute = attributes[i].split('=');\\n            this.append(deserializeParam(attribute[0]), attribute.length > 1 ? deserializeParam(attribute[1]) : '');\\n          }\\n        }\\n      });\\n    }\\n\\n    // HTMLAnchorElement\\n  })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n  (function (global) {\\n    /**\\r\\n     * Polyfill URL\\r\\n     *\\r\\n     * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n     */\\n\\n    var checkIfURLIsSupported = function () {\\n      try {\\n        var u = new global.URL('b', 'http://a');\\n        u.pathname = 'c d';\\n        return u.href === 'http://a/c%20d' && u.searchParams;\\n      } catch (e) {\\n        return false;\\n      }\\n    };\\n    var polyfillURL = function () {\\n      var _URL = global.URL;\\n      var URL = function (url, base) {\\n        if (typeof url !== 'string') url = String(url);\\n        if (base && typeof base !== 'string') base = String(base);\\n\\n        // Only create another document if the base is different from current location.\\n        var doc = document,\\n          baseElement;\\n        if (base && (global.location === void 0 || base !== global.location.href)) {\\n          base = base.toLowerCase();\\n          doc = document.implementation.createHTMLDocument('');\\n          baseElement = doc.createElement('base');\\n          baseElement.href = base;\\n          doc.head.appendChild(baseElement);\\n          try {\\n            if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\n          } catch (err) {\\n            throw new Error('URL unable to set base ' + base + ' due to ' + err);\\n          }\\n        }\\n        var anchorElement = doc.createElement('a');\\n        anchorElement.href = url;\\n        if (baseElement) {\\n          doc.body.appendChild(anchorElement);\\n          anchorElement.href = anchorElement.href; // force href to refresh\\n        }\\n\\n        var inputElement = doc.createElement('input');\\n        inputElement.type = 'url';\\n        inputElement.value = url;\\n        if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || !inputElement.checkValidity() && !base) {\\n          throw new TypeError('Invalid URL');\\n        }\\n        Object.defineProperty(this, '_anchorElement', {\\n          value: anchorElement\\n        });\\n\\n        // create a linked searchParams which reflect its changes on URL\\n        var searchParams = new global.URLSearchParams(this.search);\\n        var enableSearchUpdate = true;\\n        var enableSearchParamsUpdate = true;\\n        var _this = this;\\n        ['append', 'delete', 'set'].forEach(function (methodName) {\\n          var method = searchParams[methodName];\\n          searchParams[methodName] = function () {\\n            method.apply(searchParams, arguments);\\n            if (enableSearchUpdate) {\\n              enableSearchParamsUpdate = false;\\n              _this.search = searchParams.toString();\\n              enableSearchParamsUpdate = true;\\n            }\\n          };\\n        });\\n        Object.defineProperty(this, 'searchParams', {\\n          value: searchParams,\\n          enumerable: true\\n        });\\n        var search = void 0;\\n        Object.defineProperty(this, '_updateSearchParams', {\\n          enumerable: false,\\n          configurable: false,\\n          writable: false,\\n          value: function () {\\n            if (this.search !== search) {\\n              search = this.search;\\n              if (enableSearchParamsUpdate) {\\n                enableSearchUpdate = false;\\n                this.searchParams._fromString(this.search);\\n                enableSearchUpdate = true;\\n              }\\n            }\\n          }\\n        });\\n      };\\n      var proto = URL.prototype;\\n      var linkURLWithAnchorAttribute = function (attributeName) {\\n        Object.defineProperty(proto, attributeName, {\\n          get: function () {\\n            return this._anchorElement[attributeName];\\n          },\\n          set: function (value) {\\n            this._anchorElement[attributeName] = value;\\n          },\\n          enumerable: true\\n        });\\n      };\\n      ['hash', 'host', 'hostname', 'port', 'protocol'].forEach(function (attributeName) {\\n        linkURLWithAnchorAttribute(attributeName);\\n      });\\n      Object.defineProperty(proto, 'search', {\\n        get: function () {\\n          return this._anchorElement['search'];\\n        },\\n        set: function (value) {\\n          this._anchorElement['search'] = value;\\n          this._updateSearchParams();\\n        },\\n        enumerable: true\\n      });\\n      Object.defineProperties(proto, {\\n        'toString': {\\n          get: function () {\\n            var _this = this;\\n            return function () {\\n              return _this.href;\\n            };\\n          }\\n        },\\n        'href': {\\n          get: function () {\\n            return this._anchorElement.href.replace(/\\\\?$/, '');\\n          },\\n          set: function (value) {\\n            this._anchorElement.href = value;\\n            this._updateSearchParams();\\n          },\\n          enumerable: true\\n        },\\n        'pathname': {\\n          get: function () {\\n            return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\n          },\\n          set: function (value) {\\n            this._anchorElement.pathname = value;\\n          },\\n          enumerable: true\\n        },\\n        'origin': {\\n          get: function () {\\n            // get expected port from protocol\\n            var expectedPort = {\\n              'http:': 80,\\n              'https:': 443,\\n              'ftp:': 21\\n            }[this._anchorElement.protocol];\\n            // add port to origin if, expected port is different than actual port\\n            // and it is not empty f.e http://foo:8080\\n            // 8080 != 80 && 8080 != ''\\n            var addPortToOrigin = this._anchorElement.port != expectedPort && this._anchorElement.port !== '';\\n            return this._anchorElement.protocol + '//' + this._anchorElement.hostname + (addPortToOrigin ? ':' + this._anchorElement.port : '');\\n          },\\n          enumerable: true\\n        },\\n        'password': {\\n          // TODO\\n          get: function () {\\n            return '';\\n          },\\n          set: function (value) {},\\n          enumerable: true\\n        },\\n        'username': {\\n          // TODO\\n          get: function () {\\n            return '';\\n          },\\n          set: function (value) {},\\n          enumerable: true\\n        }\\n      });\\n      URL.createObjectURL = function (blob) {\\n        return _URL.createObjectURL.apply(_URL, arguments);\\n      };\\n      URL.revokeObjectURL = function (url) {\\n        return _URL.revokeObjectURL.apply(_URL, arguments);\\n      };\\n      global.URL = URL;\\n    };\\n    if (!checkIfURLIsSupported()) {\\n      polyfillURL();\\n    }\\n    if (global.location !== void 0 && !('origin' in global.location)) {\\n      var getOrigin = function () {\\n        return global.location.protocol + '//' + global.location.hostname + (global.location.port ? ':' + global.location.port : '');\\n      };\\n      try {\\n        Object.defineProperty(global.location, 'origin', {\\n          get: getOrigin,\\n          enumerable: true\\n        });\\n      } catch (e) {\\n        setInterval(function () {\\n          global.location.origin = getOrigin();\\n        }, 100);\\n      }\\n    }\\n  })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n\\n  function _defineProperty$1(obj, key, value) {\\n    key = _toPropertyKey(key);\\n    if (key in obj) {\\n      Object.defineProperty(obj, key, {\\n        value: value,\\n        enumerable: true,\\n        configurable: true,\\n        writable: true\\n      });\\n    } else {\\n      obj[key] = value;\\n    }\\n    return obj;\\n  }\\n  function _toPrimitive(input, hint) {\\n    if (typeof input !== \\\"object\\\" || input === null) return input;\\n    var prim = input[Symbol.toPrimitive];\\n    if (prim !== undefined) {\\n      var res = prim.call(input, hint || \\\"default\\\");\\n      if (typeof res !== \\\"object\\\") return res;\\n      throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n    }\\n    return (hint === \\\"string\\\" ? String : Number)(input);\\n  }\\n  function _toPropertyKey(arg) {\\n    var key = _toPrimitive(arg, \\\"string\\\");\\n    return typeof key === \\\"symbol\\\" ? key : String(key);\\n  }\\n\\n  function _classCallCheck(e, t) {\\n    if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n  }\\n  function _defineProperties(e, t) {\\n    for (var n = 0; n < t.length; n++) {\\n      var r = t[n];\\n      r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n    }\\n  }\\n  function _createClass(e, t, n) {\\n    return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n  }\\n  function _defineProperty(e, t, n) {\\n    return t in e ? Object.defineProperty(e, t, {\\n      value: n,\\n      enumerable: !0,\\n      configurable: !0,\\n      writable: !0\\n    }) : e[t] = n, e;\\n  }\\n  function ownKeys(e, t) {\\n    var n = Object.keys(e);\\n    if (Object.getOwnPropertySymbols) {\\n      var r = Object.getOwnPropertySymbols(e);\\n      t && (r = r.filter(function (t) {\\n        return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n      })), n.push.apply(n, r);\\n    }\\n    return n;\\n  }\\n  function _objectSpread2(e) {\\n    for (var t = 1; t < arguments.length; t++) {\\n      var n = null != arguments[t] ? arguments[t] : {};\\n      t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n        _defineProperty(e, t, n[t]);\\n      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n      });\\n    }\\n    return e;\\n  }\\n  var defaults$1 = {\\n    addCSS: !0,\\n    thumbWidth: 15,\\n    watch: !0\\n  };\\n  function matches$1(e, t) {\\n    return function () {\\n      return Array.from(document.querySelectorAll(t)).includes(this);\\n    }.call(e, t);\\n  }\\n  function trigger(e, t) {\\n    if (e && t) {\\n      var n = new Event(t, {\\n        bubbles: !0\\n      });\\n      e.dispatchEvent(n);\\n    }\\n  }\\n  var getConstructor$1 = function (e) {\\n      return null != e ? e.constructor : null;\\n    },\\n    instanceOf$1 = function (e, t) {\\n      return !!(e && t && e instanceof t);\\n    },\\n    isNullOrUndefined$1 = function (e) {\\n      return null == e;\\n    },\\n    isObject$1 = function (e) {\\n      return getConstructor$1(e) === Object;\\n    },\\n    isNumber$1 = function (e) {\\n      return getConstructor$1(e) === Number && !Number.isNaN(e);\\n    },\\n    isString$1 = function (e) {\\n      return getConstructor$1(e) === String;\\n    },\\n    isBoolean$1 = function (e) {\\n      return getConstructor$1(e) === Boolean;\\n    },\\n    isFunction$1 = function (e) {\\n      return getConstructor$1(e) === Function;\\n    },\\n    isArray$1 = function (e) {\\n      return Array.isArray(e);\\n    },\\n    isNodeList$1 = function (e) {\\n      return instanceOf$1(e, NodeList);\\n    },\\n    isElement$1 = function (e) {\\n      return instanceOf$1(e, Element);\\n    },\\n    isEvent$1 = function (e) {\\n      return instanceOf$1(e, Event);\\n    },\\n    isEmpty$1 = function (e) {\\n      return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n    },\\n    is$1 = {\\n      nullOrUndefined: isNullOrUndefined$1,\\n      object: isObject$1,\\n      number: isNumber$1,\\n      string: isString$1,\\n      boolean: isBoolean$1,\\n      function: isFunction$1,\\n      array: isArray$1,\\n      nodeList: isNodeList$1,\\n      element: isElement$1,\\n      event: isEvent$1,\\n      empty: isEmpty$1\\n    };\\n  function getDecimalPlaces(e) {\\n    var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n    return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n  }\\n  function round(e, t) {\\n    if (1 > t) {\\n      var n = getDecimalPlaces(t);\\n      return parseFloat(e.toFixed(n));\\n    }\\n    return Math.round(e / t) * t;\\n  }\\n  var RangeTouch = function () {\\n    function e(t, n) {\\n      _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n    }\\n    return _createClass(e, [{\\n      key: \\\"init\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n      }\\n    }, {\\n      key: \\\"destroy\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n      }\\n    }, {\\n      key: \\\"listeners\\\",\\n      value: function (e) {\\n        var t = this,\\n          n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n        [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n          t.element[n](e, function (e) {\\n            return t.set(e);\\n          }, !1);\\n        });\\n      }\\n    }, {\\n      key: \\\"get\\\",\\n      value: function (t) {\\n        if (!e.enabled || !is$1.event(t)) return null;\\n        var n,\\n          r = t.target,\\n          i = t.changedTouches[0],\\n          o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n          s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n          u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n          c = r.getBoundingClientRect(),\\n          a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n        return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n      }\\n    }, {\\n      key: \\\"set\\\",\\n      value: function (t) {\\n        e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n      }\\n    }], [{\\n      key: \\\"setup\\\",\\n      value: function (t) {\\n        var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n          r = null;\\n        if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n        var i = _objectSpread2({}, defaults$1, {}, n);\\n        if (is$1.string(t) && i.watch) {\\n          var o = new MutationObserver(function (n) {\\n            Array.from(n).forEach(function (n) {\\n              Array.from(n.addedNodes).forEach(function (n) {\\n                is$1.element(n) && matches$1(n, t) && new e(n, i);\\n              });\\n            });\\n          });\\n          o.observe(document.body, {\\n            childList: !0,\\n            subtree: !0\\n          });\\n        }\\n        return r.map(function (t) {\\n          return new e(t, n);\\n        });\\n      }\\n    }, {\\n      key: \\\"enabled\\\",\\n      get: function () {\\n        return \\\"ontouchstart\\\" in document.documentElement;\\n      }\\n    }]), e;\\n  }();\\n\\n  // ==========================================================================\\n  // Type checking utils\\n  // ==========================================================================\\n\\n  const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\n  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\n  const isNullOrUndefined = input => input === null || typeof input === 'undefined';\\n  const isObject = input => getConstructor(input) === Object;\\n  const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\n  const isString = input => getConstructor(input) === String;\\n  const isBoolean = input => getConstructor(input) === Boolean;\\n  const isFunction = input => typeof input === 'function';\\n  const isArray = input => Array.isArray(input);\\n  const isWeakMap = input => instanceOf(input, WeakMap);\\n  const isNodeList = input => instanceOf(input, NodeList);\\n  const isTextNode = input => getConstructor(input) === Text;\\n  const isEvent = input => instanceOf(input, Event);\\n  const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\n  const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\n  const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\n  const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\n  const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\n  const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\n  const isUrl = input => {\\n    // Accept a URL object\\n    if (instanceOf(input, window.URL)) {\\n      return true;\\n    }\\n\\n    // Must be string from here\\n    if (!isString(input)) {\\n      return false;\\n    }\\n\\n    // Add the protocol if required\\n    let string = input;\\n    if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n      string = `http://${input}`;\\n    }\\n    try {\\n      return !isEmpty(new URL(string).hostname);\\n    } catch (_) {\\n      return false;\\n    }\\n  };\\n  var is = {\\n    nullOrUndefined: isNullOrUndefined,\\n    object: isObject,\\n    number: isNumber,\\n    string: isString,\\n    boolean: isBoolean,\\n    function: isFunction,\\n    array: isArray,\\n    weakMap: isWeakMap,\\n    nodeList: isNodeList,\\n    element: isElement,\\n    textNode: isTextNode,\\n    event: isEvent,\\n    keyboardEvent: isKeyboardEvent,\\n    cue: isCue,\\n    track: isTrack,\\n    promise: isPromise,\\n    url: isUrl,\\n    empty: isEmpty\\n  };\\n\\n  // ==========================================================================\\n  const transitionEndEvent = (() => {\\n    const element = document.createElement('span');\\n    const events = {\\n      WebkitTransition: 'webkitTransitionEnd',\\n      MozTransition: 'transitionend',\\n      OTransition: 'oTransitionEnd otransitionend',\\n      transition: 'transitionend'\\n    };\\n    const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n    return is.string(type) ? events[type] : false;\\n  })();\\n\\n  // Force repaint of element\\n  function repaint(element, delay) {\\n    setTimeout(() => {\\n      try {\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = true;\\n\\n        // eslint-disable-next-line no-unused-expressions\\n        element.offsetHeight;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = false;\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    }, delay);\\n  }\\n\\n  // ==========================================================================\\n  // Browser sniffing\\n  // Unfortunately, due to mixed support, UA sniffing is required\\n  // ==========================================================================\\n\\n  const isIE = Boolean(window.document.documentMode);\\n  const isEdge = /Edge/g.test(navigator.userAgent);\\n  const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\n  const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  // navigator.platform may be deprecated but this check is still required\\n  const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\n  const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  var browser = {\\n    isIE,\\n    isEdge,\\n    isWebKit,\\n    isIPhone,\\n    isIPadOS,\\n    isIos\\n  };\\n\\n  // ==========================================================================\\n\\n  // Clone nested objects\\n  function cloneDeep(object) {\\n    return JSON.parse(JSON.stringify(object));\\n  }\\n\\n  // Get a nested value in an object\\n  function getDeep(object, path) {\\n    return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n  }\\n\\n  // Deep extend destination object with N more objects\\n  function extend(target = {}, ...sources) {\\n    if (!sources.length) {\\n      return target;\\n    }\\n    const source = sources.shift();\\n    if (!is.object(source)) {\\n      return target;\\n    }\\n    Object.keys(source).forEach(key => {\\n      if (is.object(source[key])) {\\n        if (!Object.keys(target).includes(key)) {\\n          Object.assign(target, {\\n            [key]: {}\\n          });\\n        }\\n        extend(target[key], source[key]);\\n      } else {\\n        Object.assign(target, {\\n          [key]: source[key]\\n        });\\n      }\\n    });\\n    return extend(target, ...sources);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Wrap an element\\n  function wrap(elements, wrapper) {\\n    // Convert `elements` to an array, if necessary.\\n    const targets = elements.length ? elements : [elements];\\n\\n    // Loops backwards to prevent having to clone the wrapper on the\\n    // first element (see `child` below).\\n    Array.from(targets).reverse().forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n  }\\n\\n  // Set attributes\\n  function setAttributes(element, attributes) {\\n    if (!is.element(element) || is.empty(attributes)) return;\\n\\n    // Assume null and undefined attributes should be left out,\\n    // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n    Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n  }\\n\\n  // Create a DocumentFragment\\n  function createElement(type, attributes, text) {\\n    // Create a new <element>\\n    const element = document.createElement(type);\\n\\n    // Set all passed attributes\\n    if (is.object(attributes)) {\\n      setAttributes(element, attributes);\\n    }\\n\\n    // Add text node\\n    if (is.string(text)) {\\n      element.innerText = text;\\n    }\\n\\n    // Return built element\\n    return element;\\n  }\\n\\n  // Insert an element after another\\n  function insertAfter(element, target) {\\n    if (!is.element(element) || !is.element(target)) return;\\n    target.parentNode.insertBefore(element, target.nextSibling);\\n  }\\n\\n  // Insert a DocumentFragment\\n  function insertElement(type, parent, attributes, text) {\\n    if (!is.element(parent)) return;\\n    parent.appendChild(createElement(type, attributes, text));\\n  }\\n\\n  // Remove element(s)\\n  function removeElement(element) {\\n    if (is.nodeList(element) || is.array(element)) {\\n      Array.from(element).forEach(removeElement);\\n      return;\\n    }\\n    if (!is.element(element) || !is.element(element.parentNode)) {\\n      return;\\n    }\\n    element.parentNode.removeChild(element);\\n  }\\n\\n  // Remove all child elements\\n  function emptyElement(element) {\\n    if (!is.element(element)) return;\\n    let {\\n      length\\n    } = element.childNodes;\\n    while (length > 0) {\\n      element.removeChild(element.lastChild);\\n      length -= 1;\\n    }\\n  }\\n\\n  // Replace element\\n  function replaceElement(newChild, oldChild) {\\n    if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n    oldChild.parentNode.replaceChild(newChild, oldChild);\\n    return newChild;\\n  }\\n\\n  // Get an attribute object from a string selector\\n  function getAttributesFromSelector(sel, existingAttributes) {\\n    // For example:\\n    // '.test' to { class: 'test' }\\n    // '#test' to { id: 'test' }\\n    // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n    if (!is.string(sel) || is.empty(sel)) return {};\\n    const attributes = {};\\n    const existing = extend({}, existingAttributes);\\n    sel.split(',').forEach(s => {\\n      // Remove whitespace\\n      const selector = s.trim();\\n      const className = selector.replace('.', '');\\n      const stripped = selector.replace(/[[\\\\]]/g, '');\\n      // Get the parts and value\\n      const parts = stripped.split('=');\\n      const [key] = parts;\\n      const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n      // Get the first character\\n      const start = selector.charAt(0);\\n      switch (start) {\\n        case '.':\\n          // Add to existing classname\\n          if (is.string(existing.class)) {\\n            attributes.class = `${existing.class} ${className}`;\\n          } else {\\n            attributes.class = className;\\n          }\\n          break;\\n        case '#':\\n          // ID selector\\n          attributes.id = selector.replace('#', '');\\n          break;\\n        case '[':\\n          // Attribute selector\\n          attributes[key] = value;\\n          break;\\n      }\\n    });\\n    return extend(existing, attributes);\\n  }\\n\\n  // Toggle hidden\\n  function toggleHidden(element, hidden) {\\n    if (!is.element(element)) return;\\n    let hide = hidden;\\n    if (!is.boolean(hide)) {\\n      hide = !element.hidden;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    element.hidden = hide;\\n  }\\n\\n  // Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\n  function toggleClass(element, className, force) {\\n    if (is.nodeList(element)) {\\n      return Array.from(element).map(e => toggleClass(e, className, force));\\n    }\\n    if (is.element(element)) {\\n      let method = 'toggle';\\n      if (typeof force !== 'undefined') {\\n        method = force ? 'add' : 'remove';\\n      }\\n      element.classList[method](className);\\n      return element.classList.contains(className);\\n    }\\n    return false;\\n  }\\n\\n  // Has class name\\n  function hasClass(element, className) {\\n    return is.element(element) && element.classList.contains(className);\\n  }\\n\\n  // Element matches selector\\n  function matches(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n    function match() {\\n      return Array.from(document.querySelectorAll(selector)).includes(this);\\n    }\\n    const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n    return method.call(element, selector);\\n  }\\n\\n  // Closest ancestor element matching selector (also tests element itself)\\n  function closest$1(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n\\n    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n    function closestElement() {\\n      let el = this;\\n      do {\\n        if (matches.matches(el, selector)) return el;\\n        el = el.parentElement || el.parentNode;\\n      } while (el !== null && el.nodeType === 1);\\n      return null;\\n    }\\n    const method = prototype.closest || closestElement;\\n    return method.call(element, selector);\\n  }\\n\\n  // Find all elements\\n  function getElements(selector) {\\n    return this.elements.container.querySelectorAll(selector);\\n  }\\n\\n  // Find a single element\\n  function getElement(selector) {\\n    return this.elements.container.querySelector(selector);\\n  }\\n\\n  // Set focus and tab focus class\\n  function setFocus(element = null, focusVisible = false) {\\n    if (!is.element(element)) return;\\n\\n    // Set regular focus\\n    element.focus({\\n      preventScroll: true,\\n      focusVisible\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Default codecs for checking mimetype support\\n  const defaultCodecs = {\\n    'audio/ogg': 'vorbis',\\n    'audio/wav': '1',\\n    'video/webm': 'vp8, vorbis',\\n    'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n    'video/ogg': 'theora'\\n  };\\n\\n  // Check for feature support\\n  const support = {\\n    // Basic support\\n    audio: 'canPlayType' in document.createElement('audio'),\\n    video: 'canPlayType' in document.createElement('video'),\\n    // Check for support\\n    // Basic functionality vs full UI\\n    check(type, provider) {\\n      const api = support[type] || provider !== 'html5';\\n      const ui = api && support.rangeInput;\\n      return {\\n        api,\\n        ui\\n      };\\n    },\\n    // Picture-in-picture support\\n    // Safari & Chrome only currently\\n    pip: (() => {\\n      // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n      // It will throw the following error when trying to enter picture-in-picture\\n      // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n      if (browser.isIPhone) {\\n        return false;\\n      }\\n\\n      // Safari\\n      // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n      if (is.function(createElement('video').webkitSetPresentationMode)) {\\n        return true;\\n      }\\n\\n      // Chrome\\n      // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n      if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n        return true;\\n      }\\n      return false;\\n    })(),\\n    // Airplay support\\n    // Safari only currently\\n    airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n    // Inline playback support\\n    // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n    playsinline: 'playsInline' in document.createElement('video'),\\n    // Check for mime type support against a player instance\\n    // Credits: http://diveintohtml5.info/everything.html\\n    // Related: http://www.leanbackplayer.com/test/h5mt.html\\n    mime(input) {\\n      if (is.empty(input)) {\\n        return false;\\n      }\\n      const [mediaType] = input.split('/');\\n      let type = input;\\n\\n      // Verify we're using HTML5 and there's no media type mismatch\\n      if (!this.isHTML5 || mediaType !== this.type) {\\n        return false;\\n      }\\n\\n      // Add codec if required\\n      if (Object.keys(defaultCodecs).includes(type)) {\\n        type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n      }\\n      try {\\n        return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n      } catch (_) {\\n        return false;\\n      }\\n    },\\n    // Check for textTracks support\\n    textTracks: 'textTracks' in document.createElement('video'),\\n    // <input type=\\\"range\\\"> Sliders\\n    rangeInput: (() => {\\n      const range = document.createElement('input');\\n      range.type = 'range';\\n      return range.type === 'range';\\n    })(),\\n    // Touch\\n    // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n    touch: 'ontouchstart' in document.documentElement,\\n    // Detect transitions support\\n    transitions: transitionEndEvent !== false,\\n    // Reduced motion iOS & MacOS setting\\n    // https://webkit.org/blog/7551/responsive-design-for-motion/\\n    reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n  };\\n\\n  // ==========================================================================\\n\\n  // Check for passive event listener support\\n  // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n  // https://www.youtube.com/watch?v=NPM6172J22g\\n  const supportsPassiveListeners = (() => {\\n    // Test via a getter in the options object to see if the passive property is accessed\\n    let supported = false;\\n    try {\\n      const options = Object.defineProperty({}, 'passive', {\\n        get() {\\n          supported = true;\\n          return null;\\n        }\\n      });\\n      window.addEventListener('test', null, options);\\n      window.removeEventListener('test', null, options);\\n    } catch (_) {\\n      // Do nothing\\n    }\\n    return supported;\\n  })();\\n\\n  // Toggle event listener\\n  function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n    // Bail if no element, event, or callback\\n    if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n      return;\\n    }\\n\\n    // Allow multiple events\\n    const events = event.split(' ');\\n    // Build options\\n    // Default to just the capture boolean for browsers with no passive listener support\\n    let options = capture;\\n\\n    // If passive events listeners are supported\\n    if (supportsPassiveListeners) {\\n      options = {\\n        // Whether the listener can be passive (i.e. default never prevented)\\n        passive,\\n        // Whether the listener is a capturing listener or not\\n        capture\\n      };\\n    }\\n\\n    // If a single node is passed, bind the event listener\\n    events.forEach(type => {\\n      if (this && this.eventListeners && toggle) {\\n        // Cache event listener\\n        this.eventListeners.push({\\n          element,\\n          type,\\n          callback,\\n          options\\n        });\\n      }\\n      element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n    });\\n  }\\n\\n  // Bind event handler\\n  function on(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, true, passive, capture);\\n  }\\n\\n  // Unbind event handler\\n  function off(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, false, passive, capture);\\n  }\\n\\n  // Bind once-only event handler\\n  function once(element, events = '', callback, passive = true, capture = false) {\\n    const onceCallback = (...args) => {\\n      off(element, events, onceCallback, passive, capture);\\n      callback.apply(this, args);\\n    };\\n    toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n  }\\n\\n  // Trigger event\\n  function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n    // Bail if no element\\n    if (!is.element(element) || is.empty(type)) {\\n      return;\\n    }\\n\\n    // Create and dispatch the event\\n    const event = new CustomEvent(type, {\\n      bubbles,\\n      detail: {\\n        ...detail,\\n        plyr: this\\n      }\\n    });\\n\\n    // Dispatch the event\\n    element.dispatchEvent(event);\\n  }\\n\\n  // Unbind all cached event listeners\\n  function unbindListeners() {\\n    if (this && this.eventListeners) {\\n      this.eventListeners.forEach(item => {\\n        const {\\n          element,\\n          type,\\n          callback,\\n          options\\n        } = item;\\n        element.removeEventListener(type, callback, options);\\n      });\\n      this.eventListeners = [];\\n    }\\n  }\\n\\n  // Run method when / if player is ready\\n  function ready() {\\n    return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n  }\\n\\n  /**\\n   * Silence a Promise-like object.\\n   * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n   * play promise\\\" rejection error messages.\\n   * @param  {Object} value An object that may or may not be `Promise`-like.\\n   */\\n  function silencePromise(value) {\\n    if (is.promise(value)) {\\n      value.then(null, () => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Remove duplicates in an array\\n  function dedupe(array) {\\n    if (!is.array(array)) {\\n      return array;\\n    }\\n    return array.filter((item, index) => array.indexOf(item) === index);\\n  }\\n\\n  // Get the closest value in an array\\n  function closest(array, value) {\\n    if (!is.array(array) || !array.length) {\\n      return null;\\n    }\\n    return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Check support for a CSS declaration\\n  function supportsCSS(declaration) {\\n    if (!window || !window.CSS) {\\n      return false;\\n    }\\n    return window.CSS.supports(declaration);\\n  }\\n\\n  // Standard/common aspect ratios\\n  const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n    ...out,\\n    [x / y]: [x, y]\\n  }), {});\\n\\n  // Validate an aspect ratio\\n  function validateAspectRatio(input) {\\n    if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n      return false;\\n    }\\n    const ratio = is.array(input) ? input : input.split(':');\\n    return ratio.map(Number).every(is.number);\\n  }\\n\\n  // Reduce an aspect ratio to it's lowest form\\n  function reduceAspectRatio(ratio) {\\n    if (!is.array(ratio) || !ratio.every(is.number)) {\\n      return null;\\n    }\\n    const [width, height] = ratio;\\n    const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n    const divider = getDivider(width, height);\\n    return [width / divider, height / divider];\\n  }\\n\\n  // Calculate an aspect ratio\\n  function getAspectRatio(input) {\\n    const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n    // Try provided ratio\\n    let ratio = parse(input);\\n\\n    // Get from config\\n    if (ratio === null) {\\n      ratio = parse(this.config.ratio);\\n    }\\n\\n    // Get from embed\\n    if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n      ({\\n        ratio\\n      } = this.embed);\\n    }\\n\\n    // Get from HTML5 video\\n    if (ratio === null && this.isHTML5) {\\n      const {\\n        videoWidth,\\n        videoHeight\\n      } = this.media;\\n      ratio = [videoWidth, videoHeight];\\n    }\\n    return reduceAspectRatio(ratio);\\n  }\\n\\n  // Set aspect ratio for responsive container\\n  function setAspectRatio(input) {\\n    if (!this.isVideo) {\\n      return {};\\n    }\\n    const {\\n      wrapper\\n    } = this.elements;\\n    const ratio = getAspectRatio.call(this, input);\\n    if (!is.array(ratio)) {\\n      return {};\\n    }\\n    const [x, y] = reduceAspectRatio(ratio);\\n    const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n    const padding = 100 / x * y;\\n    if (useNative) {\\n      wrapper.style.aspectRatio = `${x}/${y}`;\\n    } else {\\n      wrapper.style.paddingBottom = `${padding}%`;\\n    }\\n\\n    // For Vimeo we have an extra <div> to hide the standard controls and UI\\n    if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n      const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n      const offset = (height - padding) / (height / 50);\\n      if (this.fullscreen.active) {\\n        wrapper.style.paddingBottom = null;\\n      } else {\\n        this.media.style.transform = `translateY(-${offset}%)`;\\n      }\\n    } else if (this.isHTML5) {\\n      wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n    }\\n    return {\\n      padding,\\n      ratio\\n    };\\n  }\\n\\n  // Round an aspect ratio to closest standard ratio\\n  function roundAspectRatio(x, y, tolerance = 0.05) {\\n    const ratio = x / y;\\n    const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n    // Check match is within tolerance\\n    if (Math.abs(closestRatio - ratio) <= tolerance) {\\n      return standardRatios[closestRatio];\\n    }\\n\\n    // No match\\n    return [x, y];\\n  }\\n\\n  // Get the size of the viewport\\n  // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\n  function getViewportSize() {\\n    const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n    const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n    return [width, height];\\n  }\\n\\n  // ==========================================================================\\n  const html5 = {\\n    getSources() {\\n      if (!this.isHTML5) {\\n        return [];\\n      }\\n      const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n      // Filter out unsupported sources (if type is specified)\\n      return sources.filter(source => {\\n        const type = source.getAttribute('type');\\n        if (is.empty(type)) {\\n          return true;\\n        }\\n        return support.mime.call(this, type);\\n      });\\n    },\\n    // Get quality levels\\n    getQualityOptions() {\\n      // Whether we're forcing all options (e.g. for streaming)\\n      if (this.config.quality.forced) {\\n        return this.config.quality.options;\\n      }\\n\\n      // Get sizes from <source> elements\\n      return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n    },\\n    setup() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n      const player = this;\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set aspect ratio if fixed\\n      if (!is.empty(this.config.ratio)) {\\n        setAspectRatio.call(player);\\n      }\\n\\n      // Quality\\n      Object.defineProperty(player.media, 'quality', {\\n        get() {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n          // Return size, if match is found\\n          return source && Number(source.getAttribute('size'));\\n        },\\n        set(input) {\\n          if (player.quality === input) {\\n            return;\\n          }\\n\\n          // If we're using an external handler...\\n          if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n            player.config.quality.onChange(input);\\n          } else {\\n            // Get sources\\n            const sources = html5.getSources.call(player);\\n            // Get first match for requested size\\n            const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n            // No matching source found\\n            if (!source) {\\n              return;\\n            }\\n\\n            // Get current state\\n            const {\\n              currentTime,\\n              paused,\\n              preload,\\n              readyState,\\n              playbackRate\\n            } = player.media;\\n\\n            // Set new source\\n            player.media.src = source.getAttribute('src');\\n\\n            // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n            if (preload !== 'none' || readyState) {\\n              // Restore time\\n              player.once('loadedmetadata', () => {\\n                player.speed = playbackRate;\\n                player.currentTime = currentTime;\\n\\n                // Resume playing\\n                if (!paused) {\\n                  silencePromise(player.play());\\n                }\\n              });\\n\\n              // Load new source\\n              player.media.load();\\n            }\\n          }\\n\\n          // Trigger change event\\n          triggerEvent.call(player, player.media, 'qualitychange', false, {\\n            quality: input\\n          });\\n        }\\n      });\\n    },\\n    // Cancel current network requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    cancelRequests() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n\\n      // Remove child sources\\n      removeElement(html5.getSources.call(this));\\n\\n      // Set blank video src attribute\\n      // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n      // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n      this.media.setAttribute('src', this.config.blankVideo);\\n\\n      // Load the new empty source\\n      // This will cancel existing requests\\n      // See https://github.com/sampotts/plyr/issues/174\\n      this.media.load();\\n\\n      // Debugging\\n      this.debug.log('Cancelled network requests');\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Generate a random ID\\n  function generateId(prefix) {\\n    return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n  }\\n\\n  // Format string\\n  function format(input, ...args) {\\n    if (is.empty(input)) return input;\\n    return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n  }\\n\\n  // Get percentage\\n  function getPercentage(current, max) {\\n    if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n      return 0;\\n    }\\n    return (current / max * 100).toFixed(2);\\n  }\\n\\n  // Replace all occurrences of a string in a string\\n  const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n  // Convert to title case\\n  const toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n  // Convert string to pascalCase\\n  function toPascalCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert kebab case\\n    string = replaceAll(string, '-', ' ');\\n\\n    // Convert snake case\\n    string = replaceAll(string, '_', ' ');\\n\\n    // Convert to title case\\n    string = toTitleCase(string);\\n\\n    // Convert to pascal case\\n    return replaceAll(string, ' ', '');\\n  }\\n\\n  // Convert string to pascalCase\\n  function toCamelCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert to pascal case\\n    string = toPascalCase(string);\\n\\n    // Convert first character to lowercase\\n    return string.charAt(0).toLowerCase() + string.slice(1);\\n  }\\n\\n  // Remove HTML from a string\\n  function stripHTML(source) {\\n    const fragment = document.createDocumentFragment();\\n    const element = document.createElement('div');\\n    fragment.appendChild(element);\\n    element.innerHTML = source;\\n    return fragment.firstChild.innerText;\\n  }\\n\\n  // Like outerHTML, but also works for DocumentFragment\\n  function getHTML(element) {\\n    const wrapper = document.createElement('div');\\n    wrapper.appendChild(element);\\n    return wrapper.innerHTML;\\n  }\\n\\n  // ==========================================================================\\n\\n  // Skip i18n for abbreviations and brand names\\n  const resources = {\\n    pip: 'PIP',\\n    airplay: 'AirPlay',\\n    html5: 'HTML5',\\n    vimeo: 'Vimeo',\\n    youtube: 'YouTube'\\n  };\\n  const i18n = {\\n    get(key = '', config = {}) {\\n      if (is.empty(key) || is.empty(config)) {\\n        return '';\\n      }\\n      let string = getDeep(config.i18n, key);\\n      if (is.empty(string)) {\\n        if (Object.keys(resources).includes(key)) {\\n          return resources[key];\\n        }\\n        return '';\\n      }\\n      const replace = {\\n        '{seektime}': config.seekTime,\\n        '{title}': config.title\\n      };\\n      Object.entries(replace).forEach(([k, v]) => {\\n        string = replaceAll(string, k, v);\\n      });\\n      return string;\\n    }\\n  };\\n\\n  class Storage {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"get\\\", key => {\\n        if (!Storage.supported || !this.enabled) {\\n          return null;\\n        }\\n        const store = window.localStorage.getItem(this.key);\\n        if (is.empty(store)) {\\n          return null;\\n        }\\n        const json = JSON.parse(store);\\n        return is.string(key) && key.length ? json[key] : json;\\n      });\\n      _defineProperty$1(this, \\\"set\\\", object => {\\n        // Bail if we don't have localStorage support or it's disabled\\n        if (!Storage.supported || !this.enabled) {\\n          return;\\n        }\\n\\n        // Can only store objectst\\n        if (!is.object(object)) {\\n          return;\\n        }\\n\\n        // Get current storage\\n        let storage = this.get();\\n\\n        // Default to empty object\\n        if (is.empty(storage)) {\\n          storage = {};\\n        }\\n\\n        // Update the working copy of the values\\n        extend(storage, object);\\n\\n        // Update storage\\n        try {\\n          window.localStorage.setItem(this.key, JSON.stringify(storage));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      });\\n      this.enabled = player.config.storage.enabled;\\n      this.key = player.config.storage.key;\\n    }\\n\\n    // Check for actual support (see if we can use it)\\n    static get supported() {\\n      try {\\n        if (!('localStorage' in window)) {\\n          return false;\\n        }\\n        const test = '___test';\\n\\n        // Try to use it (it might be disabled, e.g. user is in private mode)\\n        // see: https://github.com/sampotts/plyr/issues/131\\n        window.localStorage.setItem(test, test);\\n        window.localStorage.removeItem(test);\\n        return true;\\n      } catch (_) {\\n        return false;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Fetch wrapper\\n  // Using XHR to avoid issues with older browsers\\n  // ==========================================================================\\n\\n  function fetch(url, responseType = 'text') {\\n    return new Promise((resolve, reject) => {\\n      try {\\n        const request = new XMLHttpRequest();\\n\\n        // Check for CORS support\\n        if (!('withCredentials' in request)) {\\n          return;\\n        }\\n        request.addEventListener('load', () => {\\n          if (responseType === 'text') {\\n            try {\\n              resolve(JSON.parse(request.responseText));\\n            } catch (_) {\\n              resolve(request.responseText);\\n            }\\n          } else {\\n            resolve(request.response);\\n          }\\n        });\\n        request.addEventListener('error', () => {\\n          throw new Error(request.status);\\n        });\\n        request.open('GET', url, true);\\n\\n        // Set the required response type\\n        request.responseType = responseType;\\n        request.send();\\n      } catch (error) {\\n        reject(error);\\n      }\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Load an external SVG sprite\\n  function loadSprite(url, id) {\\n    if (!is.string(url)) {\\n      return;\\n    }\\n    const prefix = 'cache';\\n    const hasId = is.string(id);\\n    let isCached = false;\\n    const exists = () => document.getElementById(id) !== null;\\n    const update = (container, data) => {\\n      // eslint-disable-next-line no-param-reassign\\n      container.innerHTML = data;\\n\\n      // Check again incase of race condition\\n      if (hasId && exists()) {\\n        return;\\n      }\\n\\n      // Inject the SVG to the body\\n      document.body.insertAdjacentElement('afterbegin', container);\\n    };\\n\\n    // Only load once if ID set\\n    if (!hasId || !exists()) {\\n      const useStorage = Storage.supported;\\n      // Create container\\n      const container = document.createElement('div');\\n      container.setAttribute('hidden', '');\\n      if (hasId) {\\n        container.setAttribute('id', id);\\n      }\\n\\n      // Check in cache\\n      if (useStorage) {\\n        const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n        isCached = cached !== null;\\n        if (isCached) {\\n          const data = JSON.parse(cached);\\n          update(container, data.content);\\n        }\\n      }\\n\\n      // Get the sprite\\n      fetch(url).then(result => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n              content: result\\n            }));\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n        update(container, result);\\n      }).catch(() => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Time helpers\\n  const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\n  const getMinutes = value => Math.trunc(value / 60 % 60, 10);\\n  const getSeconds = value => Math.trunc(value % 60, 10);\\n\\n  // Format time to UI friendly string\\n  function formatTime(time = 0, displayHours = false, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return formatTime(undefined, displayHours, inverted);\\n    }\\n\\n    // Format time component to add leading zero\\n    const format = value => `0${value}`.slice(-2);\\n    // Breakdown to hours, mins, secs\\n    let hours = getHours(time);\\n    const mins = getMinutes(time);\\n    const secs = getSeconds(time);\\n\\n    // Do we need to display hours?\\n    if (displayHours || hours > 0) {\\n      hours = `${hours}:`;\\n    } else {\\n      hours = '';\\n    }\\n\\n    // Render\\n    return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n  }\\n\\n  // ==========================================================================\\n\\n  // TODO: Don't export a massive object - break down and create class\\n  const controls = {\\n    // Get icon URL\\n    getIconUrl() {\\n      const url = new URL(this.config.iconUrl, window.location);\\n      const host = window.location.host ? window.location.host : window.top.location.host;\\n      const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n      return {\\n        url: this.config.iconUrl,\\n        cors\\n      };\\n    },\\n    // Find the UI controls\\n    findElements() {\\n      try {\\n        this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n        // Buttons\\n        this.elements.buttons = {\\n          play: getElements.call(this, this.config.selectors.buttons.play),\\n          pause: getElement.call(this, this.config.selectors.buttons.pause),\\n          restart: getElement.call(this, this.config.selectors.buttons.restart),\\n          rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n          fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n          mute: getElement.call(this, this.config.selectors.buttons.mute),\\n          pip: getElement.call(this, this.config.selectors.buttons.pip),\\n          airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n          settings: getElement.call(this, this.config.selectors.buttons.settings),\\n          captions: getElement.call(this, this.config.selectors.buttons.captions),\\n          fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n        };\\n\\n        // Progress\\n        this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n        // Inputs\\n        this.elements.inputs = {\\n          seek: getElement.call(this, this.config.selectors.inputs.seek),\\n          volume: getElement.call(this, this.config.selectors.inputs.volume)\\n        };\\n\\n        // Display\\n        this.elements.display = {\\n          buffer: getElement.call(this, this.config.selectors.display.buffer),\\n          currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n          duration: getElement.call(this, this.config.selectors.display.duration)\\n        };\\n\\n        // Seek tooltip\\n        if (is.element(this.elements.progress)) {\\n          this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n        }\\n        return true;\\n      } catch (error) {\\n        // Log it\\n        this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n        // Restore native video controls\\n        this.toggleNativeControls(true);\\n        return false;\\n      }\\n    },\\n    // Create <svg> icon\\n    createIcon(type, attributes) {\\n      const namespace = 'http://www.w3.org/2000/svg';\\n      const iconUrl = controls.getIconUrl.call(this);\\n      const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n      // Create <svg>\\n      const icon = document.createElementNS(namespace, 'svg');\\n      setAttributes(icon, extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false'\\n      }));\\n\\n      // Create the <use> to reference sprite\\n      const use = document.createElementNS(namespace, 'use');\\n      const path = `${iconPath}-${type}`;\\n\\n      // Set `href` attributes\\n      // https://github.com/sampotts/plyr/issues/460\\n      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n      if ('href' in use) {\\n        use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n      }\\n\\n      // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n      // Add <use> to <svg>\\n      icon.appendChild(use);\\n      return icon;\\n    },\\n    // Create hidden text label\\n    createLabel(key, attr = {}) {\\n      const text = i18n.get(key, this.config);\\n      const attributes = {\\n        ...attr,\\n        class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n      };\\n      return createElement('span', attributes, text);\\n    },\\n    // Create a badge\\n    createBadge(text) {\\n      if (is.empty(text)) {\\n        return null;\\n      }\\n      const badge = createElement('span', {\\n        class: this.config.classNames.menu.value\\n      });\\n      badge.appendChild(createElement('span', {\\n        class: this.config.classNames.menu.badge\\n      }, text));\\n      return badge;\\n    },\\n    // Create a <button>\\n    createButton(buttonType, attr) {\\n      const attributes = extend({}, attr);\\n      let type = toCamelCase(buttonType);\\n      const props = {\\n        element: 'button',\\n        toggle: false,\\n        label: null,\\n        icon: null,\\n        labelPressed: null,\\n        iconPressed: null\\n      };\\n      ['element', 'icon', 'label'].forEach(key => {\\n        if (Object.keys(attributes).includes(key)) {\\n          props[key] = attributes[key];\\n          delete attributes[key];\\n        }\\n      });\\n\\n      // Default to 'button' type to prevent form submission\\n      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n        attributes.type = 'button';\\n      }\\n\\n      // Set class name\\n      if (Object.keys(attributes).includes('class')) {\\n        if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n          extend(attributes, {\\n            class: `${attributes.class} ${this.config.classNames.control}`\\n          });\\n        }\\n      } else {\\n        attributes.class = this.config.classNames.control;\\n      }\\n\\n      // Large play button\\n      switch (buttonType) {\\n        case 'play':\\n          props.toggle = true;\\n          props.label = 'play';\\n          props.labelPressed = 'pause';\\n          props.icon = 'play';\\n          props.iconPressed = 'pause';\\n          break;\\n        case 'mute':\\n          props.toggle = true;\\n          props.label = 'mute';\\n          props.labelPressed = 'unmute';\\n          props.icon = 'volume';\\n          props.iconPressed = 'muted';\\n          break;\\n        case 'captions':\\n          props.toggle = true;\\n          props.label = 'enableCaptions';\\n          props.labelPressed = 'disableCaptions';\\n          props.icon = 'captions-off';\\n          props.iconPressed = 'captions-on';\\n          break;\\n        case 'fullscreen':\\n          props.toggle = true;\\n          props.label = 'enterFullscreen';\\n          props.labelPressed = 'exitFullscreen';\\n          props.icon = 'enter-fullscreen';\\n          props.iconPressed = 'exit-fullscreen';\\n          break;\\n        case 'play-large':\\n          attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n          type = 'play';\\n          props.label = 'play';\\n          props.icon = 'play';\\n          break;\\n        default:\\n          if (is.empty(props.label)) {\\n            props.label = type;\\n          }\\n          if (is.empty(props.icon)) {\\n            props.icon = buttonType;\\n          }\\n      }\\n      const button = createElement(props.element);\\n\\n      // Setup toggle icon and labels\\n      if (props.toggle) {\\n        // Icon\\n        button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed'\\n        }));\\n        button.appendChild(controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed'\\n        }));\\n\\n        // Label/Tooltip\\n        button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed'\\n        }));\\n        button.appendChild(controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed'\\n        }));\\n      } else {\\n        button.appendChild(controls.createIcon.call(this, props.icon));\\n        button.appendChild(controls.createLabel.call(this, props.label));\\n      }\\n\\n      // Merge and set attributes\\n      extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n      setAttributes(button, attributes);\\n\\n      // We have multiple play buttons\\n      if (type === 'play') {\\n        if (!is.array(this.elements.buttons[type])) {\\n          this.elements.buttons[type] = [];\\n        }\\n        this.elements.buttons[type].push(button);\\n      } else {\\n        this.elements.buttons[type] = button;\\n      }\\n      return button;\\n    },\\n    // Create an <input type='range'>\\n    createRange(type, attributes) {\\n      // Seek input\\n      const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n        type: 'range',\\n        min: 0,\\n        max: 100,\\n        step: 0.01,\\n        value: 0,\\n        autocomplete: 'off',\\n        // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n        role: 'slider',\\n        'aria-label': i18n.get(type, this.config),\\n        'aria-valuemin': 0,\\n        'aria-valuemax': 100,\\n        'aria-valuenow': 0\\n      }, attributes));\\n      this.elements.inputs[type] = input;\\n\\n      // Set the fill for webkit now\\n      controls.updateRangeFill.call(this, input);\\n\\n      // Improve support on touch devices\\n      RangeTouch.setup(input);\\n      return input;\\n    },\\n    // Create a <progress>\\n    createProgress(type, attributes) {\\n      const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n        min: 0,\\n        max: 100,\\n        value: 0,\\n        role: 'progressbar',\\n        'aria-hidden': true\\n      }, attributes));\\n\\n      // Create the label inside\\n      if (type !== 'volume') {\\n        progress.appendChild(createElement('span', null, '0'));\\n        const suffixKey = {\\n          played: 'played',\\n          buffer: 'buffered'\\n        }[type];\\n        const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n        progress.innerText = `% ${suffix.toLowerCase()}`;\\n      }\\n      this.elements.display[type] = progress;\\n      return progress;\\n    },\\n    // Create time display\\n    createTime(type, attrs) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n      const container = createElement('div', extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer'\\n      }), '00:00');\\n\\n      // Reference for updates\\n      this.elements.display[type] = container;\\n      return container;\\n    },\\n    // Bind keyboard shortcuts for a menu item\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    bindMenuItemShortcuts(menuItem, type) {\\n      // Navigate through menus via arrow keys and space\\n      on.call(this, menuItem, 'keydown keyup', event => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n              target = menuItem.nextElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      }, false);\\n\\n      // Enter will fire a `click` event but we still need to manage focus\\n      // So we bind to keyup which fires after and set focus here\\n      on.call(this, menuItem, 'keyup', event => {\\n        if (event.key !== 'Return') return;\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      });\\n    },\\n    // Create a settings menu item\\n    createMenuItem({\\n      value,\\n      list,\\n      type,\\n      title,\\n      badge = null,\\n      checked = false\\n    }) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n      const menuItem = createElement('button', extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value\\n      }));\\n      const flex = createElement('span');\\n\\n      // We have to set as HTML incase of special characters\\n      flex.innerHTML = title;\\n      if (is.element(badge)) {\\n        flex.appendChild(badge);\\n      }\\n      menuItem.appendChild(flex);\\n\\n      // Replicate radio button behavior\\n      Object.defineProperty(menuItem, 'checked', {\\n        enumerable: true,\\n        get() {\\n          return menuItem.getAttribute('aria-checked') === 'true';\\n        },\\n        set(check) {\\n          // Ensure exclusivity\\n          if (check) {\\n            Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n          }\\n          menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n        }\\n      });\\n      this.listeners.bind(menuItem, 'click keyup', event => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n        event.preventDefault();\\n        event.stopPropagation();\\n        menuItem.checked = true;\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n        }\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      }, type, false);\\n      controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n      list.appendChild(menuItem);\\n    },\\n    // Format a time for display\\n    formatTime(time = 0, inverted = false) {\\n      // Bail if the value isn't a number\\n      if (!is.number(time)) {\\n        return time;\\n      }\\n\\n      // Always display hours if duration is over an hour\\n      const forceHours = getHours(this.duration) > 0;\\n      return formatTime(time, forceHours, inverted);\\n    },\\n    // Update the displayed time\\n    updateTimeDisplay(target = null, time = 0, inverted = false) {\\n      // Bail if there's no element to display or the value isn't a number\\n      if (!is.element(target) || !is.number(time)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line no-param-reassign\\n      target.innerText = controls.formatTime(time, inverted);\\n    },\\n    // Update volume UI and storage\\n    updateVolume() {\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Update range\\n      if (is.element(this.elements.inputs.volume)) {\\n        controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n      }\\n\\n      // Update mute state\\n      if (is.element(this.elements.buttons.mute)) {\\n        this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n      }\\n    },\\n    // Update seek value and lower fill\\n    setRange(target, value = 0) {\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line\\n      target.value = value;\\n\\n      // Webkit range fill\\n      controls.updateRangeFill.call(this, target);\\n    },\\n    // Update <progress> elements\\n    updateProgress(event) {\\n      if (!this.supported.ui || !is.event(event)) {\\n        return;\\n      }\\n      let value = 0;\\n      const setProgress = (target, input) => {\\n        const val = is.number(input) ? input : 0;\\n        const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n        // Update value and label\\n        if (is.element(progress)) {\\n          progress.value = val;\\n\\n          // Update text label inside\\n          const label = progress.getElementsByTagName('span')[0];\\n          if (is.element(label)) {\\n            label.childNodes[0].nodeValue = val;\\n          }\\n        }\\n      };\\n      if (event) {\\n        switch (event.type) {\\n          // Video playing\\n          case 'timeupdate':\\n          case 'seeking':\\n          case 'seeked':\\n            value = getPercentage(this.currentTime, this.duration);\\n\\n            // Set seek range value only if it's a 'natural' time event\\n            if (event.type === 'timeupdate') {\\n              controls.setRange.call(this, this.elements.inputs.seek, value);\\n            }\\n            break;\\n\\n          // Check buffer status\\n          case 'playing':\\n          case 'progress':\\n            setProgress(this.elements.display.buffer, this.buffered * 100);\\n            break;\\n        }\\n      }\\n    },\\n    // Webkit polyfill for lower fill range\\n    updateRangeFill(target) {\\n      // Get range from event if event passed\\n      const range = is.event(target) ? target.target : target;\\n\\n      // Needs to be a valid <input type='range'>\\n      if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n        return;\\n      }\\n\\n      // Set aria values for https://github.com/sampotts/plyr/issues/905\\n      if (matches(range, this.config.selectors.inputs.seek)) {\\n        range.setAttribute('aria-valuenow', this.currentTime);\\n        const currentTime = controls.formatTime(this.currentTime);\\n        const duration = controls.formatTime(this.duration);\\n        const format = i18n.get('seekLabel', this.config);\\n        range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n      } else if (matches(range, this.config.selectors.inputs.volume)) {\\n        const percent = range.value * 100;\\n        range.setAttribute('aria-valuenow', percent);\\n        range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n      } else {\\n        range.setAttribute('aria-valuenow', range.value);\\n      }\\n\\n      // WebKit only\\n      if (!browser.isWebKit && !browser.isIPadOS) {\\n        return;\\n      }\\n\\n      // Set CSS custom property\\n      range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n    },\\n    // Update hover tooltip for seeking\\n    updateSeekTooltip(event) {\\n      var _this$config$markers, _this$config$markers$;\\n      // Bail if setting not true\\n      if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n        return;\\n      }\\n      const tipElement = this.elements.display.seekTooltip;\\n      const visible = `${this.config.classNames.tooltip}--visible`;\\n      const toggle = show => toggleClass(tipElement, visible, show);\\n\\n      // Hide on touch\\n      if (this.touch) {\\n        toggle(false);\\n        return;\\n      }\\n\\n      // Determine percentage, if already visible\\n      let percent = 0;\\n      const clientRect = this.elements.progress.getBoundingClientRect();\\n      if (is.event(event)) {\\n        percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n      } else if (hasClass(tipElement, visible)) {\\n        percent = parseFloat(tipElement.style.left, 10);\\n      } else {\\n        return;\\n      }\\n\\n      // Set bounds\\n      if (percent < 0) {\\n        percent = 0;\\n      } else if (percent > 100) {\\n        percent = 100;\\n      }\\n      const time = this.duration / 100 * percent;\\n\\n      // Display the time a click would seek to\\n      tipElement.innerText = controls.formatTime(time);\\n\\n      // Get marker point for time\\n      const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n        time: t\\n      }) => t === Math.round(time));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n\\n      // Set position\\n      tipElement.style.left = `${percent}%`;\\n\\n      // Show/hide the tooltip\\n      // If the event is a moues in/out and percentage is inside bounds\\n      if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n        toggle(event.type === 'mouseenter');\\n      }\\n    },\\n    // Handle time change event\\n    timeUpdate(event) {\\n      // Only invert if only one time element is displayed and used for both duration and currentTime\\n      const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n      // Duration\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n      // Ignore updates while seeking\\n      if (event && event.type === 'timeupdate' && this.media.seeking) {\\n        return;\\n      }\\n\\n      // Playing progress\\n      controls.updateProgress.call(this, event);\\n    },\\n    // Show the duration on metadataloaded or durationchange events\\n    durationUpdate() {\\n      // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n      if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n        return;\\n      }\\n\\n      // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n      // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n      // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n      // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n      if (this.duration >= 2 ** 32) {\\n        toggleHidden(this.elements.display.currentTime, true);\\n        toggleHidden(this.elements.progress, true);\\n        return;\\n      }\\n\\n      // Update ARIA values\\n      if (is.element(this.elements.inputs.seek)) {\\n        this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n      }\\n\\n      // If there's a spot to display duration\\n      const hasDuration = is.element(this.elements.display.duration);\\n\\n      // If there's only one time display, display duration there\\n      if (!hasDuration && this.config.displayDuration && this.paused) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n      }\\n\\n      // If there's a duration element, update content\\n      if (hasDuration) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n      }\\n      if (this.config.markers.enabled) {\\n        controls.setMarkers.call(this);\\n      }\\n\\n      // Update the tooltip (if visible)\\n      controls.updateSeekTooltip.call(this);\\n    },\\n    // Hide/show a tab\\n    toggleMenuButton(setting, toggle) {\\n      toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n    },\\n    // Update the selected setting\\n    updateSetting(setting, container, input) {\\n      const pane = this.elements.settings.panels[setting];\\n      let value = null;\\n      let list = container;\\n      if (setting === 'captions') {\\n        value = this.currentTrack;\\n      } else {\\n        value = !is.empty(input) ? input : this[setting];\\n\\n        // Get default\\n        if (is.empty(value)) {\\n          value = this.config[setting].default;\\n        }\\n\\n        // Unsupported value\\n        if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n          this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n          return;\\n        }\\n\\n        // Disabled value\\n        if (!this.config[setting].options.includes(value)) {\\n          this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n          return;\\n        }\\n      }\\n\\n      // Get the list if we need to\\n      if (!is.element(list)) {\\n        list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n      }\\n\\n      // If there's no list it means it's not been rendered...\\n      if (!is.element(list)) {\\n        return;\\n      }\\n\\n      // Update the label\\n      const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n      label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n      // Find the radio option and check it\\n      const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n      if (is.element(target)) {\\n        target.checked = true;\\n      }\\n    },\\n    // Translate a value into a nice label\\n    getLabel(setting, value) {\\n      switch (setting) {\\n        case 'speed':\\n          return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n        case 'quality':\\n          if (is.number(value)) {\\n            const label = i18n.get(`qualityLabel.${value}`, this.config);\\n            if (!label.length) {\\n              return `${value}p`;\\n            }\\n            return label;\\n          }\\n          return toTitleCase(value);\\n        case 'captions':\\n          return captions.getLabel.call(this);\\n        default:\\n          return null;\\n      }\\n    },\\n    // Set the quality menu\\n    setQualityMenu(options) {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.quality)) {\\n        return;\\n      }\\n      const type = 'quality';\\n      const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Set options if passed and filter based on uniqueness and config\\n      if (is.array(options)) {\\n        this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n      }\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Get the badge HTML for HD, 4K etc\\n      const getBadge = quality => {\\n        const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n        if (!label.length) {\\n          return null;\\n        }\\n        return controls.createBadge.call(this, label);\\n      };\\n\\n      // Sort options by the config and then render options\\n      this.options.quality.sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      }).forEach(quality => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set the looping options\\n    /* setLoopMenu() {\\n          // Menu required\\n          if (!is.element(this.elements.settings.panels.loop)) {\\n              return;\\n          }\\n           const options = ['start', 'end', 'all', 'reset'];\\n          const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n           // Show the pane and tab\\n          toggleHidden(this.elements.settings.buttons.loop, false);\\n          toggleHidden(this.elements.settings.panels.loop, false);\\n           // Toggle the pane and tab\\n          const toggle = !is.empty(this.loop.options);\\n          controls.toggleMenuButton.call(this, 'loop', toggle);\\n           // Empty the menu\\n          emptyElement(list);\\n           options.forEach(option => {\\n              const item = createElement('li');\\n               const button = createElement(\\n                  'button',\\n                  extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                      type: 'button',\\n                      class: this.config.classNames.control,\\n                      'data-plyr-loop-action': option,\\n                  }),\\n                  i18n.get(option, this.config)\\n              );\\n               if (['start', 'end'].includes(option)) {\\n                  const badge = controls.createBadge.call(this, '00:00');\\n                  button.appendChild(badge);\\n              }\\n               item.appendChild(button);\\n              list.appendChild(item);\\n          });\\n      }, */\\n\\n    // Get current selected caption language\\n    // TODO: rework this to user the getter in the API?\\n\\n    // Set a list of available captions languages\\n    setCaptionsMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.captions)) {\\n        return;\\n      }\\n\\n      // TODO: Captions or language? Currently it's mixed\\n      const type = 'captions';\\n      const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n      const tracks = captions.getTracks.call(this);\\n      const toggle = Boolean(tracks.length);\\n\\n      // Toggle the pane and tab\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If there's no captions, bail\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Generate options data\\n      const options = tracks.map((track, value) => ({\\n        value,\\n        checked: this.captions.toggled && this.currentTrack === value,\\n        title: captions.getLabel.call(this, track),\\n        badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n        list,\\n        type: 'language'\\n      }));\\n\\n      // Add the \\\"Disabled\\\" option to turn off captions\\n      options.unshift({\\n        value: -1,\\n        checked: !this.captions.toggled,\\n        title: i18n.get('disabled', this.config),\\n        list,\\n        type: 'language'\\n      });\\n\\n      // Generate options\\n      options.forEach(controls.createMenuItem.bind(this));\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set a list of available captions languages\\n    setSpeedMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.speed)) {\\n        return;\\n      }\\n      const type = 'speed';\\n      const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Filter out invalid speeds\\n      this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Create items\\n      this.options.speed.forEach(speed => {\\n        controls.createMenuItem.call(this, {\\n          value: speed,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'speed', speed)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Check if we need to hide/show the settings menu\\n    checkMenu() {\\n      const {\\n        buttons\\n      } = this.elements.settings;\\n      const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n      toggleHidden(this.elements.settings.menu, !visible);\\n    },\\n    // Focus the first menu item in a given (or visible) menu\\n    focusFirstMenuItem(pane, focusVisible = false) {\\n      if (this.elements.settings.popup.hidden) {\\n        return;\\n      }\\n      let target = pane;\\n      if (!is.element(target)) {\\n        target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n      }\\n      const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n      setFocus.call(this, firstItem, focusVisible);\\n    },\\n    // Show/hide menu\\n    toggleMenu(input) {\\n      const {\\n        popup\\n      } = this.elements.settings;\\n      const button = this.elements.buttons.settings;\\n\\n      // Menu and button are required\\n      if (!is.element(popup) || !is.element(button)) {\\n        return;\\n      }\\n\\n      // True toggle by default\\n      const {\\n        hidden\\n      } = popup;\\n      let show = hidden;\\n      if (is.boolean(input)) {\\n        show = input;\\n      } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n        show = false;\\n      } else if (is.event(input)) {\\n        // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n        // Element in the shadowDOM. The path, if available, is complete.\\n        const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n        const isMenuItem = popup.contains(target);\\n\\n        // If the click was inside the menu or if the click\\n        // wasn't the button or menu item and we're trying to\\n        // show the menu (a doc click shouldn't show the menu)\\n        if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n          return;\\n        }\\n      }\\n\\n      // Set button attributes\\n      button.setAttribute('aria-expanded', show);\\n\\n      // Show the actual popup\\n      toggleHidden(popup, !show);\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n      // Focus the first item if key interaction\\n      if (show && is.keyboardEvent(input)) {\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      } else if (!show && !hidden) {\\n        // If closing, re-focus the button\\n        setFocus.call(this, button, is.keyboardEvent(input));\\n      }\\n    },\\n    // Get the natural size of a menu panel\\n    getMenuSize(tab) {\\n      const clone = tab.cloneNode(true);\\n      clone.style.position = 'absolute';\\n      clone.style.opacity = 0;\\n      clone.removeAttribute('hidden');\\n\\n      // Append to parent so we get the \\\"real\\\" size\\n      tab.parentNode.appendChild(clone);\\n\\n      // Get the sizes before we remove\\n      const width = clone.scrollWidth;\\n      const height = clone.scrollHeight;\\n\\n      // Remove from the DOM\\n      removeElement(clone);\\n      return {\\n        width,\\n        height\\n      };\\n    },\\n    // Show a panel in the menu\\n    showMenuPanel(type = '', focusVisible = false) {\\n      const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n      // Nothing to show, bail\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // Hide all other panels\\n      const container = target.parentNode;\\n      const current = Array.from(container.children).find(node => !node.hidden);\\n\\n      // If we can do fancy animations, we'll animate the height/width\\n      if (support.transitions && !support.reducedMotion) {\\n        // Set the current width as a base\\n        container.style.width = `${current.scrollWidth}px`;\\n        container.style.height = `${current.scrollHeight}px`;\\n\\n        // Get potential sizes\\n        const size = controls.getMenuSize.call(this, target);\\n\\n        // Restore auto height/width\\n        const restore = event => {\\n          // We're only bothered about height and width on the container\\n          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n            return;\\n          }\\n\\n          // Revert back to auto\\n          container.style.width = '';\\n          container.style.height = '';\\n\\n          // Only listen once\\n          off.call(this, container, transitionEndEvent, restore);\\n        };\\n\\n        // Listen for the transition finishing and restore auto height/width\\n        on.call(this, container, transitionEndEvent, restore);\\n\\n        // Set dimensions to target\\n        container.style.width = `${size.width}px`;\\n        container.style.height = `${size.height}px`;\\n      }\\n\\n      // Set attributes on current tab\\n      toggleHidden(current, true);\\n\\n      // Set attributes on target\\n      toggleHidden(target, false);\\n\\n      // Focus the first item\\n      controls.focusFirstMenuItem.call(this, target, focusVisible);\\n    },\\n    // Set the download URL\\n    setDownloadUrl() {\\n      const button = this.elements.buttons.download;\\n\\n      // Bail if no button\\n      if (!is.element(button)) {\\n        return;\\n      }\\n\\n      // Set attribute\\n      button.setAttribute('href', this.download);\\n    },\\n    // Build the default HTML\\n    create(data) {\\n      const {\\n        bindMenuItemShortcuts,\\n        createButton,\\n        createProgress,\\n        createRange,\\n        createTime,\\n        setQualityMenu,\\n        setSpeedMenu,\\n        showMenuPanel\\n      } = controls;\\n      this.elements.controls = null;\\n\\n      // Larger overlaid play button\\n      if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n        this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n      }\\n\\n      // Create the container\\n      const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n      this.elements.controls = container;\\n\\n      // Default item attributes\\n      const defaultAttributes = {\\n        class: 'plyr__controls__item'\\n      };\\n\\n      // Loop through controls in order\\n      dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n        // Restart button\\n        if (control === 'restart') {\\n          container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n        }\\n\\n        // Rewind button\\n        if (control === 'rewind') {\\n          container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n        }\\n\\n        // Play/Pause button\\n        if (control === 'play') {\\n          container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n        }\\n\\n        // Fast forward button\\n        if (control === 'fast-forward') {\\n          container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n        }\\n\\n        // Progress\\n        if (control === 'progress') {\\n          const progressContainer = createElement('div', {\\n            class: `${defaultAttributes.class} plyr__progress__container`\\n          });\\n          const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n          // Seek range slider\\n          progress.appendChild(createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`\\n          }));\\n\\n          // Buffer progress\\n          progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n          // TODO: Add loop display indicator\\n\\n          // Seek tooltip\\n          if (this.config.tooltips.seek) {\\n            const tooltip = createElement('span', {\\n              class: this.config.classNames.tooltip\\n            }, '00:00');\\n            progress.appendChild(tooltip);\\n            this.elements.display.seekTooltip = tooltip;\\n          }\\n          this.elements.progress = progress;\\n          progressContainer.appendChild(this.elements.progress);\\n          container.appendChild(progressContainer);\\n        }\\n\\n        // Media current time display\\n        if (control === 'current-time') {\\n          container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n        }\\n\\n        // Media duration display\\n        if (control === 'duration') {\\n          container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n        }\\n\\n        // Volume controls\\n        if (control === 'mute' || control === 'volume') {\\n          let {\\n            volume\\n          } = this.elements;\\n\\n          // Create the volume container if needed\\n          if (!is.element(volume) || !container.contains(volume)) {\\n            volume = createElement('div', extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim()\\n            }));\\n            this.elements.volume = volume;\\n            container.appendChild(volume);\\n          }\\n\\n          // Toggle mute button\\n          if (control === 'mute') {\\n            volume.appendChild(createButton.call(this, 'mute'));\\n          }\\n\\n          // Volume range control\\n          // Ignored on iOS as it's handled globally\\n          // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n          if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n            // Set the attributes\\n            const attributes = {\\n              max: 1,\\n              step: 0.05,\\n              value: this.config.volume\\n            };\\n\\n            // Create the volume range slider\\n            volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n              id: `plyr-volume-${data.id}`\\n            })));\\n          }\\n        }\\n\\n        // Toggle captions button\\n        if (control === 'captions') {\\n          container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n        }\\n\\n        // Settings button / menu\\n        if (control === 'settings' && !is.empty(this.config.settings)) {\\n          const wrapper = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: ''\\n          }));\\n          wrapper.appendChild(createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false\\n          }));\\n          const popup = createElement('div', {\\n            class: 'plyr__menu__container',\\n            id: `plyr-settings-${data.id}`,\\n            hidden: ''\\n          });\\n          const inner = createElement('div');\\n          const home = createElement('div', {\\n            id: `plyr-settings-${data.id}-home`\\n          });\\n\\n          // Create the menu\\n          const menu = createElement('div', {\\n            role: 'menu'\\n          });\\n          home.appendChild(menu);\\n          inner.appendChild(home);\\n          this.elements.settings.panels.home = home;\\n\\n          // Build the menu items\\n          this.config.settings.forEach(type => {\\n            // TODO: bundle this with the createMenuItem helper and bindings\\n            const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: ''\\n            }));\\n\\n            // Bind menu shortcuts for keyboard users\\n            bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n            // Show menu on click\\n            on.call(this, menuItem, 'click', () => {\\n              showMenuPanel.call(this, type, false);\\n            });\\n            const flex = createElement('span', null, i18n.get(type, this.config));\\n            const value = createElement('span', {\\n              class: this.config.classNames.menu.value\\n            });\\n\\n            // Speed contains HTML entities\\n            value.innerHTML = data[type];\\n            flex.appendChild(value);\\n            menuItem.appendChild(flex);\\n            menu.appendChild(menuItem);\\n\\n            // Build the panes\\n            const pane = createElement('div', {\\n              id: `plyr-settings-${data.id}-${type}`,\\n              hidden: ''\\n            });\\n\\n            // Back button\\n            const backButton = createElement('button', {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n            });\\n\\n            // Visible label\\n            backButton.appendChild(createElement('span', {\\n              'aria-hidden': true\\n            }, i18n.get(type, this.config)));\\n\\n            // Screen reader label\\n            backButton.appendChild(createElement('span', {\\n              class: this.config.classNames.hidden\\n            }, i18n.get('menuBack', this.config)));\\n\\n            // Go back via keyboard\\n            on.call(this, pane, 'keydown', event => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            }, false);\\n\\n            // Go back via button click\\n            on.call(this, backButton, 'click', () => {\\n              showMenuPanel.call(this, 'home', false);\\n            });\\n\\n            // Add to pane\\n            pane.appendChild(backButton);\\n\\n            // Menu\\n            pane.appendChild(createElement('div', {\\n              role: 'menu'\\n            }));\\n            inner.appendChild(pane);\\n            this.elements.settings.buttons[type] = menuItem;\\n            this.elements.settings.panels[type] = pane;\\n          });\\n          popup.appendChild(inner);\\n          wrapper.appendChild(popup);\\n          container.appendChild(wrapper);\\n          this.elements.settings.popup = popup;\\n          this.elements.settings.menu = wrapper;\\n        }\\n\\n        // Picture in picture button\\n        if (control === 'pip' && support.pip) {\\n          container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n        }\\n\\n        // Airplay button\\n        if (control === 'airplay' && support.airplay) {\\n          container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n        }\\n\\n        // Download button\\n        if (control === 'download') {\\n          const attributes = extend({}, defaultAttributes, {\\n            element: 'a',\\n            href: this.download,\\n            target: '_blank'\\n          });\\n\\n          // Set download attribute for HTML5 only\\n          if (this.isHTML5) {\\n            attributes.download = '';\\n          }\\n          const {\\n            download\\n          } = this.config.urls;\\n          if (!is.url(download) && this.isEmbed) {\\n            extend(attributes, {\\n              icon: `logo-${this.provider}`,\\n              label: this.provider\\n            });\\n          }\\n          container.appendChild(createButton.call(this, 'download', attributes));\\n        }\\n\\n        // Toggle fullscreen button\\n        if (control === 'fullscreen') {\\n          container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n        }\\n      });\\n\\n      // Set available quality levels\\n      if (this.isHTML5) {\\n        setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n      }\\n      setSpeedMenu.call(this);\\n      return container;\\n    },\\n    // Insert controls\\n    inject() {\\n      // Sprite\\n      if (this.config.loadSprite) {\\n        const icon = controls.getIconUrl.call(this);\\n\\n        // Only load external sprite using AJAX\\n        if (icon.cors) {\\n          loadSprite(icon.url, 'sprite-plyr');\\n        }\\n      }\\n\\n      // Create a unique ID\\n      this.id = Math.floor(Math.random() * 10000);\\n\\n      // Null by default\\n      let container = null;\\n      this.elements.controls = null;\\n\\n      // Set template properties\\n      const props = {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        title: this.config.title\\n      };\\n      let update = true;\\n\\n      // If function, run it and use output\\n      if (is.function(this.config.controls)) {\\n        this.config.controls = this.config.controls.call(this, props);\\n      }\\n\\n      // Convert falsy controls to empty array (primarily for empty strings)\\n      if (!this.config.controls) {\\n        this.config.controls = [];\\n      }\\n      if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n        // HTMLElement or Non-empty string passed as the option\\n        container = this.config.controls;\\n      } else {\\n        // Create controls\\n        container = controls.create.call(this, {\\n          id: this.id,\\n          seektime: this.config.seekTime,\\n          speed: this.speed,\\n          quality: this.quality,\\n          captions: captions.getLabel.call(this)\\n          // TODO: Looping\\n          // loop: 'None',\\n        });\\n\\n        update = false;\\n      }\\n\\n      // Replace props with their value\\n      const replace = input => {\\n        let result = input;\\n        Object.entries(props).forEach(([key, value]) => {\\n          result = replaceAll(result, `{${key}}`, value);\\n        });\\n        return result;\\n      };\\n\\n      // Update markup\\n      if (update) {\\n        if (is.string(this.config.controls)) {\\n          container = replace(container);\\n        }\\n      }\\n\\n      // Controls container\\n      let target;\\n\\n      // Inject to custom location\\n      if (is.string(this.config.selectors.controls.container)) {\\n        target = document.querySelector(this.config.selectors.controls.container);\\n      }\\n\\n      // Inject into the container by default\\n      if (!is.element(target)) {\\n        target = this.elements.container;\\n      }\\n\\n      // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n      const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n      target[insertMethod]('afterbegin', container);\\n\\n      // Find the elements if need be\\n      if (!is.element(this.elements.controls)) {\\n        controls.findElements.call(this);\\n      }\\n\\n      // Add pressed property to buttons\\n      if (!is.empty(this.elements.buttons)) {\\n        const addProperty = button => {\\n          const className = this.config.classNames.controlPressed;\\n          button.setAttribute('aria-pressed', 'false');\\n          Object.defineProperty(button, 'pressed', {\\n            configurable: true,\\n            enumerable: true,\\n            get() {\\n              return hasClass(button, className);\\n            },\\n            set(pressed = false) {\\n              toggleClass(button, className, pressed);\\n              button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n            }\\n          });\\n        };\\n\\n        // Toggle classname when pressed property is set\\n        Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n      }\\n\\n      // Edge sometimes doesn't finish the paint so force a repaint\\n      if (browser.isEdge) {\\n        repaint(target);\\n      }\\n\\n      // Setup tooltips\\n      if (this.config.tooltips.controls) {\\n        const {\\n          classNames,\\n          selectors\\n        } = this.config;\\n        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n        const labels = getElements.call(this, selector);\\n        Array.from(labels).forEach(label => {\\n          toggleClass(label, this.config.classNames.hidden, false);\\n          toggleClass(label, this.config.classNames.tooltip, true);\\n        });\\n      }\\n    },\\n    // Set media metadata\\n    setMediaMetadata() {\\n      try {\\n        if ('mediaSession' in navigator) {\\n          navigator.mediaSession.metadata = new window.MediaMetadata({\\n            title: this.config.mediaMetadata.title,\\n            artist: this.config.mediaMetadata.artist,\\n            album: this.config.mediaMetadata.album,\\n            artwork: this.config.mediaMetadata.artwork\\n          });\\n        }\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    },\\n    // Add markers\\n    setMarkers() {\\n      var _this$config$markers2, _this$config$markers3;\\n      if (!this.duration || this.elements.markers) return;\\n\\n      // Get valid points\\n      const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n        time\\n      }) => time > 0 && time < this.duration);\\n      if (!(points !== null && points !== void 0 && points.length)) return;\\n      const containerFragment = document.createDocumentFragment();\\n      const pointsFragment = document.createDocumentFragment();\\n      let tipElement = null;\\n      const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n      const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n      // Inject markers to progress container\\n      points.forEach(point => {\\n        const markerElement = createElement('span', {\\n          class: this.config.classNames.marker\\n        }, '');\\n        const left = `${point.time / this.duration * 100}%`;\\n        if (tipElement) {\\n          // Show on hover\\n          markerElement.addEventListener('mouseenter', () => {\\n            if (point.label) return;\\n            tipElement.style.left = left;\\n            tipElement.innerHTML = point.label;\\n            toggleTip(true);\\n          });\\n\\n          // Hide on leave\\n          markerElement.addEventListener('mouseleave', () => {\\n            toggleTip(false);\\n          });\\n        }\\n        markerElement.addEventListener('click', () => {\\n          this.currentTime = point.time;\\n        });\\n        markerElement.style.left = left;\\n        pointsFragment.appendChild(markerElement);\\n      });\\n      containerFragment.appendChild(pointsFragment);\\n\\n      // Inject a tooltip if needed\\n      if (!this.config.tooltips.seek) {\\n        tipElement = createElement('span', {\\n          class: this.config.classNames.tooltip\\n        }, '');\\n        containerFragment.appendChild(tipElement);\\n      }\\n      this.elements.markers = {\\n        points: pointsFragment,\\n        tip: tipElement\\n      };\\n      this.elements.progress.appendChild(containerFragment);\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  /**\\n   * Parse a string to a URL object\\n   * @param {String} input - the URL to be parsed\\n   * @param {Boolean} safe - failsafe parsing\\n   */\\n  function parseUrl(input, safe = true) {\\n    let url = input;\\n    if (safe) {\\n      const parser = document.createElement('a');\\n      parser.href = url;\\n      url = parser.href;\\n    }\\n    try {\\n      return new URL(url);\\n    } catch (_) {\\n      return null;\\n    }\\n  }\\n\\n  // Convert object to URLSearchParams\\n  function buildUrlParams(input) {\\n    const params = new URLSearchParams();\\n    if (is.object(input)) {\\n      Object.entries(input).forEach(([key, value]) => {\\n        params.set(key, value);\\n      });\\n    }\\n    return params;\\n  }\\n\\n  // ==========================================================================\\n  const captions = {\\n    // Setup captions\\n    setup() {\\n      // Requires UI support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Only Vimeo and HTML5 video supported at this point\\n      if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n        // Clear menu and hide\\n        if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n          controls.setCaptionsMenu.call(this);\\n        }\\n        return;\\n      }\\n\\n      // Inject the container\\n      if (!is.element(this.elements.captions)) {\\n        this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n        this.elements.captions.setAttribute('dir', 'auto');\\n        insertAfter(this.elements.captions, this.elements.wrapper);\\n      }\\n\\n      // Fix IE captions if CORS is used\\n      // Fetch captions and inject as blobs instead (data URIs not supported!)\\n      if (browser.isIE && window.URL) {\\n        const elements = this.media.querySelectorAll('track');\\n        Array.from(elements).forEach(track => {\\n          const src = track.getAttribute('src');\\n          const url = parseUrl(src);\\n          if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n            fetch(src, 'blob').then(blob => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            }).catch(() => {\\n              removeElement(track);\\n            });\\n          }\\n        });\\n      }\\n\\n      // Get and set initial data\\n      // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n      // * languages: Array of user's browser languages.\\n      // * language:  The language preferred by user settings or config\\n      // * active:    The state preferred by user settings or config\\n      // * toggled:   The real captions state\\n\\n      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n      const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n      let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n      // Use first browser language when language is 'auto'\\n      if (language === 'auto') {\\n        [language] = languages;\\n      }\\n      let active = this.storage.get('captions');\\n      if (!is.boolean(active)) {\\n        ({\\n          active\\n        } = this.config.captions);\\n      }\\n      Object.assign(this.captions, {\\n        toggled: false,\\n        active,\\n        language,\\n        languages\\n      });\\n\\n      // Watch changes to textTracks and update captions menu\\n      if (this.isHTML5) {\\n        const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n        on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n      }\\n\\n      // Update available languages in list next tick (the event must not be triggered before the listeners)\\n      setTimeout(captions.update.bind(this), 0);\\n    },\\n    // Update available language options in settings based on tracks\\n    update() {\\n      const tracks = captions.getTracks.call(this, true);\\n      // Get the wanted language\\n      const {\\n        active,\\n        language,\\n        meta,\\n        currentTrackNode\\n      } = this.captions;\\n      const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n      // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n      if (this.isHTML5 && this.isVideo) {\\n        tracks.filter(track => !meta.get(track)).forEach(track => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing'\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n      }\\n\\n      // Update language first time it matches, or if the previous matching track was removed\\n      if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n        captions.setLanguage.call(this, language);\\n        captions.toggle.call(this, active && languageExists);\\n      }\\n\\n      // Enable or disable captions based on track length\\n      if (this.elements) {\\n        toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n      }\\n\\n      // Update available languages in list\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n    },\\n    // Toggle captions display\\n    // Used internally for the toggleCaptions method, with the passive option forced to false\\n    toggle(input, passive = true) {\\n      // If there's no full support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      const {\\n        toggled\\n      } = this.captions; // Current state\\n      const activeClass = this.config.classNames.captions.active;\\n      // Get the next state\\n      // If the method is called without parameter, toggle based on current value\\n      const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n      // Update state and trigger event\\n      if (active !== toggled) {\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.active = active;\\n          this.storage.set({\\n            captions: active\\n          });\\n        }\\n\\n        // Force language if the call isn't passive and there is no matching language to toggle to\\n        if (!this.language && active && !passive) {\\n          const tracks = captions.getTracks.call(this);\\n          const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n          // Override user preferences to avoid switching languages if a matching track is added\\n          this.captions.language = track.language;\\n\\n          // Set caption, but don't store in localStorage as user preference\\n          captions.set.call(this, tracks.indexOf(track));\\n          return;\\n        }\\n\\n        // Toggle button if it's enabled\\n        if (this.elements.buttons.captions) {\\n          this.elements.buttons.captions.pressed = active;\\n        }\\n\\n        // Add class hook\\n        toggleClass(this.elements.container, activeClass, active);\\n        this.captions.toggled = active;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // Trigger event (not used internally)\\n        triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n      }\\n\\n      // Wait for the call stack to clear before setting mode='hidden'\\n      // on the active track - forcing the browser to download it\\n      setTimeout(() => {\\n        if (active && this.captions.toggled) {\\n          this.captions.currentTrackNode.mode = 'hidden';\\n        }\\n      });\\n    },\\n    // Set captions by track index\\n    // Used internally for the currentTrack setter with the passive option forced to false\\n    set(index, passive = true) {\\n      const tracks = captions.getTracks.call(this);\\n\\n      // Disable captions if setting to -1\\n      if (index === -1) {\\n        captions.toggle.call(this, false, passive);\\n        return;\\n      }\\n      if (!is.number(index)) {\\n        this.debug.warn('Invalid caption argument', index);\\n        return;\\n      }\\n      if (!(index in tracks)) {\\n        this.debug.warn('Track not found', index);\\n        return;\\n      }\\n      if (this.captions.currentTrack !== index) {\\n        this.captions.currentTrack = index;\\n        const track = tracks[index];\\n        const {\\n          language\\n        } = track || {};\\n\\n        // Store reference to node for invalidation on remove\\n        this.captions.currentTrackNode = track;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.language = language;\\n          this.storage.set({\\n            language\\n          });\\n        }\\n\\n        // Handle Vimeo captions\\n        if (this.isVimeo) {\\n          this.embed.enableTextTrack(language);\\n        }\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'languagechange');\\n      }\\n\\n      // Show captions\\n      captions.toggle.call(this, true, passive);\\n      if (this.isHTML5 && this.isVideo) {\\n        // If we change the active track while a cue is already displayed we need to update it\\n        captions.updateCues.call(this);\\n      }\\n    },\\n    // Set captions by language\\n    // Used internally for the language setter with the passive option forced to false\\n    setLanguage(input, passive = true) {\\n      if (!is.string(input)) {\\n        this.debug.warn('Invalid language argument', input);\\n        return;\\n      }\\n      // Normalize\\n      const language = input.toLowerCase();\\n      this.captions.language = language;\\n\\n      // Set currentTrack\\n      const tracks = captions.getTracks.call(this);\\n      const track = captions.findTrack.call(this, [language]);\\n      captions.set.call(this, tracks.indexOf(track), passive);\\n    },\\n    // Get current valid caption tracks\\n    // If update is false it will also ignore tracks without metadata\\n    // This is used to \\\"freeze\\\" the language options when captions.update is false\\n    getTracks(update = false) {\\n      // Handle media or textTracks missing or null\\n      const tracks = Array.from((this.media || {}).textTracks || []);\\n      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n      return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n    },\\n    // Match tracks based on languages and get the first\\n    findTrack(languages, force = false) {\\n      const tracks = captions.getTracks.call(this);\\n      const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n      let track;\\n      languages.every(language => {\\n        track = sorted.find(t => t.language === language);\\n        return !track; // Break iteration if there is a match\\n      });\\n\\n      // If no match is found but is required, get first\\n      return track || (force ? sorted[0] : undefined);\\n    },\\n    // Get the current track\\n    getCurrentTrack() {\\n      return captions.getTracks.call(this)[this.currentTrack];\\n    },\\n    // Get UI label for track\\n    getLabel(track) {\\n      let currentTrack = track;\\n      if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n        currentTrack = captions.getCurrentTrack.call(this);\\n      }\\n      if (is.track(currentTrack)) {\\n        if (!is.empty(currentTrack.label)) {\\n          return currentTrack.label;\\n        }\\n        if (!is.empty(currentTrack.language)) {\\n          return track.language.toUpperCase();\\n        }\\n        return i18n.get('enabled', this.config);\\n      }\\n      return i18n.get('disabled', this.config);\\n    },\\n    // Update captions using current track's active cues\\n    // Also optional array argument in case there isn't any track (ex: vimeo)\\n    updateCues(input) {\\n      // Requires UI\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      if (!is.element(this.elements.captions)) {\\n        this.debug.warn('No captions element to render to');\\n        return;\\n      }\\n\\n      // Only accept array or empty input\\n      if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n        this.debug.warn('updateCues: Invalid input', input);\\n        return;\\n      }\\n      let cues = input;\\n\\n      // Get cues from track\\n      if (!cues) {\\n        const track = captions.getCurrentTrack.call(this);\\n        cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n      }\\n\\n      // Set new caption text\\n      const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n      const changed = content !== this.elements.captions.innerHTML;\\n      if (changed) {\\n        // Empty the container and create a new child element\\n        emptyElement(this.elements.captions);\\n        const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n        caption.innerHTML = content;\\n        this.elements.captions.appendChild(caption);\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'cuechange');\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr default config\\n  // ==========================================================================\\n\\n  const defaults = {\\n    // Disable\\n    enabled: true,\\n    // Custom media title\\n    title: '',\\n    // Logging to console\\n    debug: false,\\n    // Auto play (if supported)\\n    autoplay: false,\\n    // Only allow one media playing at once (vimeo only)\\n    autopause: true,\\n    // Allow inline playback on iOS\\n    playsinline: true,\\n    // Default time to skip when rewind/fast forward\\n    seekTime: 10,\\n    // Default volume\\n    volume: 1,\\n    muted: false,\\n    // Pass a custom duration\\n    duration: null,\\n    // Display the media duration on load in the current time position\\n    // If you have opted to display both duration and currentTime, this is ignored\\n    displayDuration: true,\\n    // Invert the current time to be a countdown\\n    invertTime: true,\\n    // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n    toggleInvert: true,\\n    // Force an aspect ratio\\n    // The format must be `'w:h'` (e.g. `'16:9'`)\\n    ratio: null,\\n    // Click video container to play/pause\\n    clickToPlay: true,\\n    // Auto hide the controls\\n    hideControls: true,\\n    // Reset to start when playback ended\\n    resetOnEnd: false,\\n    // Disable the standard context menu\\n    disableContextMenu: true,\\n    // Sprite (for icons)\\n    loadSprite: true,\\n    iconPrefix: 'plyr',\\n    iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n    // Blank video (used to prevent errors on source change)\\n    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n    // Quality default\\n    quality: {\\n      default: 576,\\n      // The options to display in the UI, if available for the source media\\n      options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n      forced: false,\\n      onChange: null\\n    },\\n    // Set loops\\n    loop: {\\n      active: false\\n      // start: null,\\n      // end: null,\\n    },\\n\\n    // Speed default and options to display\\n    speed: {\\n      selected: 1,\\n      // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n      options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n    },\\n    // Keyboard shortcut settings\\n    keyboard: {\\n      focused: true,\\n      global: false\\n    },\\n    // Display tooltips\\n    tooltips: {\\n      controls: false,\\n      seek: true\\n    },\\n    // Captions settings\\n    captions: {\\n      active: false,\\n      language: 'auto',\\n      // Listen to new tracks added after Plyr is initialized.\\n      // This is needed for streaming captions, but may result in unselectable options\\n      update: false\\n    },\\n    // Fullscreen settings\\n    fullscreen: {\\n      enabled: true,\\n      // Allow fullscreen?\\n      fallback: true,\\n      // Fallback using full viewport/window\\n      iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n      // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n      // Non-ancestors of the player element will be ignored\\n      // container: null, // defaults to the player element\\n    },\\n\\n    // Local storage\\n    storage: {\\n      enabled: true,\\n      key: 'plyr'\\n    },\\n    // Default controls\\n    controls: ['play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress', 'current-time',\\n    // 'duration',\\n    'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n    // 'download',\\n    'fullscreen'],\\n    settings: ['captions', 'quality', 'speed'],\\n    // Localisation\\n    i18n: {\\n      restart: 'Restart',\\n      rewind: 'Rewind {seektime}s',\\n      play: 'Play',\\n      pause: 'Pause',\\n      fastForward: 'Forward {seektime}s',\\n      seek: 'Seek',\\n      seekLabel: '{currentTime} of {duration}',\\n      played: 'Played',\\n      buffered: 'Buffered',\\n      currentTime: 'Current time',\\n      duration: 'Duration',\\n      volume: 'Volume',\\n      mute: 'Mute',\\n      unmute: 'Unmute',\\n      enableCaptions: 'Enable captions',\\n      disableCaptions: 'Disable captions',\\n      download: 'Download',\\n      enterFullscreen: 'Enter fullscreen',\\n      exitFullscreen: 'Exit fullscreen',\\n      frameTitle: 'Player for {title}',\\n      captions: 'Captions',\\n      settings: 'Settings',\\n      pip: 'PIP',\\n      menuBack: 'Go back to previous menu',\\n      speed: 'Speed',\\n      normal: 'Normal',\\n      quality: 'Quality',\\n      loop: 'Loop',\\n      start: 'Start',\\n      end: 'End',\\n      all: 'All',\\n      reset: 'Reset',\\n      disabled: 'Disabled',\\n      enabled: 'Enabled',\\n      advertisement: 'Ad',\\n      qualityBadge: {\\n        2160: '4K',\\n        1440: 'HD',\\n        1080: 'HD',\\n        720: 'HD',\\n        576: 'SD',\\n        480: 'SD'\\n      }\\n    },\\n    // URLs\\n    urls: {\\n      download: null,\\n      vimeo: {\\n        sdk: 'https://player.vimeo.com/api/player.js',\\n        iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n        api: 'https://vimeo.com/api/oembed.json?url={0}'\\n      },\\n      youtube: {\\n        sdk: 'https://www.youtube.com/iframe_api',\\n        api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\\n      },\\n      googleIMA: {\\n        sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\\n      }\\n    },\\n    // Custom control listeners\\n    listeners: {\\n      seek: null,\\n      play: null,\\n      pause: null,\\n      restart: null,\\n      rewind: null,\\n      fastForward: null,\\n      mute: null,\\n      volume: null,\\n      captions: null,\\n      download: null,\\n      fullscreen: null,\\n      pip: null,\\n      airplay: null,\\n      speed: null,\\n      quality: null,\\n      loop: null,\\n      language: null\\n    },\\n    // Events to watch and bubble\\n    events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n    // Custom events\\n    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n    // YouTube\\n    'statechange',\\n    // Quality\\n    'qualitychange',\\n    // Ads\\n    'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n    // Selectors\\n    // Change these to match your template if using custom HTML\\n    selectors: {\\n      editable: 'input, textarea, select, [contenteditable]',\\n      container: '.plyr',\\n      controls: {\\n        container: null,\\n        wrapper: '.plyr__controls'\\n      },\\n      labels: '[data-plyr]',\\n      buttons: {\\n        play: '[data-plyr=\\\"play\\\"]',\\n        pause: '[data-plyr=\\\"pause\\\"]',\\n        restart: '[data-plyr=\\\"restart\\\"]',\\n        rewind: '[data-plyr=\\\"rewind\\\"]',\\n        fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n        mute: '[data-plyr=\\\"mute\\\"]',\\n        captions: '[data-plyr=\\\"captions\\\"]',\\n        download: '[data-plyr=\\\"download\\\"]',\\n        fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n        pip: '[data-plyr=\\\"pip\\\"]',\\n        airplay: '[data-plyr=\\\"airplay\\\"]',\\n        settings: '[data-plyr=\\\"settings\\\"]',\\n        loop: '[data-plyr=\\\"loop\\\"]'\\n      },\\n      inputs: {\\n        seek: '[data-plyr=\\\"seek\\\"]',\\n        volume: '[data-plyr=\\\"volume\\\"]',\\n        speed: '[data-plyr=\\\"speed\\\"]',\\n        language: '[data-plyr=\\\"language\\\"]',\\n        quality: '[data-plyr=\\\"quality\\\"]'\\n      },\\n      display: {\\n        currentTime: '.plyr__time--current',\\n        duration: '.plyr__time--duration',\\n        buffer: '.plyr__progress__buffer',\\n        loop: '.plyr__progress__loop',\\n        // Used later\\n        volume: '.plyr__volume--display'\\n      },\\n      progress: '.plyr__progress',\\n      captions: '.plyr__captions',\\n      caption: '.plyr__caption'\\n    },\\n    // Class hooks added to the player in different states\\n    classNames: {\\n      type: 'plyr--{0}',\\n      provider: 'plyr--{0}',\\n      video: 'plyr__video-wrapper',\\n      embed: 'plyr__video-embed',\\n      videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n      embedContainer: 'plyr__video-embed__container',\\n      poster: 'plyr__poster',\\n      posterEnabled: 'plyr__poster-enabled',\\n      ads: 'plyr__ads',\\n      control: 'plyr__control',\\n      controlPressed: 'plyr__control--pressed',\\n      playing: 'plyr--playing',\\n      paused: 'plyr--paused',\\n      stopped: 'plyr--stopped',\\n      loading: 'plyr--loading',\\n      hover: 'plyr--hover',\\n      tooltip: 'plyr__tooltip',\\n      cues: 'plyr__cues',\\n      marker: 'plyr__progress__marker',\\n      hidden: 'plyr__sr-only',\\n      hideControls: 'plyr--hide-controls',\\n      isTouch: 'plyr--is-touch',\\n      uiSupported: 'plyr--full-ui',\\n      noTransition: 'plyr--no-transition',\\n      display: {\\n        time: 'plyr__time'\\n      },\\n      menu: {\\n        value: 'plyr__menu__value',\\n        badge: 'plyr__badge',\\n        open: 'plyr--menu-open'\\n      },\\n      captions: {\\n        enabled: 'plyr--captions-enabled',\\n        active: 'plyr--captions-active'\\n      },\\n      fullscreen: {\\n        enabled: 'plyr--fullscreen-enabled',\\n        fallback: 'plyr--fullscreen-fallback'\\n      },\\n      pip: {\\n        supported: 'plyr--pip-supported',\\n        active: 'plyr--pip-active'\\n      },\\n      airplay: {\\n        supported: 'plyr--airplay-supported',\\n        active: 'plyr--airplay-active'\\n      },\\n      previewThumbnails: {\\n        // Tooltip thumbs\\n        thumbContainer: 'plyr__preview-thumb',\\n        thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n        imageContainer: 'plyr__preview-thumb__image-container',\\n        timeContainer: 'plyr__preview-thumb__time-container',\\n        // Scrubbing\\n        scrubbingContainer: 'plyr__preview-scrubbing',\\n        scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n      }\\n    },\\n    // Embed attributes\\n    attributes: {\\n      embed: {\\n        provider: 'data-plyr-provider',\\n        id: 'data-plyr-embed-id',\\n        hash: 'data-plyr-embed-hash'\\n      }\\n    },\\n    // Advertisements plugin\\n    // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n    ads: {\\n      enabled: false,\\n      publisherId: '',\\n      tagUrl: ''\\n    },\\n    // Preview Thumbnails plugin\\n    previewThumbnails: {\\n      enabled: false,\\n      src: ''\\n    },\\n    // Vimeo plugin\\n    vimeo: {\\n      byline: false,\\n      portrait: false,\\n      title: false,\\n      speed: true,\\n      transparent: false,\\n      // Custom settings from Plyr\\n      customControls: true,\\n      referrerPolicy: null,\\n      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n      // Whether the owner of the video has a Pro or Business account\\n      // (which allows us to properly hide controls without CSS hacks, etc)\\n      premium: false\\n    },\\n    // YouTube plugin\\n    youtube: {\\n      rel: 0,\\n      // No related vids\\n      showinfo: 0,\\n      // Hide info\\n      iv_load_policy: 3,\\n      // Hide annotations\\n      modestbranding: 1,\\n      // Hide logos as much as possible (they still show one in the corner when paused)\\n      // Custom settings from Plyr\\n      customControls: true,\\n      noCookie: false // Whether to use an alternative version of YouTube without cookies\\n    },\\n\\n    // Media Metadata\\n    mediaMetadata: {\\n      title: '',\\n      artist: '',\\n      album: '',\\n      artwork: []\\n    },\\n    // Markers\\n    markers: {\\n      enabled: false,\\n      points: []\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr states\\n  // ==========================================================================\\n\\n  const pip = {\\n    active: 'picture-in-picture',\\n    inactive: 'inline'\\n  };\\n\\n  // ==========================================================================\\n  // Plyr supported types and providers\\n  // ==========================================================================\\n\\n  const providers = {\\n    html5: 'html5',\\n    youtube: 'youtube',\\n    vimeo: 'vimeo'\\n  };\\n  const types = {\\n    audio: 'audio',\\n    video: 'video'\\n  };\\n\\n  /**\\n   * Get provider by URL\\n   * @param {String} url\\n   */\\n  function getProviderByUrl(url) {\\n    // YouTube\\n    if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n      return providers.youtube;\\n    }\\n\\n    // Vimeo\\n    if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n      return providers.vimeo;\\n    }\\n    return null;\\n  }\\n\\n  // ==========================================================================\\n  // Console wrapper\\n  // ==========================================================================\\n\\n  const noop = () => {};\\n  class Console {\\n    constructor(enabled = false) {\\n      this.enabled = window.console && enabled;\\n      if (this.enabled) {\\n        this.log('Debugging enabled');\\n      }\\n    }\\n    get log() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n    }\\n    get warn() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n    }\\n    get error() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n    }\\n  }\\n\\n  class Fullscreen {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"onChange\\\", () => {\\n        if (!this.supported) return;\\n\\n        // Update toggle button\\n        const button = this.player.elements.buttons.fullscreen;\\n        if (is.element(button)) {\\n          button.pressed = this.active;\\n        }\\n\\n        // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n        const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n        // Trigger an event\\n        triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n      });\\n      _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n        // Store or restore scroll position\\n        if (toggle) {\\n          this.scrollPosition = {\\n            x: window.scrollX ?? 0,\\n            y: window.scrollY ?? 0\\n          };\\n        } else {\\n          window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n        }\\n\\n        // Toggle scroll\\n        document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n        // Toggle class hook\\n        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n        // Force full viewport on iPhone X+\\n        if (browser.isIos) {\\n          let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n          const property = 'viewport-fit=cover';\\n\\n          // Inject the viewport meta if required\\n          if (!viewport) {\\n            viewport = document.createElement('meta');\\n            viewport.setAttribute('name', 'viewport');\\n          }\\n\\n          // Check if the property already exists\\n          const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n          if (toggle) {\\n            this.cleanupViewport = !hasProperty;\\n            if (!hasProperty) viewport.content += `,${property}`;\\n          } else if (this.cleanupViewport) {\\n            viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n          }\\n        }\\n\\n        // Toggle button and fire events\\n        this.onChange();\\n      });\\n      // Trap focus inside container\\n      _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n        // Bail if iOS/iPadOS, not active, not the tab key\\n        if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n        // Get the current focused element\\n        const focused = document.activeElement;\\n        const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n        const [first] = focusable;\\n        const last = focusable[focusable.length - 1];\\n        if (focused === last && !event.shiftKey) {\\n          // Move focus to first element that can be tabbed if Shift isn't used\\n          first.focus();\\n          event.preventDefault();\\n        } else if (focused === first && event.shiftKey) {\\n          // Move focus to last element that can be tabbed if Shift is used\\n          last.focus();\\n          event.preventDefault();\\n        }\\n      });\\n      // Update UI\\n      _defineProperty$1(this, \\\"update\\\", () => {\\n        if (this.supported) {\\n          let mode;\\n          if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n          this.player.debug.log(`${mode} fullscreen enabled`);\\n        } else {\\n          this.player.debug.log('Fullscreen not supported and fallback disabled');\\n        }\\n\\n        // Add styling hook to show button\\n        toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n      });\\n      // Make an element fullscreen\\n      _defineProperty$1(this, \\\"enter\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen doesn't need the request step\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.requestFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(true);\\n        } else if (!this.prefix) {\\n          this.target.requestFullscreen({\\n            navigationUI: 'hide'\\n          });\\n        } else if (!is.empty(this.prefix)) {\\n          this.target[`${this.prefix}Request${this.property}`]();\\n        }\\n      });\\n      // Bail from fullscreen\\n      _defineProperty$1(this, \\\"exit\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.exitFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n          silencePromise(this.player.play());\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(false);\\n        } else if (!this.prefix) {\\n          (document.cancelFullScreen || document.exitFullscreen).call(document);\\n        } else if (!is.empty(this.prefix)) {\\n          const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n          document[`${this.prefix}${action}${this.property}`]();\\n        }\\n      });\\n      // Toggle state\\n      _defineProperty$1(this, \\\"toggle\\\", () => {\\n        if (!this.active) this.enter();else this.exit();\\n      });\\n      // Keep reference to parent\\n      this.player = player;\\n\\n      // Get prefix\\n      this.prefix = Fullscreen.prefix;\\n      this.property = Fullscreen.property;\\n\\n      // Scroll position\\n      this.scrollPosition = {\\n        x: 0,\\n        y: 0\\n      };\\n\\n      // Force the use of 'full window/browser' rather than fullscreen\\n      this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n      // Get the fullscreen element\\n      // Checks container is an ancestor, defaults to null\\n      this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n      // Register event listeners\\n      // Handle event (incase user presses escape etc)\\n      on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      });\\n\\n      // Fullscreen toggle on double click\\n      on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n        // Ignore double click in controls\\n        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n          return;\\n        }\\n        this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n      });\\n\\n      // Tap focus when in fullscreen\\n      on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n      // Update the UI\\n      this.update();\\n    }\\n\\n    // Determine if native supported\\n    static get nativeSupported() {\\n      return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n    }\\n\\n    // If we're actually using native\\n    get useNative() {\\n      return Fullscreen.nativeSupported && !this.forceFallback;\\n    }\\n\\n    // Get the prefix for handlers\\n    static get prefix() {\\n      // No prefix\\n      if (is.function(document.exitFullscreen)) return '';\\n\\n      // Check for fullscreen support by vendor prefix\\n      let value = '';\\n      const prefixes = ['webkit', 'moz', 'ms'];\\n      prefixes.some(pre => {\\n        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n          value = pre;\\n          return true;\\n        }\\n        return false;\\n      });\\n      return value;\\n    }\\n    static get property() {\\n      return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n    }\\n\\n    // Determine if fullscreen is supported\\n    get supported() {\\n      return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n    }\\n\\n    // Get active state\\n    get active() {\\n      if (!this.supported) return false;\\n\\n      // Fallback using classname\\n      if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n      }\\n      const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n      return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n    }\\n\\n    // Get target element\\n    get target() {\\n      return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Load image avoiding xhr/fetch CORS issues\\n  // Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n  // By default it checks if it is at least 1px, but you can add a second argument to change this\\n  // ==========================================================================\\n\\n  function loadImage(src, minWidth = 1) {\\n    return new Promise((resolve, reject) => {\\n      const image = new Image();\\n      const handler = () => {\\n        delete image.onload;\\n        delete image.onerror;\\n        (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n      };\\n      Object.assign(image, {\\n        onload: handler,\\n        onerror: handler,\\n        src\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n  const ui = {\\n    addStyleHook() {\\n      toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n      toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n    },\\n    // Toggle native HTML5 media controls\\n    toggleNativeControls(toggle = false) {\\n      if (toggle && this.isHTML5) {\\n        this.media.setAttribute('controls', '');\\n      } else {\\n        this.media.removeAttribute('controls');\\n      }\\n    },\\n    // Setup the UI\\n    build() {\\n      // Re-attach media element listeners\\n      // TODO: Use event bubbling?\\n      this.listeners.media();\\n\\n      // Don't setup interface if no support\\n      if (!this.supported.ui) {\\n        this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n        // Restore native controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Bail\\n        return;\\n      }\\n\\n      // Inject custom controls if not present\\n      if (!is.element(this.elements.controls)) {\\n        // Inject custom controls\\n        controls.inject.call(this);\\n\\n        // Re-attach control listeners\\n        this.listeners.controls();\\n      }\\n\\n      // Remove native controls\\n      ui.toggleNativeControls.call(this);\\n\\n      // Setup captions for HTML5\\n      if (this.isHTML5) {\\n        captions.setup.call(this);\\n      }\\n\\n      // Reset volume\\n      this.volume = null;\\n\\n      // Reset mute state\\n      this.muted = null;\\n\\n      // Reset loop state\\n      this.loop = null;\\n\\n      // Reset quality setting\\n      this.quality = null;\\n\\n      // Reset speed\\n      this.speed = null;\\n\\n      // Reset volume display\\n      controls.updateVolume.call(this);\\n\\n      // Reset time display\\n      controls.timeUpdate.call(this);\\n\\n      // Reset duration display\\n      controls.durationUpdate.call(this);\\n\\n      // Update the UI\\n      ui.checkPlaying.call(this);\\n\\n      // Check for picture-in-picture support\\n      toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n      // Check for airplay support\\n      toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n      // Add touch class\\n      toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n      // Ready for API calls\\n      this.ready = true;\\n\\n      // Ready event at end of execution stack\\n      setTimeout(() => {\\n        triggerEvent.call(this, this.media, 'ready');\\n      }, 0);\\n\\n      // Set the title\\n      ui.setTitle.call(this);\\n\\n      // Assure the poster image is set, if the property was added before the element was created\\n      if (this.poster) {\\n        ui.setPoster.call(this, this.poster, false).catch(() => {});\\n      }\\n\\n      // Manually set the duration if user has overridden it.\\n      // The event listeners for it doesn't get called if preload is disabled (#701)\\n      if (this.config.duration) {\\n        controls.durationUpdate.call(this);\\n      }\\n\\n      // Media metadata\\n      if (this.config.mediaMetadata) {\\n        controls.setMediaMetadata.call(this);\\n      }\\n    },\\n    // Setup aria attribute for play and iframe title\\n    setTitle() {\\n      // Find the current text\\n      let label = i18n.get('play', this.config);\\n\\n      // If there's a media title set, use that for the label\\n      if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n        label += `, ${this.config.title}`;\\n      }\\n\\n      // If there's a play button, set label\\n      Array.from(this.elements.buttons.play || []).forEach(button => {\\n        button.setAttribute('aria-label', label);\\n      });\\n\\n      // Set iframe title\\n      // https://github.com/sampotts/plyr/issues/124\\n      if (this.isEmbed) {\\n        const iframe = getElement.call(this, 'iframe');\\n        if (!is.element(iframe)) {\\n          return;\\n        }\\n\\n        // Default to media type\\n        const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n        const format = i18n.get('frameTitle', this.config);\\n        iframe.setAttribute('title', format.replace('{title}', title));\\n      }\\n    },\\n    // Toggle poster\\n    togglePoster(enable) {\\n      toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n    },\\n    // Set the poster image (async)\\n    // Used internally for the poster setter, with the passive option forced to false\\n    setPoster(poster, passive = true) {\\n      // Don't override if call is passive\\n      if (passive && this.poster) {\\n        return Promise.reject(new Error('Poster already set'));\\n      }\\n\\n      // Set property synchronously to respect the call order\\n      this.media.setAttribute('data-poster', poster);\\n\\n      // Show the poster\\n      this.elements.poster.removeAttribute('hidden');\\n\\n      // Wait until ui is ready\\n      return ready.call(this)\\n      // Load image\\n      .then(() => loadImage(poster)).catch(error => {\\n        // Hide poster on error unless it's been set by another call\\n        if (poster === this.poster) {\\n          ui.togglePoster.call(this, false);\\n        }\\n        // Rethrow\\n        throw error;\\n      }).then(() => {\\n        // Prevent race conditions\\n        if (poster !== this.poster) {\\n          throw new Error('setPoster cancelled by later call to setPoster');\\n        }\\n      }).then(() => {\\n        Object.assign(this.elements.poster.style, {\\n          backgroundImage: `url('${poster}')`,\\n          // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n          backgroundSize: ''\\n        });\\n        ui.togglePoster.call(this, true);\\n        return poster;\\n      });\\n    },\\n    // Check playing state\\n    checkPlaying(event) {\\n      // Class hooks\\n      toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n      toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n      // Set state\\n      Array.from(this.elements.buttons.play || []).forEach(target => {\\n        Object.assign(target, {\\n          pressed: this.playing\\n        });\\n        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n      });\\n\\n      // Only update controls on non timeupdate events\\n      if (is.event(event) && event.type === 'timeupdate') {\\n        return;\\n      }\\n\\n      // Toggle controls\\n      ui.toggleControls.call(this);\\n    },\\n    // Check if media is loading\\n    checkLoading(event) {\\n      this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n      // Clear timer\\n      clearTimeout(this.timers.loading);\\n\\n      // Timer to prevent flicker when seeking\\n      this.timers.loading = setTimeout(() => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      }, this.loading ? 250 : 0);\\n    },\\n    // Toggle controls based on state and `force` argument\\n    toggleControls(force) {\\n      const {\\n        controls: controlsElement\\n      } = this.elements;\\n      if (controlsElement && this.config.hideControls) {\\n        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n        const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n        // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n        this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n      }\\n    },\\n    // Migrate any custom properties from the media to the parent\\n    migrateStyles() {\\n      // Loop through values (as they are the keys when the object is spread 🤔)\\n      Object.values({\\n        ...this.media.style\\n      })\\n      // We're only fussed about Plyr specific properties\\n      .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n      // Remove attribute if empty\\n      if (is.empty(this.media.style)) {\\n        this.media.removeAttribute('style');\\n      }\\n    }\\n  };\\n\\n  class Listeners {\\n    constructor(_player) {\\n      // Device is touch enabled\\n      _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        player.touch = true;\\n\\n        // Add touch class\\n        toggleClass(elements.container, player.config.classNames.isTouch, true);\\n      });\\n      // Global window & document listeners\\n      _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n        const {\\n          player\\n        } = this;\\n\\n        // Keyboard shortcuts\\n        if (player.config.keyboard.global) {\\n          toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n        }\\n\\n        // Click anywhere closes menu\\n        toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n        // Detect touch by events\\n        once.call(player, document.body, 'touchstart', this.firstTouch);\\n      });\\n      // Container listeners\\n      _defineProperty$1(this, \\\"container\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          config,\\n          elements,\\n          timers\\n        } = player;\\n\\n        // Keyboard shortcuts\\n        if (!config.keyboard.global && config.keyboard.focused) {\\n          on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n        }\\n\\n        // Toggle controls on mouse events and entering fullscreen\\n        on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n          const {\\n            controls: controlsElement\\n          } = elements;\\n\\n          // Remove button states for fullscreen\\n          if (controlsElement && event.type === 'enterfullscreen') {\\n            controlsElement.pressed = false;\\n            controlsElement.hover = false;\\n          }\\n\\n          // Show, then hide after a timeout unless another control event occurs\\n          const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n          let delay = 0;\\n          if (show) {\\n            ui.toggleControls.call(player, true);\\n            // Use longer timeout for touch devices\\n            delay = player.touch ? 3000 : 2000;\\n          }\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Set new timer to prevent flicker when seeking\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Set a gutter for Vimeo\\n        const setGutter = () => {\\n          if (!player.isVimeo || player.config.vimeo.premium) {\\n            return;\\n          }\\n          const target = elements.wrapper;\\n          const {\\n            active\\n          } = player.fullscreen;\\n          const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n          const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n          // If not active, remove styles\\n          if (!active) {\\n            if (useNativeAspectRatio) {\\n              target.style.width = null;\\n              target.style.height = null;\\n            } else {\\n              target.style.maxWidth = null;\\n              target.style.margin = null;\\n            }\\n            return;\\n          }\\n\\n          // Determine which dimension will overflow and constrain view\\n          const [viewportWidth, viewportHeight] = getViewportSize();\\n          const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n          if (useNativeAspectRatio) {\\n            target.style.width = overflow ? 'auto' : '100%';\\n            target.style.height = overflow ? '100%' : 'auto';\\n          } else {\\n            target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n            target.style.margin = overflow ? '0 auto' : null;\\n          }\\n        };\\n\\n        // Handle resizing\\n        const resized = () => {\\n          clearTimeout(timers.resized);\\n          timers.resized = setTimeout(setGutter, 50);\\n        };\\n        on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n          const {\\n            target\\n          } = player.fullscreen;\\n\\n          // Ignore events not from target\\n          if (target !== elements.container) {\\n            return;\\n          }\\n\\n          // If it's not an embed and no ratio specified\\n          if (!player.isEmbed && is.empty(player.config.ratio)) {\\n            return;\\n          }\\n\\n          // Set Vimeo gutter\\n          setGutter();\\n\\n          // Watch for resizes\\n          const method = event.type === 'enterfullscreen' ? on : off;\\n          method.call(player, window, 'resize', resized);\\n        });\\n      });\\n      // Listen for media events\\n      _defineProperty$1(this, \\\"media\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n\\n        // Time change on media\\n        on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n        // Display duration\\n        on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n        // Handle the media finishing\\n        on.call(player, player.media, 'ended', () => {\\n          // Show poster on end\\n          if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n            // Restart\\n            player.restart();\\n\\n            // Call pause otherwise IE11 will start playing the video again\\n            player.pause();\\n          }\\n        });\\n\\n        // Check for buffer progress\\n        on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n        // Handle volume changes\\n        on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n        // Handle play/pause\\n        on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n        // Loading state\\n        on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n        // Click video\\n        if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n          // Re-fetch the wrapper\\n          const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n          // Bail if there's no wrapper (this should never happen)\\n          if (!is.element(wrapper)) {\\n            return;\\n          }\\n\\n          // On click play, pause or restart\\n          on.call(player, elements.container, 'click', event => {\\n            const targets = [elements.container, wrapper];\\n\\n            // Ignore if click if not container or in video wrapper\\n            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n              return;\\n            }\\n\\n            // Touch devices will just show controls (if hidden)\\n            if (player.touch && player.config.hideControls) {\\n              return;\\n            }\\n            if (player.ended) {\\n              this.proxy(event, player.restart, 'restart');\\n              this.proxy(event, () => {\\n                silencePromise(player.play());\\n              }, 'play');\\n            } else {\\n              this.proxy(event, () => {\\n                silencePromise(player.togglePlay());\\n              }, 'play');\\n            }\\n          });\\n        }\\n\\n        // Disable right click\\n        if (player.supported.ui && player.config.disableContextMenu) {\\n          on.call(player, elements.wrapper, 'contextmenu', event => {\\n            event.preventDefault();\\n          }, false);\\n        }\\n\\n        // Volume change\\n        on.call(player, player.media, 'volumechange', () => {\\n          // Save to storage\\n          player.storage.set({\\n            volume: player.volume,\\n            muted: player.muted\\n          });\\n        });\\n\\n        // Speed change\\n        on.call(player, player.media, 'ratechange', () => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'speed');\\n\\n          // Save to storage\\n          player.storage.set({\\n            speed: player.speed\\n          });\\n        });\\n\\n        // Quality change\\n        on.call(player, player.media, 'qualitychange', event => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n        });\\n\\n        // Update download link when ready and if quality changes\\n        on.call(player, player.media, 'ready qualitychange', () => {\\n          controls.setDownloadUrl.call(player);\\n        });\\n\\n        // Proxy events to container\\n        // Bubble up key events for Edge\\n        const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n        on.call(player, player.media, proxyEvents, event => {\\n          let {\\n            detail = {}\\n          } = event;\\n\\n          // Get error details from media\\n          if (event.type === 'error') {\\n            detail = player.media.error;\\n          }\\n          triggerEvent.call(player, elements.container, event.type, true, detail);\\n        });\\n      });\\n      // Run default and custom handlers\\n      _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        let returned = true;\\n\\n        // Execute custom handler\\n        if (hasCustomHandler) {\\n          returned = customHandler.call(player, event);\\n        }\\n\\n        // Only call default handler if not prevented in custom handler\\n        if (returned !== false && is.function(defaultHandler)) {\\n          defaultHandler.call(player, event);\\n        }\\n      });\\n      // Trigger custom and default handlers\\n      _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n      });\\n      // Listen for control events\\n      _defineProperty$1(this, \\\"controls\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        // IE doesn't support input event, so we fallback to change\\n        const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n        // Play/pause toggle\\n        if (elements.buttons.play) {\\n          Array.from(elements.buttons.play).forEach(button => {\\n            this.bind(button, 'click', () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          });\\n        }\\n\\n        // Pause\\n        this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n        // Rewind\\n        this.bind(elements.buttons.rewind, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n          player.lastSeekTime = Date.now();\\n          player.rewind();\\n        }, 'rewind');\\n\\n        // Rewind\\n        this.bind(elements.buttons.fastForward, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n          player.lastSeekTime = Date.now();\\n          player.forward();\\n        }, 'fastForward');\\n\\n        // Mute toggle\\n        this.bind(elements.buttons.mute, 'click', () => {\\n          player.muted = !player.muted;\\n        }, 'mute');\\n\\n        // Captions toggle\\n        this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n        // Download\\n        this.bind(elements.buttons.download, 'click', () => {\\n          triggerEvent.call(player, player.media, 'download');\\n        }, 'download');\\n\\n        // Fullscreen toggle\\n        this.bind(elements.buttons.fullscreen, 'click', () => {\\n          player.fullscreen.toggle();\\n        }, 'fullscreen');\\n\\n        // Picture-in-Picture\\n        this.bind(elements.buttons.pip, 'click', () => {\\n          player.pip = 'toggle';\\n        }, 'pip');\\n\\n        // Airplay\\n        this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n        // Settings menu - click toggle\\n        this.bind(elements.buttons.settings, 'click', event => {\\n          // Prevent the document click listener closing the menu\\n          event.stopPropagation();\\n          event.preventDefault();\\n          controls.toggleMenu.call(player, event);\\n        }, null, false); // Can't be passive as we're preventing default\\n\\n        // Settings menu - keyboard toggle\\n        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n        this.bind(elements.buttons.settings, 'keyup', event => {\\n          if (![' ', 'Enter'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Because return triggers a click anyway, all we need to do is set focus\\n          if (event.key === 'Enter') {\\n            controls.focusFirstMenuItem.call(player, null, true);\\n            return;\\n          }\\n\\n          // Prevent scroll\\n          event.preventDefault();\\n\\n          // Prevent playing video (Firefox)\\n          event.stopPropagation();\\n\\n          // Toggle menu\\n          controls.toggleMenu.call(player, event);\\n        }, null, false // Can't be passive as we're preventing default\\n        );\\n\\n        // Escape closes menu\\n        this.bind(elements.settings.menu, 'keydown', event => {\\n          if (event.key === 'Escape') {\\n            controls.toggleMenu.call(player, event);\\n          }\\n        });\\n\\n        // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n        this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n          const rect = elements.progress.getBoundingClientRect();\\n          const percent = 100 / rect.width * (event.pageX - rect.left);\\n          event.currentTarget.setAttribute('seek-value', percent);\\n        });\\n\\n        // Pause while seeking\\n        this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n          const seek = event.currentTarget;\\n          const attribute = 'play-on-seeked';\\n          if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Record seek time so we can prevent hiding controls for a few seconds after seek\\n          player.lastSeekTime = Date.now();\\n\\n          // Was playing before?\\n          const play = seek.hasAttribute(attribute);\\n          // Done seeking\\n          const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n          // If we're done seeking and it was playing, resume playback\\n          if (play && done) {\\n            seek.removeAttribute(attribute);\\n            silencePromise(player.play());\\n          } else if (!done && player.playing) {\\n            seek.setAttribute(attribute, '');\\n            player.pause();\\n          }\\n        });\\n\\n        // Fix range inputs on iOS\\n        // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n        // it takes over further interactions on the page. This is a hack\\n        if (browser.isIos) {\\n          const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n          Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n        }\\n\\n        // Seek\\n        this.bind(elements.inputs.seek, inputEvent, event => {\\n          const seek = event.currentTarget;\\n          // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n          let seekTo = seek.getAttribute('seek-value');\\n          if (is.empty(seekTo)) {\\n            seekTo = seek.value;\\n          }\\n          seek.removeAttribute('seek-value');\\n          player.currentTime = seekTo / seek.max * player.duration;\\n        }, 'seek');\\n\\n        // Seek tooltip\\n        this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n        // Preview thumbnails plugin\\n        // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n        this.bind(elements.progress, 'mousemove touchmove', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startMove(event);\\n          }\\n        });\\n\\n        // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n        this.bind(elements.progress, 'mouseleave touchend click', () => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endMove(false, true);\\n          }\\n        });\\n\\n        // Show scrubbing preview\\n        this.bind(elements.progress, 'mousedown touchstart', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startScrubbing(event);\\n          }\\n        });\\n        this.bind(elements.progress, 'mouseup touchend', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endScrubbing(event);\\n          }\\n        });\\n\\n        // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n        if (browser.isWebKit) {\\n          Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n            this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n          });\\n        }\\n\\n        // Current time invert\\n        // Only if one time element is used for both currentTime and duration\\n        if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n          this.bind(elements.display.currentTime, 'click', () => {\\n            // Do nothing if we're at the start\\n            if (player.currentTime === 0) {\\n              return;\\n            }\\n            player.config.invertTime = !player.config.invertTime;\\n            controls.timeUpdate.call(player);\\n          });\\n        }\\n\\n        // Volume\\n        this.bind(elements.inputs.volume, inputEvent, event => {\\n          player.volume = event.target.value;\\n        }, 'volume');\\n\\n        // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n          elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n        });\\n\\n        // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n        if (elements.fullscreen) {\\n          Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n            this.bind(child, 'mouseenter mouseleave', event => {\\n              if (elements.controls) {\\n                elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n              }\\n            });\\n          });\\n        }\\n\\n        // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n          elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n        });\\n\\n        // Show controls when they receive focus (e.g., when using keyboard tab key)\\n        this.bind(elements.controls, 'focusin', () => {\\n          const {\\n            config,\\n            timers\\n          } = player;\\n\\n          // Skip transition to prevent focus from scrolling the parent element\\n          toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n          // Toggle\\n          ui.toggleControls.call(player, true);\\n\\n          // Restore transition\\n          setTimeout(() => {\\n            toggleClass(elements.controls, config.classNames.noTransition, false);\\n          }, 0);\\n\\n          // Delay a little more for mouse users\\n          const delay = this.touch ? 3000 : 4000;\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Hide again after delay\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Mouse wheel for volume\\n        this.bind(elements.inputs.volume, 'wheel', event => {\\n          // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n          // Other browsers on OS X will be inverted until support improves\\n          const inverted = event.webkitDirectionInvertedFromDevice;\\n          // Get delta from event. Invert if `inverted` is true\\n          const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n          // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n          const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n          // Change the volume by 2%\\n          player.increaseVolume(direction / 50);\\n\\n          // Don't break page scrolling at max and min\\n          const {\\n            volume\\n          } = player.media;\\n          if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n            event.preventDefault();\\n          }\\n        }, 'volume', false);\\n      });\\n      this.player = _player;\\n      this.lastKey = null;\\n      this.focusTimer = null;\\n      this.lastKeyDown = null;\\n      this.handleKey = this.handleKey.bind(this);\\n      this.toggleMenu = this.toggleMenu.bind(this);\\n      this.firstTouch = this.firstTouch.bind(this);\\n    }\\n\\n    // Handle key presses\\n    handleKey(event) {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      const {\\n        key,\\n        type,\\n        altKey,\\n        ctrlKey,\\n        metaKey,\\n        shiftKey\\n      } = event;\\n      const pressed = type === 'keydown';\\n      const repeat = pressed && key === this.lastKey;\\n\\n      // Bail if a modifier key is set\\n      if (altKey || ctrlKey || metaKey || shiftKey) {\\n        return;\\n      }\\n\\n      // If the event is bubbled from the media element\\n      // Firefox doesn't get the key for whatever reason\\n      if (!key) {\\n        return;\\n      }\\n\\n      // Seek by increment\\n      const seekByIncrement = increment => {\\n        // Divide the max duration into 10th's and times by the number value\\n        player.currentTime = player.duration / 10 * increment;\\n      };\\n\\n      // Handle the key on keydown\\n      // Reset on keyup\\n      if (pressed) {\\n        // Check focused element\\n        // and if the focused element is not editable (e.g. text input)\\n        // and any that accept key input http://webaim.org/techniques/keyboard/\\n        const focused = document.activeElement;\\n        if (is.element(focused)) {\\n          const {\\n            editable\\n          } = player.config.selectors;\\n          const {\\n            seek\\n          } = elements.inputs;\\n          if (focused !== seek && matches(focused, editable)) {\\n            return;\\n          }\\n          if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n            return;\\n          }\\n        }\\n\\n        // Which keys should we prevent default\\n        const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n        // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n        if (preventDefault.includes(key)) {\\n          event.preventDefault();\\n          event.stopPropagation();\\n        }\\n        switch (key) {\\n          case '0':\\n          case '1':\\n          case '2':\\n          case '3':\\n          case '4':\\n          case '5':\\n          case '6':\\n          case '7':\\n          case '8':\\n          case '9':\\n            if (!repeat) {\\n              seekByIncrement(parseInt(key, 10));\\n            }\\n            break;\\n          case ' ':\\n          case 'k':\\n            if (!repeat) {\\n              silencePromise(player.togglePlay());\\n            }\\n            break;\\n          case 'ArrowUp':\\n            player.increaseVolume(0.1);\\n            break;\\n          case 'ArrowDown':\\n            player.decreaseVolume(0.1);\\n            break;\\n          case 'm':\\n            if (!repeat) {\\n              player.muted = !player.muted;\\n            }\\n            break;\\n          case 'ArrowRight':\\n            player.forward();\\n            break;\\n          case 'ArrowLeft':\\n            player.rewind();\\n            break;\\n          case 'f':\\n            player.fullscreen.toggle();\\n            break;\\n          case 'c':\\n            if (!repeat) {\\n              player.toggleCaptions();\\n            }\\n            break;\\n          case 'l':\\n            player.loop = !player.loop;\\n            break;\\n        }\\n\\n        // Escape is handle natively when in full screen\\n        // So we only need to worry about non native\\n        if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n          player.fullscreen.toggle();\\n        }\\n\\n        // Store last key for next cycle\\n        this.lastKey = key;\\n      } else {\\n        this.lastKey = null;\\n      }\\n    }\\n\\n    // Toggle menu\\n    toggleMenu(event) {\\n      controls.toggleMenu.call(this.player, event);\\n    }\\n  }\\n\\n  var loadjs_umd = createCommonjsModule(function (module, exports) {\\n    (function (root, factory) {\\n      {\\n        module.exports = factory();\\n      }\\n    })(commonjsGlobal, function () {\\n      /**\\n       * Global dependencies.\\n       * @global {Object} document - DOM\\n       */\\n\\n      var devnull = function () {},\\n        bundleIdCache = {},\\n        bundleResultCache = {},\\n        bundleCallbackQueue = {};\\n\\n      /**\\n       * Subscribe to bundle load event.\\n       * @param {string[]} bundleIds - Bundle ids\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function subscribe(bundleIds, callbackFn) {\\n        // listify\\n        bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n        var depsNotFound = [],\\n          i = bundleIds.length,\\n          numWaiting = i,\\n          fn,\\n          bundleId,\\n          r,\\n          q;\\n\\n        // define callback function\\n        fn = function (bundleId, pathsNotFound) {\\n          if (pathsNotFound.length) depsNotFound.push(bundleId);\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(depsNotFound);\\n        };\\n\\n        // register callback\\n        while (i--) {\\n          bundleId = bundleIds[i];\\n\\n          // execute callback if in result cache\\n          r = bundleResultCache[bundleId];\\n          if (r) {\\n            fn(bundleId, r);\\n            continue;\\n          }\\n\\n          // add to callback queue\\n          q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n          q.push(fn);\\n        }\\n      }\\n\\n      /**\\n       * Publish bundle load event.\\n       * @param {string} bundleId - Bundle id\\n       * @param {string[]} pathsNotFound - List of files not found\\n       */\\n      function publish(bundleId, pathsNotFound) {\\n        // exit if id isn't defined\\n        if (!bundleId) return;\\n        var q = bundleCallbackQueue[bundleId];\\n\\n        // cache result\\n        bundleResultCache[bundleId] = pathsNotFound;\\n\\n        // exit if queue is empty\\n        if (!q) return;\\n\\n        // empty callback queue\\n        while (q.length) {\\n          q[0](bundleId, pathsNotFound);\\n          q.splice(0, 1);\\n        }\\n      }\\n\\n      /**\\n       * Execute callbacks.\\n       * @param {Object or Function} args - The callback args\\n       * @param {string[]} depsNotFound - List of dependencies not found\\n       */\\n      function executeCallbacks(args, depsNotFound) {\\n        // accept function as argument\\n        if (args.call) args = {\\n          success: args\\n        };\\n\\n        // success and error callbacks\\n        if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n      }\\n\\n      /**\\n       * Load individual file.\\n       * @param {string} path - The file path\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFile(path, callbackFn, args, numTries) {\\n        var doc = document,\\n          async = args.async,\\n          maxTries = (args.numRetries || 0) + 1,\\n          beforeCallbackFn = args.before || devnull,\\n          pathname = path.replace(/[\\\\?|#].*$/, ''),\\n          pathStripped = path.replace(/^(css|img)!/, ''),\\n          isLegacyIECss,\\n          e;\\n        numTries = numTries || 0;\\n        if (/(^css!|\\\\.css$)/.test(pathname)) {\\n          // css\\n          e = doc.createElement('link');\\n          e.rel = 'stylesheet';\\n          e.href = pathStripped;\\n\\n          // tag IE9+\\n          isLegacyIECss = 'hideFocus' in e;\\n\\n          // use preload in IE Edge (to detect load errors)\\n          if (isLegacyIECss && e.relList) {\\n            isLegacyIECss = 0;\\n            e.rel = 'preload';\\n            e.as = 'style';\\n          }\\n        } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n          // image\\n          e = doc.createElement('img');\\n          e.src = pathStripped;\\n        } else {\\n          // javascript\\n          e = doc.createElement('script');\\n          e.src = path;\\n          e.async = async === undefined ? true : async;\\n        }\\n        e.onload = e.onerror = e.onbeforeload = function (ev) {\\n          var result = ev.type[0];\\n\\n          // treat empty stylesheets as failures to get around lack of onerror\\n          // support in IE9-11\\n          if (isLegacyIECss) {\\n            try {\\n              if (!e.sheet.cssText.length) result = 'e';\\n            } catch (x) {\\n              // sheets objects created from load errors don't allow access to\\n              // `cssText` (unless error is Code:18 SecurityError)\\n              if (x.code != 18) result = 'e';\\n            }\\n          }\\n\\n          // handle retries in case of load failure\\n          if (result == 'e') {\\n            // increment counter\\n            numTries += 1;\\n\\n            // exit function and try again\\n            if (numTries < maxTries) {\\n              return loadFile(path, callbackFn, args, numTries);\\n            }\\n          } else if (e.rel == 'preload' && e.as == 'style') {\\n            // activate preloaded stylesheets\\n            return e.rel = 'stylesheet'; // jshint ignore:line\\n          }\\n\\n          // execute callback\\n          callbackFn(path, result, ev.defaultPrevented);\\n        };\\n\\n        // add to document (unless callback returns `false`)\\n        if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n      }\\n\\n      /**\\n       * Load multiple files.\\n       * @param {string[]} paths - The file paths\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFiles(paths, callbackFn, args) {\\n        // listify paths\\n        paths = paths.push ? paths : [paths];\\n        var numWaiting = paths.length,\\n          x = numWaiting,\\n          pathsNotFound = [],\\n          fn,\\n          i;\\n\\n        // define callback function\\n        fn = function (path, result, defaultPrevented) {\\n          // handle error\\n          if (result == 'e') pathsNotFound.push(path);\\n\\n          // handle beforeload event. If defaultPrevented then that means the load\\n          // will be blocked (ex. Ghostery/ABP on Safari)\\n          if (result == 'b') {\\n            if (defaultPrevented) pathsNotFound.push(path);else return;\\n          }\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(pathsNotFound);\\n        };\\n\\n        // load scripts\\n        for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n      }\\n\\n      /**\\n       * Initiate script load and register bundle.\\n       * @param {(string|string[])} paths - The file paths\\n       * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n       *   callback or (3) object literal with success/error arguments, numRetries,\\n       *   etc.\\n       * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n       *   literal with success/error arguments, numRetries, etc.\\n       */\\n      function loadjs(paths, arg1, arg2) {\\n        var bundleId, args;\\n\\n        // bundleId (if string)\\n        if (arg1 && arg1.trim) bundleId = arg1;\\n\\n        // args (default is {})\\n        args = (bundleId ? arg2 : arg1) || {};\\n\\n        // throw error if bundle is already defined\\n        if (bundleId) {\\n          if (bundleId in bundleIdCache) {\\n            throw \\\"LoadJS\\\";\\n          } else {\\n            bundleIdCache[bundleId] = true;\\n          }\\n        }\\n        function loadFn(resolve, reject) {\\n          loadFiles(paths, function (pathsNotFound) {\\n            // execute callbacks\\n            executeCallbacks(args, pathsNotFound);\\n\\n            // resolve Promise\\n            if (resolve) {\\n              executeCallbacks({\\n                success: resolve,\\n                error: reject\\n              }, pathsNotFound);\\n            }\\n\\n            // publish bundle load event\\n            publish(bundleId, pathsNotFound);\\n          }, args);\\n        }\\n        if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n      }\\n\\n      /**\\n       * Execute callbacks when dependencies have been satisfied.\\n       * @param {(string|string[])} deps - List of bundle ids\\n       * @param {Object} args - success/error arguments\\n       */\\n      loadjs.ready = function ready(deps, args) {\\n        // subscribe to bundle load event\\n        subscribe(deps, function (depsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, depsNotFound);\\n        });\\n        return loadjs;\\n      };\\n\\n      /**\\n       * Manually satisfy bundle dependencies.\\n       * @param {string} bundleId - The bundle id\\n       */\\n      loadjs.done = function done(bundleId) {\\n        publish(bundleId, []);\\n      };\\n\\n      /**\\n       * Reset loadjs dependencies statuses\\n       */\\n      loadjs.reset = function reset() {\\n        bundleIdCache = {};\\n        bundleResultCache = {};\\n        bundleCallbackQueue = {};\\n      };\\n\\n      /**\\n       * Determine if bundle has already been defined\\n       * @param String} bundleId - The bundle id\\n       */\\n      loadjs.isDefined = function isDefined(bundleId) {\\n        return bundleId in bundleIdCache;\\n      };\\n\\n      // export\\n      return loadjs;\\n    });\\n  });\\n\\n  // ==========================================================================\\n  function loadScript(url) {\\n    return new Promise((resolve, reject) => {\\n      loadjs_umd(url, {\\n        success: resolve,\\n        error: reject\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Parse Vimeo ID from URL\\n  function parseId$1(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    if (is.number(Number(url))) {\\n      return url;\\n    }\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Try to extract a hash for private videos from the URL\\n  function parseHash(url) {\\n    /* This regex matches a hexadecimal hash if given in any of these forms:\\n     *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n     *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n     *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n     *  - video/{id}/{hash}\\n     * If matched, the hash is available in capture group 4\\n     */\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n    const found = url.match(regex);\\n    return found && found.length === 5 ? found[4] : null;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState$1(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  const vimeo = {\\n    setup() {\\n      const player = this;\\n\\n      // Add embed class for responsive\\n      toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set intial ratio\\n      setAspectRatio.call(player);\\n\\n      // Load the SDK if not already\\n      if (!is.object(window.Vimeo)) {\\n        loadScript(player.config.urls.vimeo.sdk).then(() => {\\n          vimeo.ready.call(player);\\n        }).catch(error => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n      } else {\\n        vimeo.ready.call(player);\\n      }\\n    },\\n    // API Ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.vimeo;\\n      const {\\n        premium,\\n        referrerPolicy,\\n        ...frameParams\\n      } = config;\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n      let hash = '';\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(player.config.attributes.embed.id);\\n        // hash can also be set as attribute on the <div>\\n        hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n      } else {\\n        hash = parseHash(source);\\n      }\\n      const hashParam = hash ? {\\n        h: hash\\n      } : {};\\n\\n      // If the owner has a pro or premium account then we can hide controls etc\\n      if (premium) {\\n        Object.assign(frameParams, {\\n          controls: false,\\n          sidedock: false\\n        });\\n      }\\n\\n      // Get Vimeo params for the iframe\\n      const params = buildUrlParams({\\n        loop: player.config.loop.active,\\n        autoplay: player.autoplay,\\n        muted: player.muted,\\n        gesture: 'media',\\n        playsinline: player.config.playsinline,\\n        // hash has to be added to iframe-URL\\n        ...hashParam,\\n        ...frameParams\\n      });\\n      const id = parseId$1(source);\\n      // Build an iframe\\n      const iframe = createElement('iframe');\\n      const src = format(player.config.urls.vimeo.iframe, id, params);\\n      iframe.setAttribute('src', src);\\n      iframe.setAttribute('allowfullscreen', '');\\n      iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n      // Set the referrer policy if required\\n      if (!is.empty(referrerPolicy)) {\\n        iframe.setAttribute('referrerPolicy', referrerPolicy);\\n      }\\n\\n      // Inject the package\\n      if (premium || !config.customControls) {\\n        iframe.setAttribute('data-poster', player.poster);\\n        player.media = replaceElement(iframe, player.media);\\n      } else {\\n        const wrapper = createElement('div', {\\n          class: player.config.classNames.embedContainer,\\n          'data-poster': player.poster\\n        });\\n        wrapper.appendChild(iframe);\\n        player.media = replaceElement(wrapper, player.media);\\n      }\\n\\n      // Get poster image\\n      if (!config.customControls) {\\n        fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n          if (is.empty(response) || !response.thumbnail_url) {\\n            return;\\n          }\\n\\n          // Set and show poster\\n          ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n        });\\n      }\\n\\n      // Setup instance\\n      // https://github.com/vimeo/player.js\\n      player.embed = new window.Vimeo.Player(iframe, {\\n        autopause: player.config.autopause,\\n        muted: player.muted\\n      });\\n      player.media.paused = true;\\n      player.media.currentTime = 0;\\n\\n      // Disable native text track rendering\\n      if (player.supported.ui) {\\n        player.embed.disableTextTrack();\\n      }\\n\\n      // Create a faux HTML5 API using the Vimeo API\\n      player.media.play = () => {\\n        assurePlaybackState$1.call(player, true);\\n        return player.embed.play();\\n      };\\n      player.media.pause = () => {\\n        assurePlaybackState$1.call(player, false);\\n        return player.embed.pause();\\n      };\\n      player.media.stop = () => {\\n        player.pause();\\n        player.currentTime = 0;\\n      };\\n\\n      // Seeking\\n      let {\\n        currentTime\\n      } = player.media;\\n      Object.defineProperty(player.media, 'currentTime', {\\n        get() {\\n          return currentTime;\\n        },\\n        set(time) {\\n          // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n          // Get current paused state and volume etc\\n          const {\\n            embed,\\n            media,\\n            paused,\\n            volume\\n          } = player;\\n          const restorePause = paused && !embed.hasPlayed;\\n\\n          // Set seeking state and trigger event\\n          media.seeking = true;\\n          triggerEvent.call(player, media, 'seeking');\\n\\n          // If paused, mute until seek is complete\\n          Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n            // Do nothing\\n          });\\n        }\\n      });\\n\\n      // Playback speed\\n      let speed = player.config.speed.selected;\\n      Object.defineProperty(player.media, 'playbackRate', {\\n        get() {\\n          return speed;\\n        },\\n        set(input) {\\n          player.embed.setPlaybackRate(input).then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          }).catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n        }\\n      });\\n\\n      // Volume\\n      let {\\n        volume\\n      } = player.config;\\n      Object.defineProperty(player.media, 'volume', {\\n        get() {\\n          return volume;\\n        },\\n        set(input) {\\n          player.embed.setVolume(input).then(() => {\\n            volume = input;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Muted\\n      let {\\n        muted\\n      } = player.config;\\n      Object.defineProperty(player.media, 'muted', {\\n        get() {\\n          return muted;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : false;\\n          player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n            muted = toggle;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Loop\\n      let {\\n        loop\\n      } = player.config;\\n      Object.defineProperty(player.media, 'loop', {\\n        get() {\\n          return loop;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : player.config.loop.active;\\n          player.embed.setLoop(toggle).then(() => {\\n            loop = toggle;\\n          });\\n        }\\n      });\\n\\n      // Source\\n      let currentSrc;\\n      player.embed.getVideoUrl().then(value => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      }).catch(error => {\\n        this.debug.warn(error);\\n      });\\n      Object.defineProperty(player.media, 'currentSrc', {\\n        get() {\\n          return currentSrc;\\n        }\\n      });\\n\\n      // Ended\\n      Object.defineProperty(player.media, 'ended', {\\n        get() {\\n          return player.currentTime === player.duration;\\n        }\\n      });\\n\\n      // Set aspect ratio based on video size\\n      Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n        const [width, height] = dimensions;\\n        player.embed.ratio = roundAspectRatio(width, height);\\n        setAspectRatio.call(this);\\n      });\\n\\n      // Set autopause\\n      player.embed.setAutopause(player.config.autopause).then(state => {\\n        player.config.autopause = state;\\n      });\\n\\n      // Get title\\n      player.embed.getVideoTitle().then(title => {\\n        player.config.title = title;\\n        ui.setTitle.call(this);\\n      });\\n\\n      // Get current time\\n      player.embed.getCurrentTime().then(value => {\\n        currentTime = value;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n\\n      // Get duration\\n      player.embed.getDuration().then(value => {\\n        player.media.duration = value;\\n        triggerEvent.call(player, player.media, 'durationchange');\\n      });\\n\\n      // Get captions\\n      player.embed.getTextTracks().then(tracks => {\\n        player.media.textTracks = tracks;\\n        captions.setup.call(player);\\n      });\\n      player.embed.on('cuechange', ({\\n        cues = []\\n      }) => {\\n        const strippedCues = cues.map(cue => stripHTML(cue.text));\\n        captions.updateCues.call(player, strippedCues);\\n      });\\n      player.embed.on('loaded', () => {\\n        // Assure state and events are updated on autoplay\\n        player.embed.getPaused().then(paused => {\\n          assurePlaybackState$1.call(player, !paused);\\n          if (!paused) {\\n            triggerEvent.call(player, player.media, 'playing');\\n          }\\n        });\\n        if (is.element(player.embed.element) && player.supported.ui) {\\n          const frame = player.embed.element;\\n\\n          // Fix keyboard focus issues\\n          // https://github.com/sampotts/plyr/issues/317\\n          frame.setAttribute('tabindex', -1);\\n        }\\n      });\\n      player.embed.on('bufferstart', () => {\\n        triggerEvent.call(player, player.media, 'waiting');\\n      });\\n      player.embed.on('bufferend', () => {\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('play', () => {\\n        assurePlaybackState$1.call(player, true);\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('pause', () => {\\n        assurePlaybackState$1.call(player, false);\\n      });\\n      player.embed.on('timeupdate', data => {\\n        player.media.seeking = false;\\n        currentTime = data.seconds;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n      player.embed.on('progress', data => {\\n        player.media.buffered = data.percent;\\n        triggerEvent.call(player, player.media, 'progress');\\n\\n        // Check all loaded\\n        if (parseInt(data.percent, 10) === 1) {\\n          triggerEvent.call(player, player.media, 'canplaythrough');\\n        }\\n\\n        // Get duration as if we do it before load, it gives an incorrect value\\n        // https://github.com/sampotts/plyr/issues/891\\n        player.embed.getDuration().then(value => {\\n          if (value !== player.media.duration) {\\n            player.media.duration = value;\\n            triggerEvent.call(player, player.media, 'durationchange');\\n          }\\n        });\\n      });\\n      player.embed.on('seeked', () => {\\n        player.media.seeking = false;\\n        triggerEvent.call(player, player.media, 'seeked');\\n      });\\n      player.embed.on('ended', () => {\\n        player.media.paused = true;\\n        triggerEvent.call(player, player.media, 'ended');\\n      });\\n      player.embed.on('error', detail => {\\n        player.media.error = detail;\\n        triggerEvent.call(player, player.media, 'error');\\n      });\\n\\n      // Rebuild UI\\n      if (config.customControls) {\\n        setTimeout(() => ui.build.call(player), 0);\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Parse YouTube ID from URL\\n  function parseId(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  function getHost(config) {\\n    if (config.noCookie) {\\n      return 'https://www.youtube-nocookie.com';\\n    }\\n    if (window.location.protocol === 'http:') {\\n      return 'http://www.youtube.com';\\n    }\\n\\n    // Use YouTube's default\\n    return undefined;\\n  }\\n  const youtube = {\\n    setup() {\\n      // Add embed class for responsive\\n      toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n      // Setup API\\n      if (is.object(window.YT) && is.function(window.YT.Player)) {\\n        youtube.ready.call(this);\\n      } else {\\n        // Reference current global callback\\n        const callback = window.onYouTubeIframeAPIReady;\\n\\n        // Set callback to process queue\\n        window.onYouTubeIframeAPIReady = () => {\\n          // Call global callback if set\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n          youtube.ready.call(this);\\n        };\\n\\n        // Load the SDK\\n        loadScript(this.config.urls.youtube.sdk).catch(error => {\\n          this.debug.warn('YouTube API failed to load', error);\\n        });\\n      }\\n    },\\n    // Get the media title\\n    getTitle(videoId) {\\n      const url = format(this.config.urls.youtube.api, videoId);\\n      fetch(url).then(data => {\\n        if (is.object(data)) {\\n          const {\\n            title,\\n            height,\\n            width\\n          } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n        setAspectRatio.call(this);\\n      }).catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n    },\\n    // API ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.youtube;\\n      // Ignore already setup (race condition)\\n      const currentId = player.media && player.media.getAttribute('id');\\n      if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n        return;\\n      }\\n\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(this.config.attributes.embed.id);\\n      }\\n\\n      // Replace the <iframe> with a <div> due to YouTube API issues\\n      const videoId = parseId(source);\\n      const id = generateId(player.provider);\\n      // Replace media element\\n      const container = createElement('div', {\\n        id,\\n        'data-poster': config.customControls ? player.poster : undefined\\n      });\\n      player.media = replaceElement(container, player.media);\\n\\n      // Only load the poster when using custom controls\\n      if (config.customControls) {\\n        const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n        // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n        loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        }).catch(() => {});\\n      }\\n\\n      // Setup instance\\n      // https://developers.google.com/youtube/iframe_api_reference\\n      player.embed = new window.YT.Player(player.media, {\\n        videoId,\\n        host: getHost(config),\\n        playerVars: extend({}, {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null\\n        }, config),\\n        events: {\\n          onError(event) {\\n            // YouTube may fire onError twice, so only handle it once\\n            if (!player.media.error) {\\n              const code = event.data;\\n              // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n              const message = {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n              }[code] || 'An unknown error occurred';\\n              player.media.error = {\\n                code,\\n                message\\n              };\\n              triggerEvent.call(player, player.media, 'error');\\n            }\\n          },\\n          onPlaybackRateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get current speed\\n            player.media.playbackRate = instance.getPlaybackRate();\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          },\\n          onReady(event) {\\n            // Bail if onReady has already been called. See issue #1108\\n            if (is.function(player.media.play)) {\\n              return;\\n            }\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get the title\\n            youtube.getTitle.call(player, videoId);\\n\\n            // Create a faux HTML5 API using the YouTube API\\n            player.media.play = () => {\\n              assurePlaybackState.call(player, true);\\n              instance.playVideo();\\n            };\\n            player.media.pause = () => {\\n              assurePlaybackState.call(player, false);\\n              instance.pauseVideo();\\n            };\\n            player.media.stop = () => {\\n              instance.stopVideo();\\n            };\\n            player.media.duration = instance.getDuration();\\n            player.media.paused = true;\\n\\n            // Seeking\\n            player.media.currentTime = 0;\\n            Object.defineProperty(player.media, 'currentTime', {\\n              get() {\\n                return Number(instance.getCurrentTime());\\n              },\\n              set(time) {\\n                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n                if (player.paused && !player.embed.hasPlayed) {\\n                  player.embed.mute();\\n                }\\n\\n                // Set seeking state and trigger event\\n                player.media.seeking = true;\\n                triggerEvent.call(player, player.media, 'seeking');\\n\\n                // Seek after events sent\\n                instance.seekTo(time);\\n              }\\n            });\\n\\n            // Playback speed\\n            Object.defineProperty(player.media, 'playbackRate', {\\n              get() {\\n                return instance.getPlaybackRate();\\n              },\\n              set(input) {\\n                instance.setPlaybackRate(input);\\n              }\\n            });\\n\\n            // Volume\\n            let {\\n              volume\\n            } = player.config;\\n            Object.defineProperty(player.media, 'volume', {\\n              get() {\\n                return volume;\\n              },\\n              set(input) {\\n                volume = input;\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Muted\\n            let {\\n              muted\\n            } = player.config;\\n            Object.defineProperty(player.media, 'muted', {\\n              get() {\\n                return muted;\\n              },\\n              set(input) {\\n                const toggle = is.boolean(input) ? input : muted;\\n                muted = toggle;\\n                instance[toggle ? 'mute' : 'unMute']();\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Source\\n            Object.defineProperty(player.media, 'currentSrc', {\\n              get() {\\n                return instance.getVideoUrl();\\n              }\\n            });\\n\\n            // Ended\\n            Object.defineProperty(player.media, 'ended', {\\n              get() {\\n                return player.currentTime === player.duration;\\n              }\\n            });\\n\\n            // Get available speeds\\n            const speeds = instance.getAvailablePlaybackRates();\\n            // Filter based on config\\n            player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n            // Set the tabindex to avoid focus entering iframe\\n            if (player.supported.ui && config.customControls) {\\n              player.media.setAttribute('tabindex', -1);\\n            }\\n            triggerEvent.call(player, player.media, 'timeupdate');\\n            triggerEvent.call(player, player.media, 'durationchange');\\n\\n            // Reset timer\\n            clearInterval(player.timers.buffering);\\n\\n            // Setup buffering\\n            player.timers.buffering = setInterval(() => {\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n\\n              // Trigger progress only when we actually buffer something\\n              if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n                triggerEvent.call(player, player.media, 'progress');\\n              }\\n\\n              // Set last buffer point\\n              player.media.lastBuffered = player.media.buffered;\\n\\n              // Bail if we're at 100%\\n              if (player.media.buffered === 1) {\\n                clearInterval(player.timers.buffering);\\n\\n                // Trigger event\\n                triggerEvent.call(player, player.media, 'canplaythrough');\\n              }\\n            }, 200);\\n\\n            // Rebuild UI\\n            if (config.customControls) {\\n              setTimeout(() => ui.build.call(player), 50);\\n            }\\n          },\\n          onStateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Reset timer\\n            clearInterval(player.timers.playing);\\n            const seeked = player.media.seeking && [1, 2].includes(event.data);\\n            if (seeked) {\\n              // Unset seeking and fire seeked event\\n              player.media.seeking = false;\\n              triggerEvent.call(player, player.media, 'seeked');\\n            }\\n\\n            // Handle events\\n            // -1   Unstarted\\n            // 0    Ended\\n            // 1    Playing\\n            // 2    Paused\\n            // 3    Buffering\\n            // 5    Video cued\\n            switch (event.data) {\\n              case -1:\\n                // Update scrubber\\n                triggerEvent.call(player, player.media, 'timeupdate');\\n\\n                // Get loaded % from YouTube\\n                player.media.buffered = instance.getVideoLoadedFraction();\\n                triggerEvent.call(player, player.media, 'progress');\\n                break;\\n              case 0:\\n                assurePlaybackState.call(player, false);\\n\\n                // YouTube doesn't support loop for a single video, so mimick it.\\n                if (player.media.loop) {\\n                  // YouTube needs a call to `stopVideo` before playing again\\n                  instance.stopVideo();\\n                  instance.playVideo();\\n                } else {\\n                  triggerEvent.call(player, player.media, 'ended');\\n                }\\n                break;\\n              case 1:\\n                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                  player.media.pause();\\n                } else {\\n                  assurePlaybackState.call(player, true);\\n                  triggerEvent.call(player, player.media, 'playing');\\n\\n                  // Poll to get playback progress\\n                  player.timers.playing = setInterval(() => {\\n                    triggerEvent.call(player, player.media, 'timeupdate');\\n                  }, 50);\\n\\n                  // Check duration again due to YouTube bug\\n                  // https://github.com/sampotts/plyr/issues/374\\n                  // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                  if (player.media.duration !== instance.getDuration()) {\\n                    player.media.duration = instance.getDuration();\\n                    triggerEvent.call(player, player.media, 'durationchange');\\n                  }\\n                }\\n                break;\\n              case 2:\\n                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (!player.muted) {\\n                  player.embed.unMute();\\n                }\\n                assurePlaybackState.call(player, false);\\n                break;\\n              case 3:\\n                // Trigger waiting event to add loading classes to container as the video buffers.\\n                triggerEvent.call(player, player.media, 'waiting');\\n                break;\\n            }\\n            triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n              code: event.data\\n            });\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  // ==========================================================================\\n  const media = {\\n    // Setup media\\n    setup() {\\n      // If there's no media, bail\\n      if (!this.media) {\\n        this.debug.warn('No media element found!');\\n        return;\\n      }\\n\\n      // Add type class\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n      // Add provider class\\n      toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n      // Add video class for embeds\\n      // This will require changes if audio embeds are added\\n      if (this.isEmbed) {\\n        toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n      }\\n\\n      // Inject the player wrapper\\n      if (this.isVideo) {\\n        // Create the wrapper div\\n        this.elements.wrapper = createElement('div', {\\n          class: this.config.classNames.video\\n        });\\n\\n        // Wrap the video in a container\\n        wrap(this.media, this.elements.wrapper);\\n\\n        // Poster image container\\n        this.elements.poster = createElement('div', {\\n          class: this.config.classNames.poster\\n        });\\n        this.elements.wrapper.appendChild(this.elements.poster);\\n      }\\n      if (this.isHTML5) {\\n        html5.setup.call(this);\\n      } else if (this.isYouTube) {\\n        youtube.setup.call(this);\\n      } else if (this.isVimeo) {\\n        vimeo.setup.call(this);\\n      }\\n    }\\n  };\\n\\n  const destroy = instance => {\\n    // Destroy our adsManager\\n    if (instance.manager) {\\n      instance.manager.destroy();\\n    }\\n\\n    // Destroy our adsManager\\n    if (instance.elements.displayContainer) {\\n      instance.elements.displayContainer.destroy();\\n    }\\n    instance.elements.container.remove();\\n  };\\n  class Ads {\\n    /**\\n     * Ads constructor.\\n     * @param {Object} player\\n     * @return {Ads}\\n     */\\n    constructor(player) {\\n      /**\\n       * Load the IMA SDK\\n       */\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Check if the Google IMA3 SDK is loaded or load it ourselves\\n        if (!is.object(window.google) || !is.object(window.google.ima)) {\\n          loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n            this.ready();\\n          }).catch(() => {\\n            // Script failed to load or is blocked\\n            this.trigger('error', new Error('Google IMA SDK failed to load'));\\n          });\\n        } else {\\n          this.ready();\\n        }\\n      });\\n      /**\\n       * Get the ads instance ready\\n       */\\n      _defineProperty$1(this, \\\"ready\\\", () => {\\n        // Double check we're enabled\\n        if (!this.enabled) {\\n          destroy(this);\\n        }\\n\\n        // Start ticking our safety timer. If the whole advertisement\\n        // thing doesn't resolve within our set time; we bail\\n        this.startSafetyTimer(12000, 'ready()');\\n\\n        // Clear the safety timer\\n        this.managerPromise.then(() => {\\n          this.clearSafetyTimer('onAdsManagerLoaded()');\\n        });\\n\\n        // Set listeners on the Plyr instance\\n        this.listeners();\\n\\n        // Setup the IMA SDK\\n        this.setupIMA();\\n      });\\n      /**\\n       * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n       * so here we define our ad container. This div is set up to render on top of the video player.\\n       * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n       * handle to the content video player - the SDK will poll the current time of our player to\\n       * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n       * mobile devices, this initialization is done as the result of a user action.\\n       */\\n      _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n        // Create the container for our advertisements\\n        this.elements.container = createElement('div', {\\n          class: this.player.config.classNames.ads\\n        });\\n        this.player.elements.container.appendChild(this.elements.container);\\n\\n        // So we can run VPAID2\\n        google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n        // Set language\\n        google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n        // Set playback for iOS10+\\n        google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n        // We assume the adContainer is the video container of the plyr element that will house the ads\\n        this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n        // Create ads loader\\n        this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n        // Listen and respond to ads loaded and error events\\n        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n        // Request video ads to be pre-loaded\\n        this.requestAds();\\n      });\\n      /**\\n       * Request advertisements\\n       */\\n      _defineProperty$1(this, \\\"requestAds\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        try {\\n          // Request video ads\\n          const request = new google.ima.AdsRequest();\\n          request.adTagUrl = this.tagUrl;\\n\\n          // Specify the linear and nonlinear slot sizes. This helps the SDK\\n          // to select the correct creative if multiple are returned\\n          request.linearAdSlotWidth = container.offsetWidth;\\n          request.linearAdSlotHeight = container.offsetHeight;\\n          request.nonLinearAdSlotWidth = container.offsetWidth;\\n          request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n          // We only overlay ads as we only support video.\\n          request.forceNonLinearFullSlot = false;\\n\\n          // Mute based on current state\\n          request.setAdWillPlayMuted(!this.player.muted);\\n          this.loader.requestAds(request);\\n        } catch (error) {\\n          this.onAdError(error);\\n        }\\n      });\\n      /**\\n       * Update the ad countdown\\n       * @param {Boolean} start\\n       */\\n      _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n        if (!start) {\\n          clearInterval(this.countdownTimer);\\n          this.elements.container.removeAttribute('data-badge-text');\\n          return;\\n        }\\n        const update = () => {\\n          const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n          const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n          this.elements.container.setAttribute('data-badge-text', label);\\n        };\\n        this.countdownTimer = setInterval(update, 100);\\n      });\\n      /**\\n       * This method is called whenever the ads are ready inside the AdDisplayContainer\\n       * @param {Event} event - adsManagerLoadedEvent\\n       */\\n      _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n        // Load could occur after a source change (race condition)\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Get the ads manager\\n        const settings = new google.ima.AdsRenderingSettings();\\n\\n        // Tell the SDK to save and restore content video state on our behalf\\n        settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n        settings.enablePreloading = true;\\n\\n        // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n        // so it can determine when to start the mid- and post-roll\\n        this.manager = event.getAdsManager(this.player, settings);\\n\\n        // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n        this.cuePoints = this.manager.getCuePoints();\\n\\n        // Add listeners to the required events\\n        // Advertisement error events\\n        this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n        // Advertisement regular events\\n        Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n          this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n        });\\n\\n        // Resolve our adsManager\\n        this.trigger('loaded');\\n      });\\n      _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n        // Add advertisement cue's within the time line if available\\n        if (!is.empty(this.cuePoints)) {\\n          this.cuePoints.forEach(cuePoint => {\\n            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n              const seekElement = this.player.elements.progress;\\n              if (is.element(seekElement)) {\\n                const cuePercentage = 100 / this.player.duration * cuePoint;\\n                const cue = createElement('span', {\\n                  class: this.player.config.classNames.cues\\n                });\\n                cue.style.left = `${cuePercentage.toString()}%`;\\n                seekElement.appendChild(cue);\\n              }\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n       * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n       * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n        // don't have ad object associated\\n        const ad = event.getAd();\\n        const adData = event.getAdData();\\n\\n        // Proxy event\\n        const dispatchEvent = type => {\\n          triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n        };\\n\\n        // Bubble the event\\n        dispatchEvent(event.type);\\n        switch (event.type) {\\n          case google.ima.AdEvent.Type.LOADED:\\n            // This is the first event sent for an ad - it is possible to determine whether the\\n            // ad is a video ad or an overlay\\n            this.trigger('loaded');\\n\\n            // Start countdown\\n            this.pollCountdown(true);\\n            if (!ad.isLinear()) {\\n              // Position AdDisplayContainer correctly for overlay\\n              ad.width = container.offsetWidth;\\n              ad.height = container.offsetHeight;\\n            }\\n\\n            // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n            break;\\n          case google.ima.AdEvent.Type.STARTED:\\n            // Set volume to match player\\n            this.manager.setVolume(this.player.volume);\\n            break;\\n          case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n            // All ads for the current videos are done. We can now request new advertisements\\n            // in case the video is re-played\\n\\n            // TODO: Example for what happens when a next video in a playlist would be loaded.\\n            // So here we load a new video when all ads are done.\\n            // Then we load new ads within a new adsManager. When the video\\n            // Is started - after - the ads are loaded, then we get ads.\\n            // You can also easily test cancelling and reloading by running\\n            // player.ads.cancel() and player.ads.play from the console I guess.\\n            // this.player.source = {\\n            //     type: 'video',\\n            //     title: 'View From A Blue Moon',\\n            //     sources: [{\\n            //         src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n            // 'video/mp4', }], poster:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n            // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n            // };\\n\\n            // TODO: So there is still this thing where a video should only be allowed to start\\n            // playing when the IMA SDK is ready or has failed\\n\\n            if (this.player.ended) {\\n              this.loadAds();\\n            } else {\\n              // The SDK won't allow new ads to be called without receiving a contentComplete()\\n              this.loader.contentComplete();\\n            }\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n            // This event indicates the ad has started - the video player can adjust the UI,\\n            // for example display a pause button and remaining time. Fired when content should\\n            // be paused. This usually happens right before an ad is about to cover the content\\n\\n            this.pauseContent();\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n            // This event indicates the ad has finished - the video player can perform\\n            // appropriate UI actions, such as removing the timer for remaining time detection.\\n            // Fired when content should be resumed. This usually happens when an ad finishes\\n            // or collapses\\n\\n            this.pollCountdown();\\n            this.resumeContent();\\n            break;\\n          case google.ima.AdEvent.Type.LOG:\\n            if (adData.adError) {\\n              this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n            }\\n            break;\\n        }\\n      });\\n      /**\\n       * Any ad error handling comes through here\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdError\\\", event => {\\n        this.cancel();\\n        this.player.debug.warn('Ads error', event);\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events. This ensures\\n       * the mid- and post-roll launch at the correct time. And\\n       * resize the advertisement when the player resizes\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        let time;\\n        this.player.on('canplay', () => {\\n          this.addCuePoints();\\n        });\\n        this.player.on('ended', () => {\\n          this.loader.contentComplete();\\n        });\\n        this.player.on('timeupdate', () => {\\n          time = this.player.currentTime;\\n        });\\n        this.player.on('seeked', () => {\\n          const seekedTime = this.player.currentTime;\\n          if (is.empty(this.cuePoints)) {\\n            return;\\n          }\\n          this.cuePoints.forEach((cuePoint, index) => {\\n            if (time < cuePoint && cuePoint < seekedTime) {\\n              this.manager.discardAdBreak();\\n              this.cuePoints.splice(index, 1);\\n            }\\n          });\\n        });\\n\\n        // Listen to the resizing of the window. And resize ad accordingly\\n        // TODO: eventually implement ResizeObserver\\n        window.addEventListener('resize', () => {\\n          if (this.manager) {\\n            this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n          }\\n        });\\n      });\\n      /**\\n       * Initialize the adsManager and start playing advertisements\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        if (!this.managerPromise) {\\n          this.resumeContent();\\n        }\\n\\n        // Play the requested advertisement whenever the adsManager is ready\\n        this.managerPromise.then(() => {\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n\\n          // Initialize the container. Must be done via a user action on mobile devices\\n          this.elements.displayContainer.initialize();\\n          try {\\n            if (!this.initialized) {\\n              // Initialize the ads manager. Ad rules playlist will start at this time\\n              this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n              // Call play to start showing the ad. Single video and overlay ads will\\n              // start at this time; the call will be ignored for ad rules\\n              this.manager.start();\\n            }\\n            this.initialized = true;\\n          } catch (adError) {\\n            // An error may be thrown if there was a problem with the\\n            // VAST response\\n            this.onAdError(adError);\\n          }\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Resume our video\\n       */\\n      _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n        // Hide the advertisement container\\n        this.elements.container.style.zIndex = '';\\n\\n        // Ad is stopped\\n        this.playing = false;\\n\\n        // Play video\\n        silencePromise(this.player.media.play());\\n      });\\n      /**\\n       * Pause our video\\n       */\\n      _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n        // Show the advertisement container\\n        this.elements.container.style.zIndex = 3;\\n\\n        // Ad is playing\\n        this.playing = true;\\n\\n        // Pause our video.\\n        this.player.media.pause();\\n      });\\n      /**\\n       * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n       * allowed to call new ads based on google policies, as they interpret this as an accidental\\n       * video requests. https://developers.google.com/interactive-\\n       * media-ads/docs/sdks/android/faq#8\\n       */\\n      _defineProperty$1(this, \\\"cancel\\\", () => {\\n        // Pause our video\\n        if (this.initialized) {\\n          this.resumeContent();\\n        }\\n\\n        // Tell our instance that we're done for now\\n        this.trigger('error');\\n\\n        // Re-create our adsManager\\n        this.loadAds();\\n      });\\n      /**\\n       * Re-create our adsManager\\n       */\\n      _defineProperty$1(this, \\\"loadAds\\\", () => {\\n        // Tell our adsManager to go bye bye\\n        this.managerPromise.then(() => {\\n          // Destroy our adsManager\\n          if (this.manager) {\\n            this.manager.destroy();\\n          }\\n\\n          // Re-set our adsManager promises\\n          this.managerPromise = new Promise(resolve => {\\n            this.on('loaded', resolve);\\n            this.player.debug.log(this.manager);\\n          });\\n          // Now that the manager has been destroyed set it to also be un-initialized\\n          this.initialized = false;\\n\\n          // Now request some new advertisements\\n          this.requestAds();\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Handles callbacks after an ad event was invoked\\n       * @param {String} event - Event type\\n       * @param args\\n       */\\n      _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n        const handlers = this.events[event];\\n        if (is.array(handlers)) {\\n          handlers.forEach(handler => {\\n            if (is.function(handler)) {\\n              handler.apply(this, args);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       * @return {Ads}\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        if (!is.array(this.events[event])) {\\n          this.events[event] = [];\\n        }\\n        this.events[event].push(callback);\\n        return this;\\n      });\\n      /**\\n       * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n       * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n       * advertisement is playing, or when a user action is required to start, then we clear the\\n       * timer on ad ready\\n       * @param {Number} time\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n        this.player.debug.log(`Safety timer invoked from: ${from}`);\\n        this.safetyTimer = setTimeout(() => {\\n          this.cancel();\\n          this.clearSafetyTimer('startSafetyTimer()');\\n        }, time);\\n      });\\n      /**\\n       * Clear our safety timer(s)\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n        if (!is.nullOrUndefined(this.safetyTimer)) {\\n          this.player.debug.log(`Safety timer cleared from: ${from}`);\\n          clearTimeout(this.safetyTimer);\\n          this.safetyTimer = null;\\n        }\\n      });\\n      this.player = player;\\n      this.config = player.config.ads;\\n      this.playing = false;\\n      this.initialized = false;\\n      this.elements = {\\n        container: null,\\n        displayContainer: null\\n      };\\n      this.manager = null;\\n      this.loader = null;\\n      this.cuePoints = null;\\n      this.events = {};\\n      this.safetyTimer = null;\\n      this.countdownTimer = null;\\n\\n      // Setup a promise to resolve when the IMA manager is ready\\n      this.managerPromise = new Promise((resolve, reject) => {\\n        // The ad is loaded and ready\\n        this.on('loaded', resolve);\\n\\n        // Ads failed\\n        this.on('error', reject);\\n      });\\n      this.load();\\n    }\\n    get enabled() {\\n      const {\\n        config\\n      } = this;\\n      return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n    }\\n    // Build the tag URL\\n    get tagUrl() {\\n      const {\\n        config\\n      } = this;\\n      if (is.url(config.tagUrl)) {\\n        return config.tagUrl;\\n      }\\n      const params = {\\n        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n        AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n        AV_URL: window.location.hostname,\\n        cb: Date.now(),\\n        AV_WIDTH: 640,\\n        AV_HEIGHT: 480,\\n        AV_CDIM2: config.publisherId\\n      };\\n      const base = 'https://go.aniview.com/api/adserver6/vast/';\\n      return `${base}?${buildUrlParams(params)}`;\\n    }\\n  }\\n\\n  /**\\n   * Returns a number whose value is limited to the given range.\\n   *\\n   * Example: limit the output of this computation to between 0 and 255\\n   * (x * 255).clamp(0, 255)\\n   *\\n   * @param {Number} input\\n   * @param {Number} min The lower boundary of the output range\\n   * @param {Number} max The upper boundary of the output range\\n   * @returns A number within the bounds of min and max\\n   * @type Number\\n   */\\n  function clamp(input = 0, min = 0, max = 255) {\\n    return Math.min(Math.max(input, min), max);\\n  }\\n\\n  // Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\n  const parseVtt = vttDataString => {\\n    const processedList = [];\\n    const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n    frames.forEach(frame => {\\n      const result = {};\\n      const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n      lines.forEach(line => {\\n        if (!is.number(result.startTime)) {\\n          // The line with start and end times on it is the first line of interest\\n          const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n          if (matchTimes) {\\n            result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n            result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n          }\\n        } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n          // If we already have the startTime, then we're definitely up to the text line(s)\\n          const lineSplit = line.trim().split('#xywh=');\\n          [result.text] = lineSplit;\\n\\n          // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n          if (lineSplit[1]) {\\n            [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n          }\\n        }\\n      });\\n      if (result.text) {\\n        processedList.push(result);\\n      }\\n    });\\n    return processedList;\\n  };\\n\\n  /**\\n   * Preview thumbnails for seek hover and scrubbing\\n   * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n   * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n   *\\n   * Notes:\\n   * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n   * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n   * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n   */\\n\\n  const fitRatio = (ratio, outer) => {\\n    const targetRatio = outer.width / outer.height;\\n    const result = {};\\n    if (ratio > targetRatio) {\\n      result.width = outer.width;\\n      result.height = 1 / ratio * outer.width;\\n    } else {\\n      result.height = outer.height;\\n      result.width = ratio * outer.height;\\n    }\\n    return result;\\n  };\\n  class PreviewThumbnails {\\n    /**\\n     * PreviewThumbnails constructor.\\n     * @param {Plyr} player\\n     * @return {PreviewThumbnails}\\n     */\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        // Toggle the regular seek tooltip\\n        if (this.player.elements.display.seekTooltip) {\\n          this.player.elements.display.seekTooltip.hidden = this.enabled;\\n        }\\n        if (!this.enabled) return;\\n        this.getThumbnails().then(() => {\\n          if (!this.enabled) {\\n            return;\\n          }\\n\\n          // Render DOM elements\\n          this.render();\\n\\n          // Check to see if thumb container size was specified manually in CSS\\n          this.determineContainerAutoSizing();\\n\\n          // Set up listeners\\n          this.listeners();\\n          this.loaded = true;\\n        });\\n      });\\n      // Download VTT files and parse them\\n      _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n        return new Promise(resolve => {\\n          const {\\n            src\\n          } = this.player.config.previewThumbnails;\\n          if (is.empty(src)) {\\n            throw new Error('Missing previewThumbnails.src config attribute');\\n          }\\n\\n          // Resolve promise\\n          const sortAndResolve = () => {\\n            // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n            this.thumbnails.sort((x, y) => x.height - y.height);\\n            this.player.debug.log('Preview thumbnails', this.thumbnails);\\n            resolve();\\n          };\\n\\n          // Via callback()\\n          if (is.function(src)) {\\n            src(thumbnails => {\\n              this.thumbnails = thumbnails;\\n              sortAndResolve();\\n            });\\n          }\\n          // VTT urls\\n          else {\\n            // If string, convert into single-element list\\n            const urls = is.string(src) ? [src] : src;\\n            // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n            const promises = urls.map(u => this.getThumbnail(u));\\n            // Resolve\\n            Promise.all(promises).then(sortAndResolve);\\n          }\\n        });\\n      });\\n      // Process individual VTT file\\n      _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n        return new Promise(resolve => {\\n          fetch(url).then(response => {\\n            const thumbnail = {\\n              frames: parseVtt(response),\\n              height: null,\\n              urlPrefix: ''\\n            };\\n\\n            // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n            // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n            // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n            if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n              thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n            }\\n\\n            // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n            const tempImage = new Image();\\n            tempImage.onload = () => {\\n              thumbnail.height = tempImage.naturalHeight;\\n              thumbnail.width = tempImage.naturalWidth;\\n              this.thumbnails.push(thumbnail);\\n              resolve();\\n            };\\n            tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n          });\\n        });\\n      });\\n      _defineProperty$1(this, \\\"startMove\\\", event => {\\n        if (!this.loaded) return;\\n        if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n        // Wait until media has a duration\\n        if (!this.player.media.duration) return;\\n        if (event.type === 'touchmove') {\\n          // Calculate seek hover position as approx video seconds\\n          this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n        } else {\\n          var _this$player$config$m, _this$player$config$m2;\\n          // Calculate seek hover position as approx video seconds\\n          const clientRect = this.player.elements.progress.getBoundingClientRect();\\n          const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n          this.seekTime = this.player.media.duration * (percentage / 100);\\n          if (this.seekTime < 0) {\\n            // The mousemove fires for 10+px out to the left\\n            this.seekTime = 0;\\n          }\\n          if (this.seekTime > this.player.media.duration - 1) {\\n            // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n            this.seekTime = this.player.media.duration - 1;\\n          }\\n          this.mousePosX = event.pageX;\\n\\n          // Set time text inside image container\\n          this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n          // Get marker point for time\\n          const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n            time: t\\n          }) => t === Math.round(this.seekTime));\\n\\n          // Append the point label to the tooltip\\n          if (point) {\\n            // this.elements.thumb.time.innerText.concat('\\\\n');\\n            this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n          }\\n        }\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      });\\n      _defineProperty$1(this, \\\"endMove\\\", () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n        // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n        if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n          this.mouseDown = true;\\n\\n          // Wait until media has a duration\\n          if (this.player.media.duration) {\\n            this.toggleScrubbingContainer(true);\\n            this.toggleThumbContainer(false, true);\\n\\n            // Download and show image\\n            this.showImageAtCurrentTime();\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n        this.mouseDown = false;\\n\\n        // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n        if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n          // The video was already seeked/loaded at the chosen time - hide immediately\\n          this.toggleScrubbingContainer(false);\\n        } else {\\n          // The video hasn't seeked yet. Wait for that\\n          once.call(this.player, this.player.media, 'timeupdate', () => {\\n            // Re-check mousedown - we might have already started scrubbing again\\n            if (!this.mouseDown) {\\n              this.toggleScrubbingContainer(false);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n        this.player.on('play', () => {\\n          this.toggleThumbContainer(false, true);\\n        });\\n        this.player.on('seeked', () => {\\n          this.toggleThumbContainer(false);\\n        });\\n        this.player.on('timeupdate', () => {\\n          this.lastTime = this.player.media.currentTime;\\n        });\\n      });\\n      /**\\n       * Create HTML elements for image containers\\n       */\\n      _defineProperty$1(this, \\\"render\\\", () => {\\n        // Create HTML element: plyr__preview-thumbnail-container\\n        this.elements.thumb.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.thumbContainer\\n        });\\n\\n        // Wrapper for the image for styling\\n        this.elements.thumb.imageContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.imageContainer\\n        });\\n        this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n        // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n        const timeContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.timeContainer\\n        });\\n        this.elements.thumb.time = createElement('span', {}, '00:00');\\n        timeContainer.appendChild(this.elements.thumb.time);\\n        this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n        // Inject the whole thumb\\n        if (is.element(this.player.elements.progress)) {\\n          this.player.elements.progress.appendChild(this.elements.thumb.container);\\n        }\\n\\n        // Create HTML element: plyr__preview-scrubbing-container\\n        this.elements.scrubbing.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n        });\\n        this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n      });\\n      _defineProperty$1(this, \\\"destroy\\\", () => {\\n        if (this.elements.thumb.container) {\\n          this.elements.thumb.container.remove();\\n        }\\n        if (this.elements.scrubbing.container) {\\n          this.elements.scrubbing.container.remove();\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n        if (this.mouseDown) {\\n          this.setScrubbingContainerSize();\\n        } else {\\n          this.setThumbContainerSizeAndPos();\\n        }\\n\\n        // Find the desired thumbnail index\\n        // TODO: Handle a video longer than the thumbs where thumbNum is null\\n        const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n        const hasThumb = thumbNum >= 0;\\n        let qualityIndex = 0;\\n\\n        // Show the thumb container if we're not scrubbing\\n        if (!this.mouseDown) {\\n          this.toggleThumbContainer(hasThumb);\\n        }\\n\\n        // No matching thumb found\\n        if (!hasThumb) {\\n          return;\\n        }\\n\\n        // Check to see if we've already downloaded higher quality versions of this image\\n        this.thumbnails.forEach((thumbnail, index) => {\\n          if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n            qualityIndex = index;\\n          }\\n        });\\n\\n        // Only proceed if either thumb num or thumbfilename has changed\\n        if (thumbNum !== this.showingThumb) {\\n          this.showingThumb = thumbNum;\\n          this.loadImage(qualityIndex);\\n        }\\n      });\\n      // Show the image that's currently specified in this.showingThumb\\n      _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n        const thumbNum = this.showingThumb;\\n        const thumbnail = this.thumbnails[qualityIndex];\\n        const {\\n          urlPrefix\\n        } = thumbnail;\\n        const frame = thumbnail.frames[thumbNum];\\n        const thumbFilename = thumbnail.frames[thumbNum].text;\\n        const thumbUrl = urlPrefix + thumbFilename;\\n        if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n          // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n          // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n          if (this.loadingImage && this.usingSprites) {\\n            this.loadingImage.onload = null;\\n          }\\n\\n          // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n          // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n          // images causes a flicker. Putting a new image over the top does not\\n          const previewImage = new Image();\\n          previewImage.src = thumbUrl;\\n          previewImage.dataset.index = thumbNum;\\n          previewImage.dataset.filename = thumbFilename;\\n          this.showingThumbFilename = thumbFilename;\\n          this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n          // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n          previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n          this.loadingImage = previewImage;\\n          this.removeOldImages(previewImage);\\n        } else {\\n          // Update the existing image\\n          this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n          this.currentImageElement.dataset.index = thumbNum;\\n          this.removeOldImages(this.currentImageElement);\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n        this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n        this.setImageSizeAndOffset(previewImage, frame);\\n        if (newImage) {\\n          this.currentImageContainer.appendChild(previewImage);\\n          this.currentImageElement = previewImage;\\n          if (!this.loadedImages.includes(thumbFilename)) {\\n            this.loadedImages.push(thumbFilename);\\n          }\\n        }\\n\\n        // Preload images before and after the current one\\n        // Show higher quality of the same frame\\n        // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n        this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n      });\\n      // Remove all preview images that aren't the designated current image\\n      _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n        // Get a list of all images, convert it from a DOM list to an array\\n        Array.from(this.currentImageContainer.children).forEach(image => {\\n          if (image.tagName.toLowerCase() !== 'img') {\\n            return;\\n          }\\n          const removeDelay = this.usingSprites ? 500 : 1000;\\n          if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n            // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n            // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n            // eslint-disable-next-line no-param-reassign\\n            image.dataset.deleting = true;\\n\\n            // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n            const {\\n              currentImageContainer\\n            } = this;\\n            setTimeout(() => {\\n              currentImageContainer.removeChild(image);\\n              this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n            }, removeDelay);\\n          }\\n        });\\n      });\\n      // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n      // This will only preload the lowest quality\\n      _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n        return new Promise(resolve => {\\n          setTimeout(() => {\\n            const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n            if (this.showingThumbFilename === oldThumbFilename) {\\n              // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n              let thumbnailsClone;\\n              if (forward) {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n              } else {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n              }\\n              let foundOne = false;\\n              thumbnailsClone.forEach(frame => {\\n                const newThumbFilename = frame.text;\\n                if (newThumbFilename !== oldThumbFilename) {\\n                  // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                  if (!this.loadedImages.includes(newThumbFilename)) {\\n                    foundOne = true;\\n                    this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                    const {\\n                      urlPrefix\\n                    } = this.thumbnails[0];\\n                    const thumbURL = urlPrefix + newThumbFilename;\\n                    const previewImage = new Image();\\n                    previewImage.src = thumbURL;\\n                    previewImage.onload = () => {\\n                      this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                      if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                      // We don't resolve until the thumb is loaded\\n                      resolve();\\n                    };\\n                  }\\n                }\\n              });\\n\\n              // If there are none to preload then we want to resolve immediately\\n              if (!foundOne) {\\n                resolve();\\n              }\\n            }\\n          }, 300);\\n        });\\n      });\\n      // If user has been hovering current image for half a second, look for a higher quality one\\n      _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n        if (currentQualityIndex < this.thumbnails.length - 1) {\\n          // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n          let previewImageHeight = previewImage.naturalHeight;\\n          if (this.usingSprites) {\\n            previewImageHeight = frame.h;\\n          }\\n          if (previewImageHeight < this.thumbContainerHeight) {\\n            // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n            setTimeout(() => {\\n              // Make sure the mouse hasn't already moved on and started hovering at another image\\n              if (this.showingThumbFilename === thumbFilename) {\\n                this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n                this.loadImage(currentQualityIndex + 1);\\n              }\\n            }, 300);\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n        this.elements.thumb.container.classList.toggle(className, toggle);\\n        if (!toggle && clearShowing) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n        this.elements.scrubbing.container.classList.toggle(className, toggle);\\n        if (!toggle) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n        if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n          // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n          this.sizeSpecifiedInCSS = true;\\n        }\\n      });\\n      // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n      _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n        const {\\n          imageContainer\\n        } = this.elements.thumb;\\n        if (!this.sizeSpecifiedInCSS) {\\n          const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n          imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n          const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n          const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n          imageContainer.style.height = `${thumbHeight}px`;\\n        }\\n        this.setThumbContainerPos();\\n      });\\n      _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n        const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n        const containerRect = this.player.elements.container.getBoundingClientRect();\\n        const {\\n          container\\n        } = this.elements.thumb;\\n        // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n        const min = containerRect.left - scrubberRect.left + 10;\\n        const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n        // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n        const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n        const clamped = clamp(position, min, max);\\n\\n        // Move the popover position\\n        container.style.left = `${clamped}px`;\\n\\n        // The arrow can follow the cursor\\n        container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n      });\\n      // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n      _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n        const {\\n          width,\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        this.elements.scrubbing.container.style.width = `${width}px`;\\n        this.elements.scrubbing.container.style.height = `${height}px`;\\n      });\\n      // Sprites need to be offset to the correct location\\n      _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n        if (!this.usingSprites) return;\\n\\n        // Find difference between height and preview container height\\n        const multiplier = this.thumbContainerHeight / frame.h;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.left = `-${frame.x * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.top = `-${frame.y * multiplier}px`;\\n      });\\n      this.player = player;\\n      this.thumbnails = [];\\n      this.loaded = false;\\n      this.lastMouseMoveTime = Date.now();\\n      this.mouseDown = false;\\n      this.loadedImages = [];\\n      this.elements = {\\n        thumb: {},\\n        scrubbing: {}\\n      };\\n      this.load();\\n    }\\n    get enabled() {\\n      return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n    }\\n    get currentImageContainer() {\\n      return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n    }\\n    get usingSprites() {\\n      return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n    }\\n    get thumbAspectRatio() {\\n      if (this.usingSprites) {\\n        return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n      }\\n      return this.thumbnails[0].width / this.thumbnails[0].height;\\n    }\\n    get thumbContainerHeight() {\\n      if (this.mouseDown) {\\n        const {\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        return height;\\n      }\\n\\n      // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n      if (this.sizeSpecifiedInCSS) {\\n        return this.elements.thumb.imageContainer.clientHeight;\\n      }\\n      return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n    }\\n    get currentImageElement() {\\n      return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n    }\\n    set currentImageElement(element) {\\n      if (this.mouseDown) {\\n        this.currentScrubbingImageElement = element;\\n      } else {\\n        this.currentThumbnailImageElement = element;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  const source = {\\n    // Add elements to HTML5 media (source, tracks, etc)\\n    insertElements(type, attributes) {\\n      if (is.string(attributes)) {\\n        insertElement(type, this.media, {\\n          src: attributes\\n        });\\n      } else if (is.array(attributes)) {\\n        attributes.forEach(attribute => {\\n          insertElement(type, this.media, attribute);\\n        });\\n      }\\n    },\\n    // Update source\\n    // Sources are not checked for support so be careful\\n    change(input) {\\n      if (!getDeep(input, 'sources.length')) {\\n        this.debug.warn('Invalid source format');\\n        return;\\n      }\\n\\n      // Cancel current network requests\\n      html5.cancelRequests.call(this);\\n\\n      // Destroy instance and re-setup\\n      this.destroy.call(this, () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const {\\n          sources,\\n          type\\n        } = input;\\n        const [{\\n          provider = providers.html5,\\n          src\\n        }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : {\\n          src\\n        };\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes)\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      }, true);\\n    }\\n  };\\n\\n  // Private properties\\n  // TODO: Use a WeakMap for private globals\\n  // const globals = new WeakMap();\\n\\n  // Plyr instance\\n  class Plyr {\\n    constructor(target, options) {\\n      /**\\n       * Play the media, or play the advertisement (if they are not blocked)\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        if (!is.function(this.media.play)) {\\n          return null;\\n        }\\n\\n        // Intecept play with ads\\n        if (this.ads && this.ads.enabled) {\\n          this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n        }\\n\\n        // Return the promise (for HTML5)\\n        return this.media.play();\\n      });\\n      /**\\n       * Pause the media\\n       */\\n      _defineProperty$1(this, \\\"pause\\\", () => {\\n        if (!this.playing || !is.function(this.media.pause)) {\\n          return null;\\n        }\\n        return this.media.pause();\\n      });\\n      /**\\n       * Toggle playback based on current status\\n       * @param {Boolean} input\\n       */\\n      _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n        // Toggle based on current state if nothing passed\\n        const toggle = is.boolean(input) ? input : !this.playing;\\n        if (toggle) {\\n          return this.play();\\n        }\\n        return this.pause();\\n      });\\n      /**\\n       * Stop playback\\n       */\\n      _defineProperty$1(this, \\\"stop\\\", () => {\\n        if (this.isHTML5) {\\n          this.pause();\\n          this.restart();\\n        } else if (is.function(this.media.stop)) {\\n          this.media.stop();\\n        }\\n      });\\n      /**\\n       * Restart playback\\n       */\\n      _defineProperty$1(this, \\\"restart\\\", () => {\\n        this.currentTime = 0;\\n      });\\n      /**\\n       * Rewind\\n       * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n        this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Fast forward\\n       * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n        this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Increase volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n        const volume = this.media.muted ? 0 : this.volume;\\n        this.volume = volume + (is.number(step) ? step : 0);\\n      });\\n      /**\\n       * Decrease volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n        this.increaseVolume(-step);\\n      });\\n      /**\\n       * Trigger the airplay dialog\\n       * TODO: update player with state, support, enabled\\n       */\\n      _defineProperty$1(this, \\\"airplay\\\", () => {\\n        // Show dialog if supported\\n        if (support.airplay) {\\n          this.media.webkitShowPlaybackTargetPicker();\\n        }\\n      });\\n      /**\\n       * Toggle the player controls\\n       * @param {Boolean} [toggle] - Whether to show the controls\\n       */\\n      _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n        // Don't toggle if missing UI support or if it's audio\\n        if (this.supported.ui && !this.isAudio) {\\n          // Get state before change\\n          const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n          // Negate the argument if not undefined since adding the class to hides the controls\\n          const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n          // Apply and get updated state\\n          const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n          // Close menu\\n          if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n            controls.toggleMenu.call(this, false);\\n          }\\n\\n          // Trigger event on change\\n          if (hiding !== isHidden) {\\n            const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n            triggerEvent.call(this, this.media, eventName);\\n          }\\n          return !hiding;\\n        }\\n        return false;\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        on.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Add event listeners once\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n        once.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Remove event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n        off(this.elements.container, event, callback);\\n      });\\n      /**\\n       * Destroy an instance\\n       * Event listeners are removed when elements are removed\\n       * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n       * @param {Function} callback - Callback for when destroy is complete\\n       * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n       */\\n      _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n        if (!this.ready) {\\n          return;\\n        }\\n        const done = () => {\\n          // Reset overflow (incase destroyed while in fullscreen)\\n          document.body.style.overflow = '';\\n\\n          // GC for embed\\n          this.embed = null;\\n\\n          // If it's a soft destroy, make minimal changes\\n          if (soft) {\\n            if (Object.keys(this.elements).length) {\\n              // Remove elements\\n              removeElement(this.elements.buttons.play);\\n              removeElement(this.elements.captions);\\n              removeElement(this.elements.controls);\\n              removeElement(this.elements.wrapper);\\n\\n              // Clear for GC\\n              this.elements.buttons.play = null;\\n              this.elements.captions = null;\\n              this.elements.controls = null;\\n              this.elements.wrapper = null;\\n            }\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback();\\n            }\\n          } else {\\n            // Unbind listeners\\n            unbindListeners.call(this);\\n\\n            // Cancel current network requests\\n            html5.cancelRequests.call(this);\\n\\n            // Replace the container with the original element provided\\n            replaceElement(this.elements.original, this.elements.container);\\n\\n            // Event\\n            triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback.call(this.elements.original);\\n            }\\n\\n            // Reset state\\n            this.ready = false;\\n\\n            // Clear for garbage collection\\n            setTimeout(() => {\\n              this.elements = null;\\n              this.media = null;\\n            }, 200);\\n          }\\n        };\\n\\n        // Stop playback\\n        this.stop();\\n\\n        // Clear timeouts\\n        clearTimeout(this.timers.loading);\\n        clearTimeout(this.timers.controls);\\n        clearTimeout(this.timers.resized);\\n\\n        // Provider specific stuff\\n        if (this.isHTML5) {\\n          // Restore native video controls\\n          ui.toggleNativeControls.call(this, true);\\n\\n          // Clean up\\n          done();\\n        } else if (this.isYouTube) {\\n          // Clear timers\\n          clearInterval(this.timers.buffering);\\n          clearInterval(this.timers.playing);\\n\\n          // Destroy YouTube API\\n          if (this.embed !== null && is.function(this.embed.destroy)) {\\n            this.embed.destroy();\\n          }\\n\\n          // Clean up\\n          done();\\n        } else if (this.isVimeo) {\\n          // Destroy Vimeo API\\n          // then clean up (wait, to prevent postmessage errors)\\n          if (this.embed !== null) {\\n            this.embed.unload().then(done);\\n          }\\n\\n          // Vimeo does not always return\\n          setTimeout(done, 200);\\n        }\\n      });\\n      /**\\n       * Check for support for a mime type (HTML5 only)\\n       * @param {String} type - Mime type\\n       */\\n      _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n      this.timers = {};\\n\\n      // State\\n      this.ready = false;\\n      this.loading = false;\\n      this.failed = false;\\n\\n      // Touch device\\n      this.touch = support.touch;\\n\\n      // Set the media element\\n      this.media = target;\\n\\n      // String selector passed\\n      if (is.string(this.media)) {\\n        this.media = document.querySelectorAll(this.media);\\n      }\\n\\n      // jQuery, NodeList or Array passed, use first element\\n      if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n        // eslint-disable-next-line\\n        this.media = this.media[0];\\n      }\\n\\n      // Set config\\n      this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })());\\n\\n      // Elements cache\\n      this.elements = {\\n        container: null,\\n        fullscreen: null,\\n        captions: null,\\n        buttons: {},\\n        display: {},\\n        progress: {},\\n        inputs: {},\\n        settings: {\\n          popup: null,\\n          menu: null,\\n          panels: {},\\n          buttons: {}\\n        }\\n      };\\n\\n      // Captions\\n      this.captions = {\\n        active: null,\\n        currentTrack: -1,\\n        meta: new WeakMap()\\n      };\\n\\n      // Fullscreen\\n      this.fullscreen = {\\n        active: false\\n      };\\n\\n      // Options\\n      this.options = {\\n        speed: [],\\n        quality: []\\n      };\\n\\n      // Debugging\\n      // TODO: move to globals\\n      this.debug = new Console(this.config.debug);\\n\\n      // Log config options and support\\n      this.debug.log('Config', this.config);\\n      this.debug.log('Support', support);\\n\\n      // We need an element to setup\\n      if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n        this.debug.error('Setup failed: no suitable element passed');\\n        return;\\n      }\\n\\n      // Bail if the element is initialized\\n      if (this.media.plyr) {\\n        this.debug.warn('Target already setup');\\n        return;\\n      }\\n\\n      // Bail if not enabled\\n      if (!this.config.enabled) {\\n        this.debug.error('Setup failed: disabled by config');\\n        return;\\n      }\\n\\n      // Bail if disabled or no basic support\\n      // You may want to disable certain UAs etc\\n      if (!support.check().api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n\\n      // Cache original element state for .destroy()\\n      const clone = this.media.cloneNode(true);\\n      clone.autoplay = false;\\n      this.elements.original = clone;\\n\\n      // Set media type based on tag or data attribute\\n      // Supported: video, audio, vimeo, youtube\\n      const _type = this.media.tagName.toLowerCase();\\n      // Embed properties\\n      let iframe = null;\\n      let url = null;\\n\\n      // Different setup based on type\\n      switch (_type) {\\n        case 'div':\\n          // Find the frame\\n          iframe = this.media.querySelector('iframe');\\n\\n          // <iframe> type\\n          if (is.element(iframe)) {\\n            // Detect provider\\n            url = parseUrl(iframe.getAttribute('src'));\\n            this.provider = getProviderByUrl(url.toString());\\n\\n            // Rework elements\\n            this.elements.container = this.media;\\n            this.media = iframe;\\n\\n            // Reset classname\\n            this.elements.container.className = '';\\n\\n            // Get attributes from URL and set config\\n            if (url.search.length) {\\n              const truthy = ['1', 'true'];\\n              if (truthy.includes(url.searchParams.get('autoplay'))) {\\n                this.config.autoplay = true;\\n              }\\n              if (truthy.includes(url.searchParams.get('loop'))) {\\n                this.config.loop.active = true;\\n              }\\n\\n              // TODO: replace fullscreen.iosNative with this playsinline config option\\n              // YouTube requires the playsinline in the URL\\n              if (this.isYouTube) {\\n                this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n                this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n              } else {\\n                this.config.playsinline = true;\\n              }\\n            }\\n          } else {\\n            // <div> with attributes\\n            this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n            // Remove attribute\\n            this.media.removeAttribute(this.config.attributes.embed.provider);\\n          }\\n\\n          // Unsupported or missing provider\\n          if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n            this.debug.error('Setup failed: Invalid provider');\\n            return;\\n          }\\n\\n          // Audio will come later for external providers\\n          this.type = types.video;\\n          break;\\n        case 'video':\\n        case 'audio':\\n          this.type = _type;\\n          this.provider = providers.html5;\\n\\n          // Get config from attributes\\n          if (this.media.hasAttribute('crossorigin')) {\\n            this.config.crossorigin = true;\\n          }\\n          if (this.media.hasAttribute('autoplay')) {\\n            this.config.autoplay = true;\\n          }\\n          if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n            this.config.playsinline = true;\\n          }\\n          if (this.media.hasAttribute('muted')) {\\n            this.config.muted = true;\\n          }\\n          if (this.media.hasAttribute('loop')) {\\n            this.config.loop.active = true;\\n          }\\n          break;\\n        default:\\n          this.debug.error('Setup failed: unsupported type');\\n          return;\\n      }\\n\\n      // Check for support again but with type\\n      this.supported = support.check(this.type, this.provider);\\n\\n      // If no support for even API, bail\\n      if (!this.supported.api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n      this.eventListeners = [];\\n\\n      // Create listeners\\n      this.listeners = new Listeners(this);\\n\\n      // Setup local storage for user settings\\n      this.storage = new Storage(this);\\n\\n      // Store reference\\n      this.media.plyr = this;\\n\\n      // Wrap media\\n      if (!is.element(this.elements.container)) {\\n        this.elements.container = createElement('div');\\n        wrap(this.media, this.elements.container);\\n      }\\n\\n      // Migrate custom properties from media to container (so they work 😉)\\n      ui.migrateStyles.call(this);\\n\\n      // Add style hook\\n      ui.addStyleHook.call(this);\\n\\n      // Setup media\\n      media.setup.call(this);\\n\\n      // Listen for events if debugging\\n      if (this.config.debug) {\\n        on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n          this.debug.log(`event: ${event.type}`);\\n        });\\n      }\\n\\n      // Setup fullscreen\\n      this.fullscreen = new Fullscreen(this);\\n\\n      // Setup interface\\n      // If embed but not fully supported, build interface now to avoid flash of controls\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        ui.build.call(this);\\n      }\\n\\n      // Container listeners\\n      this.listeners.container();\\n\\n      // Global listeners\\n      this.listeners.global();\\n\\n      // Setup ads if provided\\n      if (this.config.ads.enabled) {\\n        this.ads = new Ads(this);\\n      }\\n\\n      // Autoplay if required\\n      if (this.isHTML5 && this.config.autoplay) {\\n        this.once('canplay', () => silencePromise(this.play()));\\n      }\\n\\n      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n      this.lastSeekTime = 0;\\n\\n      // Setup preview thumbnails if enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n\\n    // ---------------------------------------\\n    // API\\n    // ---------------------------------------\\n\\n    /**\\n     * Types and provider helpers\\n     */\\n    get isHTML5() {\\n      return this.provider === providers.html5;\\n    }\\n    get isEmbed() {\\n      return this.isYouTube || this.isVimeo;\\n    }\\n    get isYouTube() {\\n      return this.provider === providers.youtube;\\n    }\\n    get isVimeo() {\\n      return this.provider === providers.vimeo;\\n    }\\n    get isVideo() {\\n      return this.type === types.video;\\n    }\\n    get isAudio() {\\n      return this.type === types.audio;\\n    }\\n    /**\\n     * Get playing state\\n     */\\n    get playing() {\\n      return Boolean(this.ready && !this.paused && !this.ended);\\n    }\\n\\n    /**\\n     * Get paused state\\n     */\\n    get paused() {\\n      return Boolean(this.media.paused);\\n    }\\n\\n    /**\\n     * Get stopped state\\n     */\\n    get stopped() {\\n      return Boolean(this.paused && this.currentTime === 0);\\n    }\\n\\n    /**\\n     * Get ended state\\n     */\\n    get ended() {\\n      return Boolean(this.media.ended);\\n    }\\n    /**\\n     * Seek to a time\\n     * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n     */\\n    set currentTime(input) {\\n      // Bail if media duration isn't available yet\\n      if (!this.duration) {\\n        return;\\n      }\\n\\n      // Validate input\\n      const inputIsValid = is.number(input) && input > 0;\\n\\n      // Set\\n      this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n      // Logging\\n      this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n    }\\n\\n    /**\\n     * Get current time\\n     */\\n    get currentTime() {\\n      return Number(this.media.currentTime);\\n    }\\n\\n    /**\\n     * Get buffered\\n     */\\n    get buffered() {\\n      const {\\n        buffered\\n      } = this.media;\\n\\n      // YouTube / Vimeo return a float between 0-1\\n      if (is.number(buffered)) {\\n        return buffered;\\n      }\\n\\n      // HTML5\\n      // TODO: Handle buffered chunks of the media\\n      // (i.e. seek to another section buffers only that section)\\n      if (buffered && buffered.length && this.duration > 0) {\\n        return buffered.end(0) / this.duration;\\n      }\\n      return 0;\\n    }\\n\\n    /**\\n     * Get seeking status\\n     */\\n    get seeking() {\\n      return Boolean(this.media.seeking);\\n    }\\n\\n    /**\\n     * Get the duration of the current media\\n     */\\n    get duration() {\\n      // Faux duration set via config\\n      const fauxDuration = parseFloat(this.config.duration);\\n      // Media duration can be NaN or Infinity before the media has loaded\\n      const realDuration = (this.media || {}).duration;\\n      const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n      // If config duration is funky, use regular duration\\n      return fauxDuration || duration;\\n    }\\n\\n    /**\\n     * Set the player volume\\n     * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n     */\\n    set volume(value) {\\n      let volume = value;\\n      const max = 1;\\n      const min = 0;\\n      if (is.string(volume)) {\\n        volume = Number(volume);\\n      }\\n\\n      // Load volume from storage if no value specified\\n      if (!is.number(volume)) {\\n        volume = this.storage.get('volume');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.number(volume)) {\\n        ({\\n          volume\\n        } = this.config);\\n      }\\n\\n      // Maximum is volumeMax\\n      if (volume > max) {\\n        volume = max;\\n      }\\n      // Minimum is volumeMin\\n      if (volume < min) {\\n        volume = min;\\n      }\\n\\n      // Update config\\n      this.config.volume = volume;\\n\\n      // Set the player volume\\n      this.media.volume = volume;\\n\\n      // If muted, and we're increasing volume manually, reset muted state\\n      if (!is.empty(value) && this.muted && volume > 0) {\\n        this.muted = false;\\n      }\\n    }\\n\\n    /**\\n     * Get the current player volume\\n     */\\n    get volume() {\\n      return Number(this.media.volume);\\n    }\\n    /**\\n     * Set muted state\\n     * @param {Boolean} mute\\n     */\\n    set muted(mute) {\\n      let toggle = mute;\\n\\n      // Load muted state from storage\\n      if (!is.boolean(toggle)) {\\n        toggle = this.storage.get('muted');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.boolean(toggle)) {\\n        toggle = this.config.muted;\\n      }\\n\\n      // Update config\\n      this.config.muted = toggle;\\n\\n      // Set mute on the player\\n      this.media.muted = toggle;\\n    }\\n\\n    /**\\n     * Get current muted state\\n     */\\n    get muted() {\\n      return Boolean(this.media.muted);\\n    }\\n\\n    /**\\n     * Check if the media has audio\\n     */\\n    get hasAudio() {\\n      // Assume yes for all non HTML5 (as we can't tell...)\\n      if (!this.isHTML5) {\\n        return true;\\n      }\\n      if (this.isAudio) {\\n        return true;\\n      }\\n\\n      // Get audio tracks\\n      return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n    }\\n\\n    /**\\n     * Set playback speed\\n     * @param {Number} input - the speed of playback (0.5-2.0)\\n     */\\n    set speed(input) {\\n      let speed = null;\\n      if (is.number(input)) {\\n        speed = input;\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.storage.get('speed');\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.config.speed.selected;\\n      }\\n\\n      // Clamp to min/max\\n      const {\\n        minimumSpeed: min,\\n        maximumSpeed: max\\n      } = this;\\n      speed = clamp(speed, min, max);\\n\\n      // Update config\\n      this.config.speed.selected = speed;\\n\\n      // Set media speed\\n      setTimeout(() => {\\n        if (this.media) {\\n          this.media.playbackRate = speed;\\n        }\\n      }, 0);\\n    }\\n\\n    /**\\n     * Get current playback speed\\n     */\\n    get speed() {\\n      return Number(this.media.playbackRate);\\n    }\\n\\n    /**\\n     * Get the minimum allowed speed\\n     */\\n    get minimumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.min(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 0.5;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 0.0625;\\n    }\\n\\n    /**\\n     * Get the maximum allowed speed\\n     */\\n    get maximumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.max(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 2;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 16;\\n    }\\n\\n    /**\\n     * Set playback quality\\n     * Currently HTML5 & YouTube only\\n     * @param {Number} input - Quality level\\n     */\\n    set quality(input) {\\n      const config = this.config.quality;\\n      const options = this.options.quality;\\n      if (!options.length) {\\n        return;\\n      }\\n      let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n      let updateStorage = true;\\n      if (!options.includes(quality)) {\\n        const value = closest(options, quality);\\n        this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n        quality = value;\\n\\n        // Don't update storage if quality is not supported\\n        updateStorage = false;\\n      }\\n\\n      // Update config\\n      config.selected = quality;\\n\\n      // Set quality\\n      this.media.quality = quality;\\n\\n      // Save to storage\\n      if (updateStorage) {\\n        this.storage.set({\\n          quality\\n        });\\n      }\\n    }\\n\\n    /**\\n     * Get current quality level\\n     */\\n    get quality() {\\n      return this.media.quality;\\n    }\\n\\n    /**\\n     * Toggle loop\\n     * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n     * @param {Boolean} input - Whether to loop or not\\n     */\\n    set loop(input) {\\n      const toggle = is.boolean(input) ? input : this.config.loop.active;\\n      this.config.loop.active = toggle;\\n      this.media.loop = toggle;\\n\\n      // Set default to be a true toggle\\n      /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n           switch (type) {\\n              case 'start':\\n                  if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                      this.config.loop.end = null;\\n                  }\\n                  this.config.loop.start = this.currentTime;\\n                  // this.config.loop.indicator.start = this.elements.display.played.value;\\n                  break;\\n               case 'end':\\n                  if (this.config.loop.start >= this.currentTime) {\\n                      return this;\\n                  }\\n                  this.config.loop.end = this.currentTime;\\n                  // this.config.loop.indicator.end = this.elements.display.played.value;\\n                  break;\\n               case 'all':\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = this.duration - 2;\\n                  this.config.loop.indicator.start = 0;\\n                  this.config.loop.indicator.end = 100;\\n                  break;\\n               case 'toggle':\\n                  if (this.config.loop.active) {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = null;\\n                  } else {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = this.duration - 2;\\n                  }\\n                  break;\\n               default:\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = null;\\n                  break;\\n          } */\\n    }\\n\\n    /**\\n     * Get current loop state\\n     */\\n    get loop() {\\n      return Boolean(this.media.loop);\\n    }\\n\\n    /**\\n     * Set new media source\\n     * @param {Object} input - The new source object (see docs)\\n     */\\n    set source(input) {\\n      source.change.call(this, input);\\n    }\\n\\n    /**\\n     * Get current source\\n     */\\n    get source() {\\n      return this.media.currentSrc;\\n    }\\n\\n    /**\\n     * Get a download URL (either source or custom)\\n     */\\n    get download() {\\n      const {\\n        download\\n      } = this.config.urls;\\n      return is.url(download) ? download : this.source;\\n    }\\n\\n    /**\\n     * Set the download URL\\n     */\\n    set download(input) {\\n      if (!is.url(input)) {\\n        return;\\n      }\\n      this.config.urls.download = input;\\n      controls.setDownloadUrl.call(this);\\n    }\\n\\n    /**\\n     * Set the poster image for a video\\n     * @param {String} input - the URL for the new poster image\\n     */\\n    set poster(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Poster can only be set for video');\\n        return;\\n      }\\n      ui.setPoster.call(this, input, false).catch(() => {});\\n    }\\n\\n    /**\\n     * Get the current poster image\\n     */\\n    get poster() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n    }\\n\\n    /**\\n     * Get the current aspect ratio in use\\n     */\\n    get ratio() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n      return is.array(ratio) ? ratio.join(':') : ratio;\\n    }\\n\\n    /**\\n     * Set video aspect ratio\\n     */\\n    set ratio(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Aspect ratio can only be set for video');\\n        return;\\n      }\\n      if (!is.string(input) || !validateAspectRatio(input)) {\\n        this.debug.error(`Invalid aspect ratio specified (${input})`);\\n        return;\\n      }\\n      this.config.ratio = reduceAspectRatio(input);\\n      setAspectRatio.call(this);\\n    }\\n\\n    /**\\n     * Set the autoplay state\\n     * @param {Boolean} input - Whether to autoplay or not\\n     */\\n    set autoplay(input) {\\n      this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n    }\\n\\n    /**\\n     * Get the current autoplay state\\n     */\\n    get autoplay() {\\n      return Boolean(this.config.autoplay);\\n    }\\n\\n    /**\\n     * Toggle captions\\n     * @param {Boolean} input - Whether to enable captions\\n     */\\n    toggleCaptions(input) {\\n      captions.toggle.call(this, input, false);\\n    }\\n\\n    /**\\n     * Set the caption track by index\\n     * @param {Number} input - Caption index\\n     */\\n    set currentTrack(input) {\\n      captions.set.call(this, input, false);\\n      captions.setup.call(this);\\n    }\\n\\n    /**\\n     * Get the current caption track index (-1 if disabled)\\n     */\\n    get currentTrack() {\\n      const {\\n        toggled,\\n        currentTrack\\n      } = this.captions;\\n      return toggled ? currentTrack : -1;\\n    }\\n\\n    /**\\n     * Set the wanted language for captions\\n     * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n     * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n     */\\n    set language(input) {\\n      captions.setLanguage.call(this, input, false);\\n    }\\n\\n    /**\\n     * Get the current track's language\\n     */\\n    get language() {\\n      return (captions.getCurrentTrack.call(this) || {}).language;\\n    }\\n\\n    /**\\n     * Toggle picture-in-picture playback on WebKit/MacOS\\n     * TODO: update player with state, support, enabled\\n     * TODO: detect outside changes\\n     */\\n    set pip(input) {\\n      // Bail if no support\\n      if (!support.pip) {\\n        return;\\n      }\\n\\n      // Toggle based on current state if not passed\\n      const toggle = is.boolean(input) ? input : !this.pip;\\n\\n      // Toggle based on current state\\n      // Safari\\n      if (is.function(this.media.webkitSetPresentationMode)) {\\n        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n      }\\n\\n      // Chrome\\n      if (is.function(this.media.requestPictureInPicture)) {\\n        if (!this.pip && toggle) {\\n          this.media.requestPictureInPicture();\\n        } else if (this.pip && !toggle) {\\n          document.exitPictureInPicture();\\n        }\\n      }\\n    }\\n\\n    /**\\n     * Get the current picture-in-picture state\\n     */\\n    get pip() {\\n      if (!support.pip) {\\n        return null;\\n      }\\n\\n      // Safari\\n      if (!is.empty(this.media.webkitPresentationMode)) {\\n        return this.media.webkitPresentationMode === pip.active;\\n      }\\n\\n      // Chrome\\n      return this.media === document.pictureInPictureElement;\\n    }\\n\\n    /**\\n     * Sets the preview thumbnails for the current source\\n     */\\n    setPreviewThumbnails(thumbnailSource) {\\n      if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n        this.previewThumbnails.destroy();\\n        this.previewThumbnails = null;\\n      }\\n      Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n      // Create new instance if it is still enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n    /**\\n     * Check for support\\n     * @param {String} type - Player type (audio/video)\\n     * @param {String} provider - Provider (html5/youtube/vimeo)\\n     */\\n    static supported(type, provider) {\\n      return support.check(type, provider);\\n    }\\n\\n    /**\\n     * Load an SVG sprite into the page\\n     * @param {String} url - URL for the SVG sprite\\n     * @param {String} [id] - Unique ID\\n     */\\n    static loadSprite(url, id) {\\n      return loadSprite(url, id);\\n    }\\n\\n    /**\\n     * Setup multiple instances\\n     * @param {*} selector\\n     * @param {Object} options\\n     */\\n    static setup(selector, options = {}) {\\n      let targets = null;\\n      if (is.string(selector)) {\\n        targets = Array.from(document.querySelectorAll(selector));\\n      } else if (is.nodeList(selector)) {\\n        targets = Array.from(selector);\\n      } else if (is.array(selector)) {\\n        targets = selector.filter(is.element);\\n      }\\n      if (is.empty(targets)) {\\n        return null;\\n      }\\n      return targets.map(t => new Plyr(t, options));\\n    }\\n  }\\n  Plyr.defaults = cloneDeep(defaults);\\n\\n  // ==========================================================================\\n\\n  return Plyr;\\n\\n}));\\n//# sourceMappingURL=plyr.polyfilled.js.map\\n\",\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function() {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n\\n  try {\\n    var ce = new window.CustomEvent('test', { cancelable: true });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function(event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(\\n        event,\\n        params.bubbles,\\n        params.cancelable,\\n        params.detail\\n      );\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function() {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function() {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"(function(global) {\\r\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\r\\n\\r\\n  var checkIfIteratorIsSupported = function() {\\r\\n    try {\\r\\n      return !!Symbol.iterator;\\r\\n    } catch (error) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var iteratorSupported = checkIfIteratorIsSupported();\\r\\n\\r\\n  var createIterator = function(items) {\\r\\n    var iterator = {\\r\\n      next: function() {\\r\\n        var value = items.shift();\\r\\n        return { done: value === void 0, value: value };\\r\\n      }\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      iterator[Symbol.iterator] = function() {\\r\\n        return iterator;\\r\\n      };\\r\\n    }\\r\\n\\r\\n    return iterator;\\r\\n  };\\r\\n\\r\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\r\\n  var serializeParam = function(value) {\\r\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\r\\n  };\\r\\n\\r\\n  var deserializeParam = function(value) {\\r\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\r\\n  };\\r\\n\\r\\n  var polyfillURLSearchParams = function() {\\r\\n\\r\\n    var URLSearchParams = function(searchString) {\\r\\n      Object.defineProperty(this, '_entries', { writable: true, value: {} });\\r\\n      var typeofSearchString = typeof searchString;\\r\\n\\r\\n      if (typeofSearchString === 'undefined') {\\r\\n        // do nothing\\r\\n      } else if (typeofSearchString === 'string') {\\r\\n        if (searchString !== '') {\\r\\n          this._fromString(searchString);\\r\\n        }\\r\\n      } else if (searchString instanceof URLSearchParams) {\\r\\n        var _this = this;\\r\\n        searchString.forEach(function(value, name) {\\r\\n          _this.append(name, value);\\r\\n        });\\r\\n      } else if ((searchString !== null) && (typeofSearchString === 'object')) {\\r\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\r\\n          for (var i = 0; i < searchString.length; i++) {\\r\\n            var entry = searchString[i];\\r\\n            if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) {\\r\\n              this.append(entry[0], entry[1]);\\r\\n            } else {\\r\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\r\\n            }\\r\\n          }\\r\\n        } else {\\r\\n          for (var key in searchString) {\\r\\n            if (searchString.hasOwnProperty(key)) {\\r\\n              this.append(key, searchString[key]);\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      } else {\\r\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\r\\n      }\\r\\n    };\\r\\n\\r\\n    var proto = URLSearchParams.prototype;\\r\\n\\r\\n    proto.append = function(name, value) {\\r\\n      if (name in this._entries) {\\r\\n        this._entries[name].push(String(value));\\r\\n      } else {\\r\\n        this._entries[name] = [String(value)];\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.delete = function(name) {\\r\\n      delete this._entries[name];\\r\\n    };\\r\\n\\r\\n    proto.get = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name][0] : null;\\r\\n    };\\r\\n\\r\\n    proto.getAll = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name].slice(0) : [];\\r\\n    };\\r\\n\\r\\n    proto.has = function(name) {\\r\\n      return (name in this._entries);\\r\\n    };\\r\\n\\r\\n    proto.set = function(name, value) {\\r\\n      this._entries[name] = [String(value)];\\r\\n    };\\r\\n\\r\\n    proto.forEach = function(callback, thisArg) {\\r\\n      var entries;\\r\\n      for (var name in this._entries) {\\r\\n        if (this._entries.hasOwnProperty(name)) {\\r\\n          entries = this._entries[name];\\r\\n          for (var i = 0; i < entries.length; i++) {\\r\\n            callback.call(thisArg, entries[i], name, this);\\r\\n          }\\r\\n        }\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.keys = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push(name);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.values = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value) {\\r\\n        items.push(value);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.entries = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      proto[Symbol.iterator] = proto.entries;\\r\\n    }\\r\\n\\r\\n    proto.toString = function() {\\r\\n      var searchArray = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\r\\n      });\\r\\n      return searchArray.join('&');\\r\\n    };\\r\\n\\r\\n\\r\\n    global.URLSearchParams = URLSearchParams;\\r\\n  };\\r\\n\\r\\n  var checkIfURLSearchParamsSupported = function() {\\r\\n    try {\\r\\n      var URLSearchParams = global.URLSearchParams;\\r\\n\\r\\n      return (\\r\\n        (new URLSearchParams('?a=1').toString() === 'a=1') &&\\r\\n        (typeof URLSearchParams.prototype.set === 'function') &&\\r\\n        (typeof URLSearchParams.prototype.entries === 'function')\\r\\n      );\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLSearchParamsSupported()) {\\r\\n    polyfillURLSearchParams();\\r\\n  }\\r\\n\\r\\n  var proto = global.URLSearchParams.prototype;\\r\\n\\r\\n  if (typeof proto.sort !== 'function') {\\r\\n    proto.sort = function() {\\r\\n      var _this = this;\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n        if (!_this._entries) {\\r\\n          _this.delete(name);\\r\\n        }\\r\\n      });\\r\\n      items.sort(function(a, b) {\\r\\n        if (a[0] < b[0]) {\\r\\n          return -1;\\r\\n        } else if (a[0] > b[0]) {\\r\\n          return +1;\\r\\n        } else {\\r\\n          return 0;\\r\\n        }\\r\\n      });\\r\\n      if (_this._entries) { // force reset because IE keeps keys index\\r\\n        _this._entries = {};\\r\\n      }\\r\\n      for (var i = 0; i < items.length; i++) {\\r\\n        this.append(items[i][0], items[i][1]);\\r\\n      }\\r\\n    };\\r\\n  }\\r\\n\\r\\n  if (typeof proto._fromString !== 'function') {\\r\\n    Object.defineProperty(proto, '_fromString', {\\r\\n      enumerable: false,\\r\\n      configurable: false,\\r\\n      writable: false,\\r\\n      value: function(searchString) {\\r\\n        if (this._entries) {\\r\\n          this._entries = {};\\r\\n        } else {\\r\\n          var keys = [];\\r\\n          this.forEach(function(value, name) {\\r\\n            keys.push(name);\\r\\n          });\\r\\n          for (var i = 0; i < keys.length; i++) {\\r\\n            this.delete(keys[i]);\\r\\n          }\\r\\n        }\\r\\n\\r\\n        searchString = searchString.replace(/^\\\\?/, '');\\r\\n        var attributes = searchString.split('&');\\r\\n        var attribute;\\r\\n        for (var i = 0; i < attributes.length; i++) {\\r\\n          attribute = attributes[i].split('=');\\r\\n          this.append(\\r\\n            deserializeParam(attribute[0]),\\r\\n            (attribute.length > 1) ? deserializeParam(attribute[1]) : ''\\r\\n          );\\r\\n        }\\r\\n      }\\r\\n    });\\r\\n  }\\r\\n\\r\\n  // HTMLAnchorElement\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\\r\\n(function(global) {\\r\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\r\\n\\r\\n  var checkIfURLIsSupported = function() {\\r\\n    try {\\r\\n      var u = new global.URL('b', 'http://a');\\r\\n      u.pathname = 'c d';\\r\\n      return (u.href === 'http://a/c%20d') && u.searchParams;\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var polyfillURL = function() {\\r\\n    var _URL = global.URL;\\r\\n\\r\\n    var URL = function(url, base) {\\r\\n      if (typeof url !== 'string') url = String(url);\\r\\n      if (base && typeof base !== 'string') base = String(base);\\r\\n\\r\\n      // Only create another document if the base is different from current location.\\r\\n      var doc = document, baseElement;\\r\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\r\\n        base = base.toLowerCase();\\r\\n        doc = document.implementation.createHTMLDocument('');\\r\\n        baseElement = doc.createElement('base');\\r\\n        baseElement.href = base;\\r\\n        doc.head.appendChild(baseElement);\\r\\n        try {\\r\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\r\\n        } catch (err) {\\r\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\r\\n        }\\r\\n      }\\r\\n\\r\\n      var anchorElement = doc.createElement('a');\\r\\n      anchorElement.href = url;\\r\\n      if (baseElement) {\\r\\n        doc.body.appendChild(anchorElement);\\r\\n        anchorElement.href = anchorElement.href; // force href to refresh\\r\\n      }\\r\\n\\r\\n      var inputElement = doc.createElement('input');\\r\\n      inputElement.type = 'url';\\r\\n      inputElement.value = url;\\r\\n\\r\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || (!inputElement.checkValidity() && !base)) {\\r\\n        throw new TypeError('Invalid URL');\\r\\n      }\\r\\n\\r\\n      Object.defineProperty(this, '_anchorElement', {\\r\\n        value: anchorElement\\r\\n      });\\r\\n\\r\\n\\r\\n      // create a linked searchParams which reflect its changes on URL\\r\\n      var searchParams = new global.URLSearchParams(this.search);\\r\\n      var enableSearchUpdate = true;\\r\\n      var enableSearchParamsUpdate = true;\\r\\n      var _this = this;\\r\\n      ['append', 'delete', 'set'].forEach(function(methodName) {\\r\\n        var method = searchParams[methodName];\\r\\n        searchParams[methodName] = function() {\\r\\n          method.apply(searchParams, arguments);\\r\\n          if (enableSearchUpdate) {\\r\\n            enableSearchParamsUpdate = false;\\r\\n            _this.search = searchParams.toString();\\r\\n            enableSearchParamsUpdate = true;\\r\\n          }\\r\\n        };\\r\\n      });\\r\\n\\r\\n      Object.defineProperty(this, 'searchParams', {\\r\\n        value: searchParams,\\r\\n        enumerable: true\\r\\n      });\\r\\n\\r\\n      var search = void 0;\\r\\n      Object.defineProperty(this, '_updateSearchParams', {\\r\\n        enumerable: false,\\r\\n        configurable: false,\\r\\n        writable: false,\\r\\n        value: function() {\\r\\n          if (this.search !== search) {\\r\\n            search = this.search;\\r\\n            if (enableSearchParamsUpdate) {\\r\\n              enableSearchUpdate = false;\\r\\n              this.searchParams._fromString(this.search);\\r\\n              enableSearchUpdate = true;\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      });\\r\\n    };\\r\\n\\r\\n    var proto = URL.prototype;\\r\\n\\r\\n    var linkURLWithAnchorAttribute = function(attributeName) {\\r\\n      Object.defineProperty(proto, attributeName, {\\r\\n        get: function() {\\r\\n          return this._anchorElement[attributeName];\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement[attributeName] = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      });\\r\\n    };\\r\\n\\r\\n    ['hash', 'host', 'hostname', 'port', 'protocol']\\r\\n      .forEach(function(attributeName) {\\r\\n        linkURLWithAnchorAttribute(attributeName);\\r\\n      });\\r\\n\\r\\n    Object.defineProperty(proto, 'search', {\\r\\n      get: function() {\\r\\n        return this._anchorElement['search'];\\r\\n      },\\r\\n      set: function(value) {\\r\\n        this._anchorElement['search'] = value;\\r\\n        this._updateSearchParams();\\r\\n      },\\r\\n      enumerable: true\\r\\n    });\\r\\n\\r\\n    Object.defineProperties(proto, {\\r\\n\\r\\n      'toString': {\\r\\n        get: function() {\\r\\n          var _this = this;\\r\\n          return function() {\\r\\n            return _this.href;\\r\\n          };\\r\\n        }\\r\\n      },\\r\\n\\r\\n      'href': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.href = value;\\r\\n          this._updateSearchParams();\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'pathname': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.pathname = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'origin': {\\r\\n        get: function() {\\r\\n          // get expected port from protocol\\r\\n          var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol];\\r\\n          // add port to origin if, expected port is different than actual port\\r\\n          // and it is not empty f.e http://foo:8080\\r\\n          // 8080 != 80 && 8080 != ''\\r\\n          var addPortToOrigin = this._anchorElement.port != expectedPort &&\\r\\n            this._anchorElement.port !== '';\\r\\n\\r\\n          return this._anchorElement.protocol +\\r\\n            '//' +\\r\\n            this._anchorElement.hostname +\\r\\n            (addPortToOrigin ? (':' + this._anchorElement.port) : '');\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'password': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'username': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n    });\\r\\n\\r\\n    URL.createObjectURL = function(blob) {\\r\\n      return _URL.createObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    URL.revokeObjectURL = function(url) {\\r\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    global.URL = URL;\\r\\n\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLIsSupported()) {\\r\\n    polyfillURL();\\r\\n  }\\r\\n\\r\\n  if ((global.location !== void 0) && !('origin' in global.location)) {\\r\\n    var getOrigin = function() {\\r\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');\\r\\n    };\\r\\n\\r\\n    try {\\r\\n      Object.defineProperty(global.location, 'origin', {\\r\\n        get: getOrigin,\\r\\n        enumerable: true\\r\\n      });\\r\\n    } catch (e) {\\r\\n      setInterval(function() {\\r\\n        global.location.origin = getOrigin();\\r\\n      }, 100);\\r\\n    }\\r\\n  }\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}',\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',\\n    },\\n  },\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\n+{\"version\":3,\"sources\":[\"plyr.polyfilled.js\",\"node_modules/.pnpm/custom-event-polyfill@1.0.7/node_modules/custom-event-polyfill/polyfill.js\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"node_modules/.pnpm/url-polyfill@1.1.12/node_modules/url-polyfill/url-polyfill.js\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"navigator\",\"global\",\"factory\",\"exports\",\"module\",\"define\",\"amd\",\"globalThis\",\"self\",\"Plyr\",\"this\",\"window\",\"ce\",\"CustomEvent\",\"cancelable\",\"preventDefault\",\"defaultPrevented\",\"Error\",\"e\",\"event\",\"params\",\"evt\",\"origPrevent\",\"bubbles\",\"document\",\"createEvent\",\"initCustomEvent\",\"detail\",\"call\",\"Object\",\"defineProperty\",\"get\",\"prototype\",\"Event\",\"commonjsGlobal\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"arg\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"TypeError\",\"String\",\"Number\",\"_toPrimitive\",\"_toPropertyKey\",\"enumerable\",\"configurable\",\"writable\",\"_defineProperties\",\"t\",\"n\",\"length\",\"r\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"iteratorSupported\",\"iterator\",\"error\",\"checkIfIteratorIsSupported\",\"createIterator\",\"items\",\"next\",\"shift\",\"done\",\"serializeParam\",\"encodeURIComponent\",\"replace\",\"deserializeParam\",\"decodeURIComponent\",\"URLSearchParams\",\"toString\",\"set\",\"entries\",\"checkIfURLSearchParamsSupported\",\"searchString\",\"typeofSearchString\",\"_fromString\",\"_this\",\"name\",\"append\",\"i\",\"entry\",\"hasOwnProperty\",\"proto\",\"_entries\",\"delete\",\"getAll\",\"slice\",\"has\",\"callback\",\"thisArg\",\"values\",\"searchArray\",\"join\",\"polyfillURLSearchParams\",\"sort\",\"a\",\"b\",\"attribute\",\"attributes\",\"split\",\"u\",\"URL\",\"pathname\",\"href\",\"searchParams\",\"checkIfURLIsSupported\",\"_URL\",\"url\",\"base\",\"baseElement\",\"doc\",\"location\",\"toLowerCase\",\"implementation\",\"createHTMLDocument\",\"createElement\",\"head\",\"appendChild\",\"indexOf\",\"err\",\"anchorElement\",\"body\",\"inputElement\",\"type\",\"protocol\",\"test\",\"checkValidity\",\"search\",\"enableSearchUpdate\",\"enableSearchParamsUpdate\",\"methodName\",\"method\",\"attributeName\",\"_anchorElement\",\"linkURLWithAnchorAttribute\",\"_updateSearchParams\",\"origin\",\"expectedPort\",\"addPortToOrigin\",\"port\",\"hostname\",\"password\",\"username\",\"createObjectURL\",\"blob\",\"revokeObjectURL\",\"polyfillURL\",\"getOrigin\",\"setInterval\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isString\",\"isArray\",\"Array\",\"isNodeList\",\"NodeList\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"isNaN\",\"string\",\"boolean\",\"Boolean\",\"function\",\"Function\",\"array\",\"nodeList\",\"element\",\"Element\",\"empty\",\"round\",\"concat\",\"match\",\"Math\",\"max\",\"getDecimalPlaces\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"_classCallCheck\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"_createClass\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"target\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"c\",\"getBoundingClientRect\",\"width\",\"clientX\",\"left\",\"disabled\",\"dispatchEvent\",\"trigger\",\"from\",\"querySelectorAll\",\"MutationObserver\",\"addedNodes\",\"includes\",\"matches\",\"observe\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isFunction\",\"isEmpty\",\"weakMap\",\"WeakMap\",\"nodeType\",\"ownerDocument\",\"textNode\",\"Text\",\"keyboardEvent\",\"KeyboardEvent\",\"cue\",\"TextTrackCue\",\"VTTCue\",\"track\",\"TextTrack\",\"kind\",\"promise\",\"Promise\",\"then\",\"startsWith\",\"_\",\"transitionEndEvent\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"browser\",\"isIE\",\"documentMode\",\"isEdge\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"getDeep\",\"path\",\"reduce\",\"extend\",\"sources\",\"source\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"insertBefore\",\"setAttributes\",\"setAttribute\",\"text\",\"innerText\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"closest\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"parse\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"format\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"toCamelCase\",\"toPascalCase\",\"getHTML\",\"innerHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"JSON\",\"storage\",\"setItem\",\"stringify\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"status\",\"open\",\"send\",\"loadSprite\",\"prefix\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"current\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"urls\",\"isEmbed\",\"inject\",\"floor\",\"random\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"createDocumentFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"isYouTube\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"providers\",\"types\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"el\",\"parentElement\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"hasAttribute\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"fn\",\"createCommonjsModule\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"loadScript\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"found\",\"parseHash\",\"hashParam\",\"sidedock\",\"gesture\",\"$2\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"fragment\",\"firstChild\",\"stripHTML\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"Ads\",\"google\",\"ima\",\"manager\",\"destroy\",\"displayContainer\",\"remove\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"getProviderByUrl\",\"truthy\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.polyfilled.min.js\",\"sourcesContent\":[\"typeof navigator === \\\"object\\\" && (function (global, factory) {\\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\\n  typeof define === 'function' && define.amd ? define('Plyr', factory) :\\n  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory());\\n})(this, (function () { 'use strict';\\n\\n  // Polyfill for creating CustomEvents on IE9/10/11\\n\\n  // code pulled from:\\n  // https://github.com/d4tocchini/customevent-polyfill\\n  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n  (function () {\\n    if (typeof window === 'undefined') {\\n      return;\\n    }\\n    try {\\n      var ce = new window.CustomEvent('test', {\\n        cancelable: true\\n      });\\n      ce.preventDefault();\\n      if (ce.defaultPrevented !== true) {\\n        // IE has problems with .preventDefault() on custom events\\n        // http://stackoverflow.com/questions/23349191\\n        throw new Error('Could not prevent default');\\n      }\\n    } catch (e) {\\n      var CustomEvent = function (event, params) {\\n        var evt, origPrevent;\\n        params = params || {};\\n        params.bubbles = !!params.bubbles;\\n        params.cancelable = !!params.cancelable;\\n        evt = document.createEvent('CustomEvent');\\n        evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);\\n        origPrevent = evt.preventDefault;\\n        evt.preventDefault = function () {\\n          origPrevent.call(this);\\n          try {\\n            Object.defineProperty(this, 'defaultPrevented', {\\n              get: function () {\\n                return true;\\n              }\\n            });\\n          } catch (e) {\\n            this.defaultPrevented = true;\\n          }\\n        };\\n        return evt;\\n      };\\n      CustomEvent.prototype = window.Event.prototype;\\n      window.CustomEvent = CustomEvent; // expose definition to window\\n    }\\n  })();\\n\\n  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\n  function createCommonjsModule(fn, module) {\\n  \\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n  }\\n\\n  (function (global) {\\n    /**\\r\\n     * Polyfill URLSearchParams\\r\\n     *\\r\\n     * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n     */\\n\\n    var checkIfIteratorIsSupported = function () {\\n      try {\\n        return !!Symbol.iterator;\\n      } catch (error) {\\n        return false;\\n      }\\n    };\\n    var iteratorSupported = checkIfIteratorIsSupported();\\n    var createIterator = function (items) {\\n      var iterator = {\\n        next: function () {\\n          var value = items.shift();\\n          return {\\n            done: value === void 0,\\n            value: value\\n          };\\n        }\\n      };\\n      if (iteratorSupported) {\\n        iterator[Symbol.iterator] = function () {\\n          return iterator;\\n        };\\n      }\\n      return iterator;\\n    };\\n\\n    /**\\r\\n     * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n     * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n     */\\n    var serializeParam = function (value) {\\n      return encodeURIComponent(value).replace(/%20/g, '+');\\n    };\\n    var deserializeParam = function (value) {\\n      return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\n    };\\n    var polyfillURLSearchParams = function () {\\n      var URLSearchParams = function (searchString) {\\n        Object.defineProperty(this, '_entries', {\\n          writable: true,\\n          value: {}\\n        });\\n        var typeofSearchString = typeof searchString;\\n        if (typeofSearchString === 'undefined') ; else if (typeofSearchString === 'string') {\\n          if (searchString !== '') {\\n            this._fromString(searchString);\\n          }\\n        } else if (searchString instanceof URLSearchParams) {\\n          var _this = this;\\n          searchString.forEach(function (value, name) {\\n            _this.append(name, value);\\n          });\\n        } else if (searchString !== null && typeofSearchString === 'object') {\\n          if (Object.prototype.toString.call(searchString) === '[object Array]') {\\n            for (var i = 0; i < searchString.length; i++) {\\n              var entry = searchString[i];\\n              if (Object.prototype.toString.call(entry) === '[object Array]' || entry.length !== 2) {\\n                this.append(entry[0], entry[1]);\\n              } else {\\n                throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\n              }\\n            }\\n          } else {\\n            for (var key in searchString) {\\n              if (searchString.hasOwnProperty(key)) {\\n                this.append(key, searchString[key]);\\n              }\\n            }\\n          }\\n        } else {\\n          throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\n        }\\n      };\\n      var proto = URLSearchParams.prototype;\\n      proto.append = function (name, value) {\\n        if (name in this._entries) {\\n          this._entries[name].push(String(value));\\n        } else {\\n          this._entries[name] = [String(value)];\\n        }\\n      };\\n      proto.delete = function (name) {\\n        delete this._entries[name];\\n      };\\n      proto.get = function (name) {\\n        return name in this._entries ? this._entries[name][0] : null;\\n      };\\n      proto.getAll = function (name) {\\n        return name in this._entries ? this._entries[name].slice(0) : [];\\n      };\\n      proto.has = function (name) {\\n        return name in this._entries;\\n      };\\n      proto.set = function (name, value) {\\n        this._entries[name] = [String(value)];\\n      };\\n      proto.forEach = function (callback, thisArg) {\\n        var entries;\\n        for (var name in this._entries) {\\n          if (this._entries.hasOwnProperty(name)) {\\n            entries = this._entries[name];\\n            for (var i = 0; i < entries.length; i++) {\\n              callback.call(thisArg, entries[i], name, this);\\n            }\\n          }\\n        }\\n      };\\n      proto.keys = function () {\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push(name);\\n        });\\n        return createIterator(items);\\n      };\\n      proto.values = function () {\\n        var items = [];\\n        this.forEach(function (value) {\\n          items.push(value);\\n        });\\n        return createIterator(items);\\n      };\\n      proto.entries = function () {\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push([name, value]);\\n        });\\n        return createIterator(items);\\n      };\\n      if (iteratorSupported) {\\n        proto[Symbol.iterator] = proto.entries;\\n      }\\n      proto.toString = function () {\\n        var searchArray = [];\\n        this.forEach(function (value, name) {\\n          searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\n        });\\n        return searchArray.join('&');\\n      };\\n      global.URLSearchParams = URLSearchParams;\\n    };\\n    var checkIfURLSearchParamsSupported = function () {\\n      try {\\n        var URLSearchParams = global.URLSearchParams;\\n        return new URLSearchParams('?a=1').toString() === 'a=1' && typeof URLSearchParams.prototype.set === 'function' && typeof URLSearchParams.prototype.entries === 'function';\\n      } catch (e) {\\n        return false;\\n      }\\n    };\\n    if (!checkIfURLSearchParamsSupported()) {\\n      polyfillURLSearchParams();\\n    }\\n    var proto = global.URLSearchParams.prototype;\\n    if (typeof proto.sort !== 'function') {\\n      proto.sort = function () {\\n        var _this = this;\\n        var items = [];\\n        this.forEach(function (value, name) {\\n          items.push([name, value]);\\n          if (!_this._entries) {\\n            _this.delete(name);\\n          }\\n        });\\n        items.sort(function (a, b) {\\n          if (a[0] < b[0]) {\\n            return -1;\\n          } else if (a[0] > b[0]) {\\n            return +1;\\n          } else {\\n            return 0;\\n          }\\n        });\\n        if (_this._entries) {\\n          // force reset because IE keeps keys index\\n          _this._entries = {};\\n        }\\n        for (var i = 0; i < items.length; i++) {\\n          this.append(items[i][0], items[i][1]);\\n        }\\n      };\\n    }\\n    if (typeof proto._fromString !== 'function') {\\n      Object.defineProperty(proto, '_fromString', {\\n        enumerable: false,\\n        configurable: false,\\n        writable: false,\\n        value: function (searchString) {\\n          if (this._entries) {\\n            this._entries = {};\\n          } else {\\n            var keys = [];\\n            this.forEach(function (value, name) {\\n              keys.push(name);\\n            });\\n            for (var i = 0; i < keys.length; i++) {\\n              this.delete(keys[i]);\\n            }\\n          }\\n          searchString = searchString.replace(/^\\\\?/, '');\\n          var attributes = searchString.split('&');\\n          var attribute;\\n          for (var i = 0; i < attributes.length; i++) {\\n            attribute = attributes[i].split('=');\\n            this.append(deserializeParam(attribute[0]), attribute.length > 1 ? deserializeParam(attribute[1]) : '');\\n          }\\n        }\\n      });\\n    }\\n\\n    // HTMLAnchorElement\\n  })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n  (function (global) {\\n    /**\\r\\n     * Polyfill URL\\r\\n     *\\r\\n     * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n     */\\n\\n    var checkIfURLIsSupported = function () {\\n      try {\\n        var u = new global.URL('b', 'http://a');\\n        u.pathname = 'c d';\\n        return u.href === 'http://a/c%20d' && u.searchParams;\\n      } catch (e) {\\n        return false;\\n      }\\n    };\\n    var polyfillURL = function () {\\n      var _URL = global.URL;\\n      var URL = function (url, base) {\\n        if (typeof url !== 'string') url = String(url);\\n        if (base && typeof base !== 'string') base = String(base);\\n\\n        // Only create another document if the base is different from current location.\\n        var doc = document,\\n          baseElement;\\n        if (base && (global.location === void 0 || base !== global.location.href)) {\\n          base = base.toLowerCase();\\n          doc = document.implementation.createHTMLDocument('');\\n          baseElement = doc.createElement('base');\\n          baseElement.href = base;\\n          doc.head.appendChild(baseElement);\\n          try {\\n            if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\n          } catch (err) {\\n            throw new Error('URL unable to set base ' + base + ' due to ' + err);\\n          }\\n        }\\n        var anchorElement = doc.createElement('a');\\n        anchorElement.href = url;\\n        if (baseElement) {\\n          doc.body.appendChild(anchorElement);\\n          anchorElement.href = anchorElement.href; // force href to refresh\\n        }\\n\\n        var inputElement = doc.createElement('input');\\n        inputElement.type = 'url';\\n        inputElement.value = url;\\n        if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || !inputElement.checkValidity() && !base) {\\n          throw new TypeError('Invalid URL');\\n        }\\n        Object.defineProperty(this, '_anchorElement', {\\n          value: anchorElement\\n        });\\n\\n        // create a linked searchParams which reflect its changes on URL\\n        var searchParams = new global.URLSearchParams(this.search);\\n        var enableSearchUpdate = true;\\n        var enableSearchParamsUpdate = true;\\n        var _this = this;\\n        ['append', 'delete', 'set'].forEach(function (methodName) {\\n          var method = searchParams[methodName];\\n          searchParams[methodName] = function () {\\n            method.apply(searchParams, arguments);\\n            if (enableSearchUpdate) {\\n              enableSearchParamsUpdate = false;\\n              _this.search = searchParams.toString();\\n              enableSearchParamsUpdate = true;\\n            }\\n          };\\n        });\\n        Object.defineProperty(this, 'searchParams', {\\n          value: searchParams,\\n          enumerable: true\\n        });\\n        var search = void 0;\\n        Object.defineProperty(this, '_updateSearchParams', {\\n          enumerable: false,\\n          configurable: false,\\n          writable: false,\\n          value: function () {\\n            if (this.search !== search) {\\n              search = this.search;\\n              if (enableSearchParamsUpdate) {\\n                enableSearchUpdate = false;\\n                this.searchParams._fromString(this.search);\\n                enableSearchUpdate = true;\\n              }\\n            }\\n          }\\n        });\\n      };\\n      var proto = URL.prototype;\\n      var linkURLWithAnchorAttribute = function (attributeName) {\\n        Object.defineProperty(proto, attributeName, {\\n          get: function () {\\n            return this._anchorElement[attributeName];\\n          },\\n          set: function (value) {\\n            this._anchorElement[attributeName] = value;\\n          },\\n          enumerable: true\\n        });\\n      };\\n      ['hash', 'host', 'hostname', 'port', 'protocol'].forEach(function (attributeName) {\\n        linkURLWithAnchorAttribute(attributeName);\\n      });\\n      Object.defineProperty(proto, 'search', {\\n        get: function () {\\n          return this._anchorElement['search'];\\n        },\\n        set: function (value) {\\n          this._anchorElement['search'] = value;\\n          this._updateSearchParams();\\n        },\\n        enumerable: true\\n      });\\n      Object.defineProperties(proto, {\\n        'toString': {\\n          get: function () {\\n            var _this = this;\\n            return function () {\\n              return _this.href;\\n            };\\n          }\\n        },\\n        'href': {\\n          get: function () {\\n            return this._anchorElement.href.replace(/\\\\?$/, '');\\n          },\\n          set: function (value) {\\n            this._anchorElement.href = value;\\n            this._updateSearchParams();\\n          },\\n          enumerable: true\\n        },\\n        'pathname': {\\n          get: function () {\\n            return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\n          },\\n          set: function (value) {\\n            this._anchorElement.pathname = value;\\n          },\\n          enumerable: true\\n        },\\n        'origin': {\\n          get: function () {\\n            // get expected port from protocol\\n            var expectedPort = {\\n              'http:': 80,\\n              'https:': 443,\\n              'ftp:': 21\\n            }[this._anchorElement.protocol];\\n            // add port to origin if, expected port is different than actual port\\n            // and it is not empty f.e http://foo:8080\\n            // 8080 != 80 && 8080 != ''\\n            var addPortToOrigin = this._anchorElement.port != expectedPort && this._anchorElement.port !== '';\\n            return this._anchorElement.protocol + '//' + this._anchorElement.hostname + (addPortToOrigin ? ':' + this._anchorElement.port : '');\\n          },\\n          enumerable: true\\n        },\\n        'password': {\\n          // TODO\\n          get: function () {\\n            return '';\\n          },\\n          set: function (value) {},\\n          enumerable: true\\n        },\\n        'username': {\\n          // TODO\\n          get: function () {\\n            return '';\\n          },\\n          set: function (value) {},\\n          enumerable: true\\n        }\\n      });\\n      URL.createObjectURL = function (blob) {\\n        return _URL.createObjectURL.apply(_URL, arguments);\\n      };\\n      URL.revokeObjectURL = function (url) {\\n        return _URL.revokeObjectURL.apply(_URL, arguments);\\n      };\\n      global.URL = URL;\\n    };\\n    if (!checkIfURLIsSupported()) {\\n      polyfillURL();\\n    }\\n    if (global.location !== void 0 && !('origin' in global.location)) {\\n      var getOrigin = function () {\\n        return global.location.protocol + '//' + global.location.hostname + (global.location.port ? ':' + global.location.port : '');\\n      };\\n      try {\\n        Object.defineProperty(global.location, 'origin', {\\n          get: getOrigin,\\n          enumerable: true\\n        });\\n      } catch (e) {\\n        setInterval(function () {\\n          global.location.origin = getOrigin();\\n        }, 100);\\n      }\\n    }\\n  })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n\\n  function _defineProperty$1(obj, key, value) {\\n    key = _toPropertyKey(key);\\n    if (key in obj) {\\n      Object.defineProperty(obj, key, {\\n        value: value,\\n        enumerable: true,\\n        configurable: true,\\n        writable: true\\n      });\\n    } else {\\n      obj[key] = value;\\n    }\\n    return obj;\\n  }\\n  function _toPrimitive(input, hint) {\\n    if (typeof input !== \\\"object\\\" || input === null) return input;\\n    var prim = input[Symbol.toPrimitive];\\n    if (prim !== undefined) {\\n      var res = prim.call(input, hint || \\\"default\\\");\\n      if (typeof res !== \\\"object\\\") return res;\\n      throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n    }\\n    return (hint === \\\"string\\\" ? String : Number)(input);\\n  }\\n  function _toPropertyKey(arg) {\\n    var key = _toPrimitive(arg, \\\"string\\\");\\n    return typeof key === \\\"symbol\\\" ? key : String(key);\\n  }\\n\\n  function _classCallCheck(e, t) {\\n    if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n  }\\n  function _defineProperties(e, t) {\\n    for (var n = 0; n < t.length; n++) {\\n      var r = t[n];\\n      r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n    }\\n  }\\n  function _createClass(e, t, n) {\\n    return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n  }\\n  function _defineProperty(e, t, n) {\\n    return t in e ? Object.defineProperty(e, t, {\\n      value: n,\\n      enumerable: !0,\\n      configurable: !0,\\n      writable: !0\\n    }) : e[t] = n, e;\\n  }\\n  function ownKeys(e, t) {\\n    var n = Object.keys(e);\\n    if (Object.getOwnPropertySymbols) {\\n      var r = Object.getOwnPropertySymbols(e);\\n      t && (r = r.filter(function (t) {\\n        return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n      })), n.push.apply(n, r);\\n    }\\n    return n;\\n  }\\n  function _objectSpread2(e) {\\n    for (var t = 1; t < arguments.length; t++) {\\n      var n = null != arguments[t] ? arguments[t] : {};\\n      t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n        _defineProperty(e, t, n[t]);\\n      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n        Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n      });\\n    }\\n    return e;\\n  }\\n  var defaults$1 = {\\n    addCSS: !0,\\n    thumbWidth: 15,\\n    watch: !0\\n  };\\n  function matches$1(e, t) {\\n    return function () {\\n      return Array.from(document.querySelectorAll(t)).includes(this);\\n    }.call(e, t);\\n  }\\n  function trigger(e, t) {\\n    if (e && t) {\\n      var n = new Event(t, {\\n        bubbles: !0\\n      });\\n      e.dispatchEvent(n);\\n    }\\n  }\\n  var getConstructor$1 = function (e) {\\n      return null != e ? e.constructor : null;\\n    },\\n    instanceOf$1 = function (e, t) {\\n      return !!(e && t && e instanceof t);\\n    },\\n    isNullOrUndefined$1 = function (e) {\\n      return null == e;\\n    },\\n    isObject$1 = function (e) {\\n      return getConstructor$1(e) === Object;\\n    },\\n    isNumber$1 = function (e) {\\n      return getConstructor$1(e) === Number && !Number.isNaN(e);\\n    },\\n    isString$1 = function (e) {\\n      return getConstructor$1(e) === String;\\n    },\\n    isBoolean$1 = function (e) {\\n      return getConstructor$1(e) === Boolean;\\n    },\\n    isFunction$1 = function (e) {\\n      return getConstructor$1(e) === Function;\\n    },\\n    isArray$1 = function (e) {\\n      return Array.isArray(e);\\n    },\\n    isNodeList$1 = function (e) {\\n      return instanceOf$1(e, NodeList);\\n    },\\n    isElement$1 = function (e) {\\n      return instanceOf$1(e, Element);\\n    },\\n    isEvent$1 = function (e) {\\n      return instanceOf$1(e, Event);\\n    },\\n    isEmpty$1 = function (e) {\\n      return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n    },\\n    is$1 = {\\n      nullOrUndefined: isNullOrUndefined$1,\\n      object: isObject$1,\\n      number: isNumber$1,\\n      string: isString$1,\\n      boolean: isBoolean$1,\\n      function: isFunction$1,\\n      array: isArray$1,\\n      nodeList: isNodeList$1,\\n      element: isElement$1,\\n      event: isEvent$1,\\n      empty: isEmpty$1\\n    };\\n  function getDecimalPlaces(e) {\\n    var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n    return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n  }\\n  function round(e, t) {\\n    if (1 > t) {\\n      var n = getDecimalPlaces(t);\\n      return parseFloat(e.toFixed(n));\\n    }\\n    return Math.round(e / t) * t;\\n  }\\n  var RangeTouch = function () {\\n    function e(t, n) {\\n      _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n    }\\n    return _createClass(e, [{\\n      key: \\\"init\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n      }\\n    }, {\\n      key: \\\"destroy\\\",\\n      value: function () {\\n        e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n      }\\n    }, {\\n      key: \\\"listeners\\\",\\n      value: function (e) {\\n        var t = this,\\n          n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n        [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n          t.element[n](e, function (e) {\\n            return t.set(e);\\n          }, !1);\\n        });\\n      }\\n    }, {\\n      key: \\\"get\\\",\\n      value: function (t) {\\n        if (!e.enabled || !is$1.event(t)) return null;\\n        var n,\\n          r = t.target,\\n          i = t.changedTouches[0],\\n          o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n          s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n          u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n          c = r.getBoundingClientRect(),\\n          a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n        return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n      }\\n    }, {\\n      key: \\\"set\\\",\\n      value: function (t) {\\n        e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n      }\\n    }], [{\\n      key: \\\"setup\\\",\\n      value: function (t) {\\n        var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n          r = null;\\n        if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n        var i = _objectSpread2({}, defaults$1, {}, n);\\n        if (is$1.string(t) && i.watch) {\\n          var o = new MutationObserver(function (n) {\\n            Array.from(n).forEach(function (n) {\\n              Array.from(n.addedNodes).forEach(function (n) {\\n                is$1.element(n) && matches$1(n, t) && new e(n, i);\\n              });\\n            });\\n          });\\n          o.observe(document.body, {\\n            childList: !0,\\n            subtree: !0\\n          });\\n        }\\n        return r.map(function (t) {\\n          return new e(t, n);\\n        });\\n      }\\n    }, {\\n      key: \\\"enabled\\\",\\n      get: function () {\\n        return \\\"ontouchstart\\\" in document.documentElement;\\n      }\\n    }]), e;\\n  }();\\n\\n  // ==========================================================================\\n  // Type checking utils\\n  // ==========================================================================\\n\\n  const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\n  const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\n  const isNullOrUndefined = input => input === null || typeof input === 'undefined';\\n  const isObject = input => getConstructor(input) === Object;\\n  const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\n  const isString = input => getConstructor(input) === String;\\n  const isBoolean = input => getConstructor(input) === Boolean;\\n  const isFunction = input => typeof input === 'function';\\n  const isArray = input => Array.isArray(input);\\n  const isWeakMap = input => instanceOf(input, WeakMap);\\n  const isNodeList = input => instanceOf(input, NodeList);\\n  const isTextNode = input => getConstructor(input) === Text;\\n  const isEvent = input => instanceOf(input, Event);\\n  const isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\n  const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\n  const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\n  const isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\n  const isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\n  const isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\n  const isUrl = input => {\\n    // Accept a URL object\\n    if (instanceOf(input, window.URL)) {\\n      return true;\\n    }\\n\\n    // Must be string from here\\n    if (!isString(input)) {\\n      return false;\\n    }\\n\\n    // Add the protocol if required\\n    let string = input;\\n    if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n      string = `http://${input}`;\\n    }\\n    try {\\n      return !isEmpty(new URL(string).hostname);\\n    } catch (_) {\\n      return false;\\n    }\\n  };\\n  var is = {\\n    nullOrUndefined: isNullOrUndefined,\\n    object: isObject,\\n    number: isNumber,\\n    string: isString,\\n    boolean: isBoolean,\\n    function: isFunction,\\n    array: isArray,\\n    weakMap: isWeakMap,\\n    nodeList: isNodeList,\\n    element: isElement,\\n    textNode: isTextNode,\\n    event: isEvent,\\n    keyboardEvent: isKeyboardEvent,\\n    cue: isCue,\\n    track: isTrack,\\n    promise: isPromise,\\n    url: isUrl,\\n    empty: isEmpty\\n  };\\n\\n  // ==========================================================================\\n  const transitionEndEvent = (() => {\\n    const element = document.createElement('span');\\n    const events = {\\n      WebkitTransition: 'webkitTransitionEnd',\\n      MozTransition: 'transitionend',\\n      OTransition: 'oTransitionEnd otransitionend',\\n      transition: 'transitionend'\\n    };\\n    const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n    return is.string(type) ? events[type] : false;\\n  })();\\n\\n  // Force repaint of element\\n  function repaint(element, delay) {\\n    setTimeout(() => {\\n      try {\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = true;\\n\\n        // eslint-disable-next-line no-unused-expressions\\n        element.offsetHeight;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        element.hidden = false;\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    }, delay);\\n  }\\n\\n  // ==========================================================================\\n  // Browser sniffing\\n  // Unfortunately, due to mixed support, UA sniffing is required\\n  // ==========================================================================\\n\\n  const isIE = Boolean(window.document.documentMode);\\n  const isEdge = /Edge/g.test(navigator.userAgent);\\n  const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\n  const isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  // navigator.platform may be deprecated but this check is still required\\n  const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\n  const isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n  var browser = {\\n    isIE,\\n    isEdge,\\n    isWebKit,\\n    isIPhone,\\n    isIPadOS,\\n    isIos\\n  };\\n\\n  // ==========================================================================\\n\\n  // Clone nested objects\\n  function cloneDeep(object) {\\n    return JSON.parse(JSON.stringify(object));\\n  }\\n\\n  // Get a nested value in an object\\n  function getDeep(object, path) {\\n    return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n  }\\n\\n  // Deep extend destination object with N more objects\\n  function extend(target = {}, ...sources) {\\n    if (!sources.length) {\\n      return target;\\n    }\\n    const source = sources.shift();\\n    if (!is.object(source)) {\\n      return target;\\n    }\\n    Object.keys(source).forEach(key => {\\n      if (is.object(source[key])) {\\n        if (!Object.keys(target).includes(key)) {\\n          Object.assign(target, {\\n            [key]: {}\\n          });\\n        }\\n        extend(target[key], source[key]);\\n      } else {\\n        Object.assign(target, {\\n          [key]: source[key]\\n        });\\n      }\\n    });\\n    return extend(target, ...sources);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Wrap an element\\n  function wrap(elements, wrapper) {\\n    // Convert `elements` to an array, if necessary.\\n    const targets = elements.length ? elements : [elements];\\n\\n    // Loops backwards to prevent having to clone the wrapper on the\\n    // first element (see `child` below).\\n    Array.from(targets).reverse().forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n  }\\n\\n  // Set attributes\\n  function setAttributes(element, attributes) {\\n    if (!is.element(element) || is.empty(attributes)) return;\\n\\n    // Assume null and undefined attributes should be left out,\\n    // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n    Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n  }\\n\\n  // Create a DocumentFragment\\n  function createElement(type, attributes, text) {\\n    // Create a new <element>\\n    const element = document.createElement(type);\\n\\n    // Set all passed attributes\\n    if (is.object(attributes)) {\\n      setAttributes(element, attributes);\\n    }\\n\\n    // Add text node\\n    if (is.string(text)) {\\n      element.innerText = text;\\n    }\\n\\n    // Return built element\\n    return element;\\n  }\\n\\n  // Insert an element after another\\n  function insertAfter(element, target) {\\n    if (!is.element(element) || !is.element(target)) return;\\n    target.parentNode.insertBefore(element, target.nextSibling);\\n  }\\n\\n  // Insert a DocumentFragment\\n  function insertElement(type, parent, attributes, text) {\\n    if (!is.element(parent)) return;\\n    parent.appendChild(createElement(type, attributes, text));\\n  }\\n\\n  // Remove element(s)\\n  function removeElement(element) {\\n    if (is.nodeList(element) || is.array(element)) {\\n      Array.from(element).forEach(removeElement);\\n      return;\\n    }\\n    if (!is.element(element) || !is.element(element.parentNode)) {\\n      return;\\n    }\\n    element.parentNode.removeChild(element);\\n  }\\n\\n  // Remove all child elements\\n  function emptyElement(element) {\\n    if (!is.element(element)) return;\\n    let {\\n      length\\n    } = element.childNodes;\\n    while (length > 0) {\\n      element.removeChild(element.lastChild);\\n      length -= 1;\\n    }\\n  }\\n\\n  // Replace element\\n  function replaceElement(newChild, oldChild) {\\n    if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n    oldChild.parentNode.replaceChild(newChild, oldChild);\\n    return newChild;\\n  }\\n\\n  // Get an attribute object from a string selector\\n  function getAttributesFromSelector(sel, existingAttributes) {\\n    // For example:\\n    // '.test' to { class: 'test' }\\n    // '#test' to { id: 'test' }\\n    // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n    if (!is.string(sel) || is.empty(sel)) return {};\\n    const attributes = {};\\n    const existing = extend({}, existingAttributes);\\n    sel.split(',').forEach(s => {\\n      // Remove whitespace\\n      const selector = s.trim();\\n      const className = selector.replace('.', '');\\n      const stripped = selector.replace(/[[\\\\]]/g, '');\\n      // Get the parts and value\\n      const parts = stripped.split('=');\\n      const [key] = parts;\\n      const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n      // Get the first character\\n      const start = selector.charAt(0);\\n      switch (start) {\\n        case '.':\\n          // Add to existing classname\\n          if (is.string(existing.class)) {\\n            attributes.class = `${existing.class} ${className}`;\\n          } else {\\n            attributes.class = className;\\n          }\\n          break;\\n        case '#':\\n          // ID selector\\n          attributes.id = selector.replace('#', '');\\n          break;\\n        case '[':\\n          // Attribute selector\\n          attributes[key] = value;\\n          break;\\n      }\\n    });\\n    return extend(existing, attributes);\\n  }\\n\\n  // Toggle hidden\\n  function toggleHidden(element, hidden) {\\n    if (!is.element(element)) return;\\n    let hide = hidden;\\n    if (!is.boolean(hide)) {\\n      hide = !element.hidden;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    element.hidden = hide;\\n  }\\n\\n  // Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\n  function toggleClass(element, className, force) {\\n    if (is.nodeList(element)) {\\n      return Array.from(element).map(e => toggleClass(e, className, force));\\n    }\\n    if (is.element(element)) {\\n      let method = 'toggle';\\n      if (typeof force !== 'undefined') {\\n        method = force ? 'add' : 'remove';\\n      }\\n      element.classList[method](className);\\n      return element.classList.contains(className);\\n    }\\n    return false;\\n  }\\n\\n  // Has class name\\n  function hasClass(element, className) {\\n    return is.element(element) && element.classList.contains(className);\\n  }\\n\\n  // Element matches selector\\n  function matches(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n    function match() {\\n      return Array.from(document.querySelectorAll(selector)).includes(this);\\n    }\\n    const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n    return method.call(element, selector);\\n  }\\n\\n  // Closest ancestor element matching selector (also tests element itself)\\n  function closest$1(element, selector) {\\n    const {\\n      prototype\\n    } = Element;\\n\\n    // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n    function closestElement() {\\n      let el = this;\\n      do {\\n        if (matches.matches(el, selector)) return el;\\n        el = el.parentElement || el.parentNode;\\n      } while (el !== null && el.nodeType === 1);\\n      return null;\\n    }\\n    const method = prototype.closest || closestElement;\\n    return method.call(element, selector);\\n  }\\n\\n  // Find all elements\\n  function getElements(selector) {\\n    return this.elements.container.querySelectorAll(selector);\\n  }\\n\\n  // Find a single element\\n  function getElement(selector) {\\n    return this.elements.container.querySelector(selector);\\n  }\\n\\n  // Set focus and tab focus class\\n  function setFocus(element = null, focusVisible = false) {\\n    if (!is.element(element)) return;\\n\\n    // Set regular focus\\n    element.focus({\\n      preventScroll: true,\\n      focusVisible\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Default codecs for checking mimetype support\\n  const defaultCodecs = {\\n    'audio/ogg': 'vorbis',\\n    'audio/wav': '1',\\n    'video/webm': 'vp8, vorbis',\\n    'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n    'video/ogg': 'theora'\\n  };\\n\\n  // Check for feature support\\n  const support = {\\n    // Basic support\\n    audio: 'canPlayType' in document.createElement('audio'),\\n    video: 'canPlayType' in document.createElement('video'),\\n    // Check for support\\n    // Basic functionality vs full UI\\n    check(type, provider) {\\n      const api = support[type] || provider !== 'html5';\\n      const ui = api && support.rangeInput;\\n      return {\\n        api,\\n        ui\\n      };\\n    },\\n    // Picture-in-picture support\\n    // Safari & Chrome only currently\\n    pip: (() => {\\n      // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n      // It will throw the following error when trying to enter picture-in-picture\\n      // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n      if (browser.isIPhone) {\\n        return false;\\n      }\\n\\n      // Safari\\n      // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n      if (is.function(createElement('video').webkitSetPresentationMode)) {\\n        return true;\\n      }\\n\\n      // Chrome\\n      // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n      if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n        return true;\\n      }\\n      return false;\\n    })(),\\n    // Airplay support\\n    // Safari only currently\\n    airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n    // Inline playback support\\n    // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n    playsinline: 'playsInline' in document.createElement('video'),\\n    // Check for mime type support against a player instance\\n    // Credits: http://diveintohtml5.info/everything.html\\n    // Related: http://www.leanbackplayer.com/test/h5mt.html\\n    mime(input) {\\n      if (is.empty(input)) {\\n        return false;\\n      }\\n      const [mediaType] = input.split('/');\\n      let type = input;\\n\\n      // Verify we're using HTML5 and there's no media type mismatch\\n      if (!this.isHTML5 || mediaType !== this.type) {\\n        return false;\\n      }\\n\\n      // Add codec if required\\n      if (Object.keys(defaultCodecs).includes(type)) {\\n        type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n      }\\n      try {\\n        return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n      } catch (_) {\\n        return false;\\n      }\\n    },\\n    // Check for textTracks support\\n    textTracks: 'textTracks' in document.createElement('video'),\\n    // <input type=\\\"range\\\"> Sliders\\n    rangeInput: (() => {\\n      const range = document.createElement('input');\\n      range.type = 'range';\\n      return range.type === 'range';\\n    })(),\\n    // Touch\\n    // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n    touch: 'ontouchstart' in document.documentElement,\\n    // Detect transitions support\\n    transitions: transitionEndEvent !== false,\\n    // Reduced motion iOS & MacOS setting\\n    // https://webkit.org/blog/7551/responsive-design-for-motion/\\n    reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n  };\\n\\n  // ==========================================================================\\n\\n  // Check for passive event listener support\\n  // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n  // https://www.youtube.com/watch?v=NPM6172J22g\\n  const supportsPassiveListeners = (() => {\\n    // Test via a getter in the options object to see if the passive property is accessed\\n    let supported = false;\\n    try {\\n      const options = Object.defineProperty({}, 'passive', {\\n        get() {\\n          supported = true;\\n          return null;\\n        }\\n      });\\n      window.addEventListener('test', null, options);\\n      window.removeEventListener('test', null, options);\\n    } catch (_) {\\n      // Do nothing\\n    }\\n    return supported;\\n  })();\\n\\n  // Toggle event listener\\n  function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n    // Bail if no element, event, or callback\\n    if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n      return;\\n    }\\n\\n    // Allow multiple events\\n    const events = event.split(' ');\\n    // Build options\\n    // Default to just the capture boolean for browsers with no passive listener support\\n    let options = capture;\\n\\n    // If passive events listeners are supported\\n    if (supportsPassiveListeners) {\\n      options = {\\n        // Whether the listener can be passive (i.e. default never prevented)\\n        passive,\\n        // Whether the listener is a capturing listener or not\\n        capture\\n      };\\n    }\\n\\n    // If a single node is passed, bind the event listener\\n    events.forEach(type => {\\n      if (this && this.eventListeners && toggle) {\\n        // Cache event listener\\n        this.eventListeners.push({\\n          element,\\n          type,\\n          callback,\\n          options\\n        });\\n      }\\n      element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n    });\\n  }\\n\\n  // Bind event handler\\n  function on(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, true, passive, capture);\\n  }\\n\\n  // Unbind event handler\\n  function off(element, events = '', callback, passive = true, capture = false) {\\n    toggleListener.call(this, element, events, callback, false, passive, capture);\\n  }\\n\\n  // Bind once-only event handler\\n  function once(element, events = '', callback, passive = true, capture = false) {\\n    const onceCallback = (...args) => {\\n      off(element, events, onceCallback, passive, capture);\\n      callback.apply(this, args);\\n    };\\n    toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n  }\\n\\n  // Trigger event\\n  function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n    // Bail if no element\\n    if (!is.element(element) || is.empty(type)) {\\n      return;\\n    }\\n\\n    // Create and dispatch the event\\n    const event = new CustomEvent(type, {\\n      bubbles,\\n      detail: {\\n        ...detail,\\n        plyr: this\\n      }\\n    });\\n\\n    // Dispatch the event\\n    element.dispatchEvent(event);\\n  }\\n\\n  // Unbind all cached event listeners\\n  function unbindListeners() {\\n    if (this && this.eventListeners) {\\n      this.eventListeners.forEach(item => {\\n        const {\\n          element,\\n          type,\\n          callback,\\n          options\\n        } = item;\\n        element.removeEventListener(type, callback, options);\\n      });\\n      this.eventListeners = [];\\n    }\\n  }\\n\\n  // Run method when / if player is ready\\n  function ready() {\\n    return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n  }\\n\\n  /**\\n   * Silence a Promise-like object.\\n   * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n   * play promise\\\" rejection error messages.\\n   * @param  {Object} value An object that may or may not be `Promise`-like.\\n   */\\n  function silencePromise(value) {\\n    if (is.promise(value)) {\\n      value.then(null, () => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Remove duplicates in an array\\n  function dedupe(array) {\\n    if (!is.array(array)) {\\n      return array;\\n    }\\n    return array.filter((item, index) => array.indexOf(item) === index);\\n  }\\n\\n  // Get the closest value in an array\\n  function closest(array, value) {\\n    if (!is.array(array) || !array.length) {\\n      return null;\\n    }\\n    return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n  }\\n\\n  // ==========================================================================\\n\\n  // Check support for a CSS declaration\\n  function supportsCSS(declaration) {\\n    if (!window || !window.CSS) {\\n      return false;\\n    }\\n    return window.CSS.supports(declaration);\\n  }\\n\\n  // Standard/common aspect ratios\\n  const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n    ...out,\\n    [x / y]: [x, y]\\n  }), {});\\n\\n  // Validate an aspect ratio\\n  function validateAspectRatio(input) {\\n    if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n      return false;\\n    }\\n    const ratio = is.array(input) ? input : input.split(':');\\n    return ratio.map(Number).every(is.number);\\n  }\\n\\n  // Reduce an aspect ratio to it's lowest form\\n  function reduceAspectRatio(ratio) {\\n    if (!is.array(ratio) || !ratio.every(is.number)) {\\n      return null;\\n    }\\n    const [width, height] = ratio;\\n    const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n    const divider = getDivider(width, height);\\n    return [width / divider, height / divider];\\n  }\\n\\n  // Calculate an aspect ratio\\n  function getAspectRatio(input) {\\n    const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n    // Try provided ratio\\n    let ratio = parse(input);\\n\\n    // Get from config\\n    if (ratio === null) {\\n      ratio = parse(this.config.ratio);\\n    }\\n\\n    // Get from embed\\n    if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n      ({\\n        ratio\\n      } = this.embed);\\n    }\\n\\n    // Get from HTML5 video\\n    if (ratio === null && this.isHTML5) {\\n      const {\\n        videoWidth,\\n        videoHeight\\n      } = this.media;\\n      ratio = [videoWidth, videoHeight];\\n    }\\n    return reduceAspectRatio(ratio);\\n  }\\n\\n  // Set aspect ratio for responsive container\\n  function setAspectRatio(input) {\\n    if (!this.isVideo) {\\n      return {};\\n    }\\n    const {\\n      wrapper\\n    } = this.elements;\\n    const ratio = getAspectRatio.call(this, input);\\n    if (!is.array(ratio)) {\\n      return {};\\n    }\\n    const [x, y] = reduceAspectRatio(ratio);\\n    const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n    const padding = 100 / x * y;\\n    if (useNative) {\\n      wrapper.style.aspectRatio = `${x}/${y}`;\\n    } else {\\n      wrapper.style.paddingBottom = `${padding}%`;\\n    }\\n\\n    // For Vimeo we have an extra <div> to hide the standard controls and UI\\n    if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n      const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n      const offset = (height - padding) / (height / 50);\\n      if (this.fullscreen.active) {\\n        wrapper.style.paddingBottom = null;\\n      } else {\\n        this.media.style.transform = `translateY(-${offset}%)`;\\n      }\\n    } else if (this.isHTML5) {\\n      wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n    }\\n    return {\\n      padding,\\n      ratio\\n    };\\n  }\\n\\n  // Round an aspect ratio to closest standard ratio\\n  function roundAspectRatio(x, y, tolerance = 0.05) {\\n    const ratio = x / y;\\n    const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n    // Check match is within tolerance\\n    if (Math.abs(closestRatio - ratio) <= tolerance) {\\n      return standardRatios[closestRatio];\\n    }\\n\\n    // No match\\n    return [x, y];\\n  }\\n\\n  // Get the size of the viewport\\n  // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\n  function getViewportSize() {\\n    const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n    const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n    return [width, height];\\n  }\\n\\n  // ==========================================================================\\n  const html5 = {\\n    getSources() {\\n      if (!this.isHTML5) {\\n        return [];\\n      }\\n      const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n      // Filter out unsupported sources (if type is specified)\\n      return sources.filter(source => {\\n        const type = source.getAttribute('type');\\n        if (is.empty(type)) {\\n          return true;\\n        }\\n        return support.mime.call(this, type);\\n      });\\n    },\\n    // Get quality levels\\n    getQualityOptions() {\\n      // Whether we're forcing all options (e.g. for streaming)\\n      if (this.config.quality.forced) {\\n        return this.config.quality.options;\\n      }\\n\\n      // Get sizes from <source> elements\\n      return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n    },\\n    setup() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n      const player = this;\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set aspect ratio if fixed\\n      if (!is.empty(this.config.ratio)) {\\n        setAspectRatio.call(player);\\n      }\\n\\n      // Quality\\n      Object.defineProperty(player.media, 'quality', {\\n        get() {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n          // Return size, if match is found\\n          return source && Number(source.getAttribute('size'));\\n        },\\n        set(input) {\\n          if (player.quality === input) {\\n            return;\\n          }\\n\\n          // If we're using an external handler...\\n          if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n            player.config.quality.onChange(input);\\n          } else {\\n            // Get sources\\n            const sources = html5.getSources.call(player);\\n            // Get first match for requested size\\n            const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n            // No matching source found\\n            if (!source) {\\n              return;\\n            }\\n\\n            // Get current state\\n            const {\\n              currentTime,\\n              paused,\\n              preload,\\n              readyState,\\n              playbackRate\\n            } = player.media;\\n\\n            // Set new source\\n            player.media.src = source.getAttribute('src');\\n\\n            // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n            if (preload !== 'none' || readyState) {\\n              // Restore time\\n              player.once('loadedmetadata', () => {\\n                player.speed = playbackRate;\\n                player.currentTime = currentTime;\\n\\n                // Resume playing\\n                if (!paused) {\\n                  silencePromise(player.play());\\n                }\\n              });\\n\\n              // Load new source\\n              player.media.load();\\n            }\\n          }\\n\\n          // Trigger change event\\n          triggerEvent.call(player, player.media, 'qualitychange', false, {\\n            quality: input\\n          });\\n        }\\n      });\\n    },\\n    // Cancel current network requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    cancelRequests() {\\n      if (!this.isHTML5) {\\n        return;\\n      }\\n\\n      // Remove child sources\\n      removeElement(html5.getSources.call(this));\\n\\n      // Set blank video src attribute\\n      // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n      // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n      this.media.setAttribute('src', this.config.blankVideo);\\n\\n      // Load the new empty source\\n      // This will cancel existing requests\\n      // See https://github.com/sampotts/plyr/issues/174\\n      this.media.load();\\n\\n      // Debugging\\n      this.debug.log('Cancelled network requests');\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Generate a random ID\\n  function generateId(prefix) {\\n    return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n  }\\n\\n  // Format string\\n  function format(input, ...args) {\\n    if (is.empty(input)) return input;\\n    return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n  }\\n\\n  // Get percentage\\n  function getPercentage(current, max) {\\n    if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n      return 0;\\n    }\\n    return (current / max * 100).toFixed(2);\\n  }\\n\\n  // Replace all occurrences of a string in a string\\n  const replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n  // Convert to title case\\n  const toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n  // Convert string to pascalCase\\n  function toPascalCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert kebab case\\n    string = replaceAll(string, '-', ' ');\\n\\n    // Convert snake case\\n    string = replaceAll(string, '_', ' ');\\n\\n    // Convert to title case\\n    string = toTitleCase(string);\\n\\n    // Convert to pascal case\\n    return replaceAll(string, ' ', '');\\n  }\\n\\n  // Convert string to pascalCase\\n  function toCamelCase(input = '') {\\n    let string = input.toString();\\n\\n    // Convert to pascal case\\n    string = toPascalCase(string);\\n\\n    // Convert first character to lowercase\\n    return string.charAt(0).toLowerCase() + string.slice(1);\\n  }\\n\\n  // Remove HTML from a string\\n  function stripHTML(source) {\\n    const fragment = document.createDocumentFragment();\\n    const element = document.createElement('div');\\n    fragment.appendChild(element);\\n    element.innerHTML = source;\\n    return fragment.firstChild.innerText;\\n  }\\n\\n  // Like outerHTML, but also works for DocumentFragment\\n  function getHTML(element) {\\n    const wrapper = document.createElement('div');\\n    wrapper.appendChild(element);\\n    return wrapper.innerHTML;\\n  }\\n\\n  // ==========================================================================\\n\\n  // Skip i18n for abbreviations and brand names\\n  const resources = {\\n    pip: 'PIP',\\n    airplay: 'AirPlay',\\n    html5: 'HTML5',\\n    vimeo: 'Vimeo',\\n    youtube: 'YouTube'\\n  };\\n  const i18n = {\\n    get(key = '', config = {}) {\\n      if (is.empty(key) || is.empty(config)) {\\n        return '';\\n      }\\n      let string = getDeep(config.i18n, key);\\n      if (is.empty(string)) {\\n        if (Object.keys(resources).includes(key)) {\\n          return resources[key];\\n        }\\n        return '';\\n      }\\n      const replace = {\\n        '{seektime}': config.seekTime,\\n        '{title}': config.title\\n      };\\n      Object.entries(replace).forEach(([k, v]) => {\\n        string = replaceAll(string, k, v);\\n      });\\n      return string;\\n    }\\n  };\\n\\n  class Storage {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"get\\\", key => {\\n        if (!Storage.supported || !this.enabled) {\\n          return null;\\n        }\\n        const store = window.localStorage.getItem(this.key);\\n        if (is.empty(store)) {\\n          return null;\\n        }\\n        const json = JSON.parse(store);\\n        return is.string(key) && key.length ? json[key] : json;\\n      });\\n      _defineProperty$1(this, \\\"set\\\", object => {\\n        // Bail if we don't have localStorage support or it's disabled\\n        if (!Storage.supported || !this.enabled) {\\n          return;\\n        }\\n\\n        // Can only store objectst\\n        if (!is.object(object)) {\\n          return;\\n        }\\n\\n        // Get current storage\\n        let storage = this.get();\\n\\n        // Default to empty object\\n        if (is.empty(storage)) {\\n          storage = {};\\n        }\\n\\n        // Update the working copy of the values\\n        extend(storage, object);\\n\\n        // Update storage\\n        try {\\n          window.localStorage.setItem(this.key, JSON.stringify(storage));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      });\\n      this.enabled = player.config.storage.enabled;\\n      this.key = player.config.storage.key;\\n    }\\n\\n    // Check for actual support (see if we can use it)\\n    static get supported() {\\n      try {\\n        if (!('localStorage' in window)) {\\n          return false;\\n        }\\n        const test = '___test';\\n\\n        // Try to use it (it might be disabled, e.g. user is in private mode)\\n        // see: https://github.com/sampotts/plyr/issues/131\\n        window.localStorage.setItem(test, test);\\n        window.localStorage.removeItem(test);\\n        return true;\\n      } catch (_) {\\n        return false;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Fetch wrapper\\n  // Using XHR to avoid issues with older browsers\\n  // ==========================================================================\\n\\n  function fetch(url, responseType = 'text') {\\n    return new Promise((resolve, reject) => {\\n      try {\\n        const request = new XMLHttpRequest();\\n\\n        // Check for CORS support\\n        if (!('withCredentials' in request)) {\\n          return;\\n        }\\n        request.addEventListener('load', () => {\\n          if (responseType === 'text') {\\n            try {\\n              resolve(JSON.parse(request.responseText));\\n            } catch (_) {\\n              resolve(request.responseText);\\n            }\\n          } else {\\n            resolve(request.response);\\n          }\\n        });\\n        request.addEventListener('error', () => {\\n          throw new Error(request.status);\\n        });\\n        request.open('GET', url, true);\\n\\n        // Set the required response type\\n        request.responseType = responseType;\\n        request.send();\\n      } catch (error) {\\n        reject(error);\\n      }\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Load an external SVG sprite\\n  function loadSprite(url, id) {\\n    if (!is.string(url)) {\\n      return;\\n    }\\n    const prefix = 'cache';\\n    const hasId = is.string(id);\\n    let isCached = false;\\n    const exists = () => document.getElementById(id) !== null;\\n    const update = (container, data) => {\\n      // eslint-disable-next-line no-param-reassign\\n      container.innerHTML = data;\\n\\n      // Check again incase of race condition\\n      if (hasId && exists()) {\\n        return;\\n      }\\n\\n      // Inject the SVG to the body\\n      document.body.insertAdjacentElement('afterbegin', container);\\n    };\\n\\n    // Only load once if ID set\\n    if (!hasId || !exists()) {\\n      const useStorage = Storage.supported;\\n      // Create container\\n      const container = document.createElement('div');\\n      container.setAttribute('hidden', '');\\n      if (hasId) {\\n        container.setAttribute('id', id);\\n      }\\n\\n      // Check in cache\\n      if (useStorage) {\\n        const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n        isCached = cached !== null;\\n        if (isCached) {\\n          const data = JSON.parse(cached);\\n          update(container, data.content);\\n        }\\n      }\\n\\n      // Get the sprite\\n      fetch(url).then(result => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n              content: result\\n            }));\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n        update(container, result);\\n      }).catch(() => {});\\n    }\\n  }\\n\\n  // ==========================================================================\\n\\n  // Time helpers\\n  const getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\n  const getMinutes = value => Math.trunc(value / 60 % 60, 10);\\n  const getSeconds = value => Math.trunc(value % 60, 10);\\n\\n  // Format time to UI friendly string\\n  function formatTime(time = 0, displayHours = false, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return formatTime(undefined, displayHours, inverted);\\n    }\\n\\n    // Format time component to add leading zero\\n    const format = value => `0${value}`.slice(-2);\\n    // Breakdown to hours, mins, secs\\n    let hours = getHours(time);\\n    const mins = getMinutes(time);\\n    const secs = getSeconds(time);\\n\\n    // Do we need to display hours?\\n    if (displayHours || hours > 0) {\\n      hours = `${hours}:`;\\n    } else {\\n      hours = '';\\n    }\\n\\n    // Render\\n    return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n  }\\n\\n  // ==========================================================================\\n\\n  // TODO: Don't export a massive object - break down and create class\\n  const controls = {\\n    // Get icon URL\\n    getIconUrl() {\\n      const url = new URL(this.config.iconUrl, window.location);\\n      const host = window.location.host ? window.location.host : window.top.location.host;\\n      const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n      return {\\n        url: this.config.iconUrl,\\n        cors\\n      };\\n    },\\n    // Find the UI controls\\n    findElements() {\\n      try {\\n        this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n        // Buttons\\n        this.elements.buttons = {\\n          play: getElements.call(this, this.config.selectors.buttons.play),\\n          pause: getElement.call(this, this.config.selectors.buttons.pause),\\n          restart: getElement.call(this, this.config.selectors.buttons.restart),\\n          rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n          fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n          mute: getElement.call(this, this.config.selectors.buttons.mute),\\n          pip: getElement.call(this, this.config.selectors.buttons.pip),\\n          airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n          settings: getElement.call(this, this.config.selectors.buttons.settings),\\n          captions: getElement.call(this, this.config.selectors.buttons.captions),\\n          fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n        };\\n\\n        // Progress\\n        this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n        // Inputs\\n        this.elements.inputs = {\\n          seek: getElement.call(this, this.config.selectors.inputs.seek),\\n          volume: getElement.call(this, this.config.selectors.inputs.volume)\\n        };\\n\\n        // Display\\n        this.elements.display = {\\n          buffer: getElement.call(this, this.config.selectors.display.buffer),\\n          currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n          duration: getElement.call(this, this.config.selectors.display.duration)\\n        };\\n\\n        // Seek tooltip\\n        if (is.element(this.elements.progress)) {\\n          this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n        }\\n        return true;\\n      } catch (error) {\\n        // Log it\\n        this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n        // Restore native video controls\\n        this.toggleNativeControls(true);\\n        return false;\\n      }\\n    },\\n    // Create <svg> icon\\n    createIcon(type, attributes) {\\n      const namespace = 'http://www.w3.org/2000/svg';\\n      const iconUrl = controls.getIconUrl.call(this);\\n      const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n      // Create <svg>\\n      const icon = document.createElementNS(namespace, 'svg');\\n      setAttributes(icon, extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false'\\n      }));\\n\\n      // Create the <use> to reference sprite\\n      const use = document.createElementNS(namespace, 'use');\\n      const path = `${iconPath}-${type}`;\\n\\n      // Set `href` attributes\\n      // https://github.com/sampotts/plyr/issues/460\\n      // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n      if ('href' in use) {\\n        use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n      }\\n\\n      // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n      // Add <use> to <svg>\\n      icon.appendChild(use);\\n      return icon;\\n    },\\n    // Create hidden text label\\n    createLabel(key, attr = {}) {\\n      const text = i18n.get(key, this.config);\\n      const attributes = {\\n        ...attr,\\n        class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n      };\\n      return createElement('span', attributes, text);\\n    },\\n    // Create a badge\\n    createBadge(text) {\\n      if (is.empty(text)) {\\n        return null;\\n      }\\n      const badge = createElement('span', {\\n        class: this.config.classNames.menu.value\\n      });\\n      badge.appendChild(createElement('span', {\\n        class: this.config.classNames.menu.badge\\n      }, text));\\n      return badge;\\n    },\\n    // Create a <button>\\n    createButton(buttonType, attr) {\\n      const attributes = extend({}, attr);\\n      let type = toCamelCase(buttonType);\\n      const props = {\\n        element: 'button',\\n        toggle: false,\\n        label: null,\\n        icon: null,\\n        labelPressed: null,\\n        iconPressed: null\\n      };\\n      ['element', 'icon', 'label'].forEach(key => {\\n        if (Object.keys(attributes).includes(key)) {\\n          props[key] = attributes[key];\\n          delete attributes[key];\\n        }\\n      });\\n\\n      // Default to 'button' type to prevent form submission\\n      if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n        attributes.type = 'button';\\n      }\\n\\n      // Set class name\\n      if (Object.keys(attributes).includes('class')) {\\n        if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n          extend(attributes, {\\n            class: `${attributes.class} ${this.config.classNames.control}`\\n          });\\n        }\\n      } else {\\n        attributes.class = this.config.classNames.control;\\n      }\\n\\n      // Large play button\\n      switch (buttonType) {\\n        case 'play':\\n          props.toggle = true;\\n          props.label = 'play';\\n          props.labelPressed = 'pause';\\n          props.icon = 'play';\\n          props.iconPressed = 'pause';\\n          break;\\n        case 'mute':\\n          props.toggle = true;\\n          props.label = 'mute';\\n          props.labelPressed = 'unmute';\\n          props.icon = 'volume';\\n          props.iconPressed = 'muted';\\n          break;\\n        case 'captions':\\n          props.toggle = true;\\n          props.label = 'enableCaptions';\\n          props.labelPressed = 'disableCaptions';\\n          props.icon = 'captions-off';\\n          props.iconPressed = 'captions-on';\\n          break;\\n        case 'fullscreen':\\n          props.toggle = true;\\n          props.label = 'enterFullscreen';\\n          props.labelPressed = 'exitFullscreen';\\n          props.icon = 'enter-fullscreen';\\n          props.iconPressed = 'exit-fullscreen';\\n          break;\\n        case 'play-large':\\n          attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n          type = 'play';\\n          props.label = 'play';\\n          props.icon = 'play';\\n          break;\\n        default:\\n          if (is.empty(props.label)) {\\n            props.label = type;\\n          }\\n          if (is.empty(props.icon)) {\\n            props.icon = buttonType;\\n          }\\n      }\\n      const button = createElement(props.element);\\n\\n      // Setup toggle icon and labels\\n      if (props.toggle) {\\n        // Icon\\n        button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed'\\n        }));\\n        button.appendChild(controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed'\\n        }));\\n\\n        // Label/Tooltip\\n        button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed'\\n        }));\\n        button.appendChild(controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed'\\n        }));\\n      } else {\\n        button.appendChild(controls.createIcon.call(this, props.icon));\\n        button.appendChild(controls.createLabel.call(this, props.label));\\n      }\\n\\n      // Merge and set attributes\\n      extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n      setAttributes(button, attributes);\\n\\n      // We have multiple play buttons\\n      if (type === 'play') {\\n        if (!is.array(this.elements.buttons[type])) {\\n          this.elements.buttons[type] = [];\\n        }\\n        this.elements.buttons[type].push(button);\\n      } else {\\n        this.elements.buttons[type] = button;\\n      }\\n      return button;\\n    },\\n    // Create an <input type='range'>\\n    createRange(type, attributes) {\\n      // Seek input\\n      const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n        type: 'range',\\n        min: 0,\\n        max: 100,\\n        step: 0.01,\\n        value: 0,\\n        autocomplete: 'off',\\n        // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n        role: 'slider',\\n        'aria-label': i18n.get(type, this.config),\\n        'aria-valuemin': 0,\\n        'aria-valuemax': 100,\\n        'aria-valuenow': 0\\n      }, attributes));\\n      this.elements.inputs[type] = input;\\n\\n      // Set the fill for webkit now\\n      controls.updateRangeFill.call(this, input);\\n\\n      // Improve support on touch devices\\n      RangeTouch.setup(input);\\n      return input;\\n    },\\n    // Create a <progress>\\n    createProgress(type, attributes) {\\n      const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n        min: 0,\\n        max: 100,\\n        value: 0,\\n        role: 'progressbar',\\n        'aria-hidden': true\\n      }, attributes));\\n\\n      // Create the label inside\\n      if (type !== 'volume') {\\n        progress.appendChild(createElement('span', null, '0'));\\n        const suffixKey = {\\n          played: 'played',\\n          buffer: 'buffered'\\n        }[type];\\n        const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n        progress.innerText = `% ${suffix.toLowerCase()}`;\\n      }\\n      this.elements.display[type] = progress;\\n      return progress;\\n    },\\n    // Create time display\\n    createTime(type, attrs) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n      const container = createElement('div', extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer'\\n      }), '00:00');\\n\\n      // Reference for updates\\n      this.elements.display[type] = container;\\n      return container;\\n    },\\n    // Bind keyboard shortcuts for a menu item\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    bindMenuItemShortcuts(menuItem, type) {\\n      // Navigate through menus via arrow keys and space\\n      on.call(this, menuItem, 'keydown keyup', event => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n              target = menuItem.nextElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      }, false);\\n\\n      // Enter will fire a `click` event but we still need to manage focus\\n      // So we bind to keyup which fires after and set focus here\\n      on.call(this, menuItem, 'keyup', event => {\\n        if (event.key !== 'Return') return;\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      });\\n    },\\n    // Create a settings menu item\\n    createMenuItem({\\n      value,\\n      list,\\n      type,\\n      title,\\n      badge = null,\\n      checked = false\\n    }) {\\n      const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n      const menuItem = createElement('button', extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value\\n      }));\\n      const flex = createElement('span');\\n\\n      // We have to set as HTML incase of special characters\\n      flex.innerHTML = title;\\n      if (is.element(badge)) {\\n        flex.appendChild(badge);\\n      }\\n      menuItem.appendChild(flex);\\n\\n      // Replicate radio button behavior\\n      Object.defineProperty(menuItem, 'checked', {\\n        enumerable: true,\\n        get() {\\n          return menuItem.getAttribute('aria-checked') === 'true';\\n        },\\n        set(check) {\\n          // Ensure exclusivity\\n          if (check) {\\n            Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n          }\\n          menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n        }\\n      });\\n      this.listeners.bind(menuItem, 'click keyup', event => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n        event.preventDefault();\\n        event.stopPropagation();\\n        menuItem.checked = true;\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n        }\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      }, type, false);\\n      controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n      list.appendChild(menuItem);\\n    },\\n    // Format a time for display\\n    formatTime(time = 0, inverted = false) {\\n      // Bail if the value isn't a number\\n      if (!is.number(time)) {\\n        return time;\\n      }\\n\\n      // Always display hours if duration is over an hour\\n      const forceHours = getHours(this.duration) > 0;\\n      return formatTime(time, forceHours, inverted);\\n    },\\n    // Update the displayed time\\n    updateTimeDisplay(target = null, time = 0, inverted = false) {\\n      // Bail if there's no element to display or the value isn't a number\\n      if (!is.element(target) || !is.number(time)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line no-param-reassign\\n      target.innerText = controls.formatTime(time, inverted);\\n    },\\n    // Update volume UI and storage\\n    updateVolume() {\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Update range\\n      if (is.element(this.elements.inputs.volume)) {\\n        controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n      }\\n\\n      // Update mute state\\n      if (is.element(this.elements.buttons.mute)) {\\n        this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n      }\\n    },\\n    // Update seek value and lower fill\\n    setRange(target, value = 0) {\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // eslint-disable-next-line\\n      target.value = value;\\n\\n      // Webkit range fill\\n      controls.updateRangeFill.call(this, target);\\n    },\\n    // Update <progress> elements\\n    updateProgress(event) {\\n      if (!this.supported.ui || !is.event(event)) {\\n        return;\\n      }\\n      let value = 0;\\n      const setProgress = (target, input) => {\\n        const val = is.number(input) ? input : 0;\\n        const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n        // Update value and label\\n        if (is.element(progress)) {\\n          progress.value = val;\\n\\n          // Update text label inside\\n          const label = progress.getElementsByTagName('span')[0];\\n          if (is.element(label)) {\\n            label.childNodes[0].nodeValue = val;\\n          }\\n        }\\n      };\\n      if (event) {\\n        switch (event.type) {\\n          // Video playing\\n          case 'timeupdate':\\n          case 'seeking':\\n          case 'seeked':\\n            value = getPercentage(this.currentTime, this.duration);\\n\\n            // Set seek range value only if it's a 'natural' time event\\n            if (event.type === 'timeupdate') {\\n              controls.setRange.call(this, this.elements.inputs.seek, value);\\n            }\\n            break;\\n\\n          // Check buffer status\\n          case 'playing':\\n          case 'progress':\\n            setProgress(this.elements.display.buffer, this.buffered * 100);\\n            break;\\n        }\\n      }\\n    },\\n    // Webkit polyfill for lower fill range\\n    updateRangeFill(target) {\\n      // Get range from event if event passed\\n      const range = is.event(target) ? target.target : target;\\n\\n      // Needs to be a valid <input type='range'>\\n      if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n        return;\\n      }\\n\\n      // Set aria values for https://github.com/sampotts/plyr/issues/905\\n      if (matches(range, this.config.selectors.inputs.seek)) {\\n        range.setAttribute('aria-valuenow', this.currentTime);\\n        const currentTime = controls.formatTime(this.currentTime);\\n        const duration = controls.formatTime(this.duration);\\n        const format = i18n.get('seekLabel', this.config);\\n        range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n      } else if (matches(range, this.config.selectors.inputs.volume)) {\\n        const percent = range.value * 100;\\n        range.setAttribute('aria-valuenow', percent);\\n        range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n      } else {\\n        range.setAttribute('aria-valuenow', range.value);\\n      }\\n\\n      // WebKit only\\n      if (!browser.isWebKit && !browser.isIPadOS) {\\n        return;\\n      }\\n\\n      // Set CSS custom property\\n      range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n    },\\n    // Update hover tooltip for seeking\\n    updateSeekTooltip(event) {\\n      var _this$config$markers, _this$config$markers$;\\n      // Bail if setting not true\\n      if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n        return;\\n      }\\n      const tipElement = this.elements.display.seekTooltip;\\n      const visible = `${this.config.classNames.tooltip}--visible`;\\n      const toggle = show => toggleClass(tipElement, visible, show);\\n\\n      // Hide on touch\\n      if (this.touch) {\\n        toggle(false);\\n        return;\\n      }\\n\\n      // Determine percentage, if already visible\\n      let percent = 0;\\n      const clientRect = this.elements.progress.getBoundingClientRect();\\n      if (is.event(event)) {\\n        percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n      } else if (hasClass(tipElement, visible)) {\\n        percent = parseFloat(tipElement.style.left, 10);\\n      } else {\\n        return;\\n      }\\n\\n      // Set bounds\\n      if (percent < 0) {\\n        percent = 0;\\n      } else if (percent > 100) {\\n        percent = 100;\\n      }\\n      const time = this.duration / 100 * percent;\\n\\n      // Display the time a click would seek to\\n      tipElement.innerText = controls.formatTime(time);\\n\\n      // Get marker point for time\\n      const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n        time: t\\n      }) => t === Math.round(time));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n\\n      // Set position\\n      tipElement.style.left = `${percent}%`;\\n\\n      // Show/hide the tooltip\\n      // If the event is a moues in/out and percentage is inside bounds\\n      if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n        toggle(event.type === 'mouseenter');\\n      }\\n    },\\n    // Handle time change event\\n    timeUpdate(event) {\\n      // Only invert if only one time element is displayed and used for both duration and currentTime\\n      const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n      // Duration\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n      // Ignore updates while seeking\\n      if (event && event.type === 'timeupdate' && this.media.seeking) {\\n        return;\\n      }\\n\\n      // Playing progress\\n      controls.updateProgress.call(this, event);\\n    },\\n    // Show the duration on metadataloaded or durationchange events\\n    durationUpdate() {\\n      // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n      if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n        return;\\n      }\\n\\n      // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n      // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n      // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n      // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n      if (this.duration >= 2 ** 32) {\\n        toggleHidden(this.elements.display.currentTime, true);\\n        toggleHidden(this.elements.progress, true);\\n        return;\\n      }\\n\\n      // Update ARIA values\\n      if (is.element(this.elements.inputs.seek)) {\\n        this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n      }\\n\\n      // If there's a spot to display duration\\n      const hasDuration = is.element(this.elements.display.duration);\\n\\n      // If there's only one time display, display duration there\\n      if (!hasDuration && this.config.displayDuration && this.paused) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n      }\\n\\n      // If there's a duration element, update content\\n      if (hasDuration) {\\n        controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n      }\\n      if (this.config.markers.enabled) {\\n        controls.setMarkers.call(this);\\n      }\\n\\n      // Update the tooltip (if visible)\\n      controls.updateSeekTooltip.call(this);\\n    },\\n    // Hide/show a tab\\n    toggleMenuButton(setting, toggle) {\\n      toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n    },\\n    // Update the selected setting\\n    updateSetting(setting, container, input) {\\n      const pane = this.elements.settings.panels[setting];\\n      let value = null;\\n      let list = container;\\n      if (setting === 'captions') {\\n        value = this.currentTrack;\\n      } else {\\n        value = !is.empty(input) ? input : this[setting];\\n\\n        // Get default\\n        if (is.empty(value)) {\\n          value = this.config[setting].default;\\n        }\\n\\n        // Unsupported value\\n        if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n          this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n          return;\\n        }\\n\\n        // Disabled value\\n        if (!this.config[setting].options.includes(value)) {\\n          this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n          return;\\n        }\\n      }\\n\\n      // Get the list if we need to\\n      if (!is.element(list)) {\\n        list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n      }\\n\\n      // If there's no list it means it's not been rendered...\\n      if (!is.element(list)) {\\n        return;\\n      }\\n\\n      // Update the label\\n      const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n      label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n      // Find the radio option and check it\\n      const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n      if (is.element(target)) {\\n        target.checked = true;\\n      }\\n    },\\n    // Translate a value into a nice label\\n    getLabel(setting, value) {\\n      switch (setting) {\\n        case 'speed':\\n          return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n        case 'quality':\\n          if (is.number(value)) {\\n            const label = i18n.get(`qualityLabel.${value}`, this.config);\\n            if (!label.length) {\\n              return `${value}p`;\\n            }\\n            return label;\\n          }\\n          return toTitleCase(value);\\n        case 'captions':\\n          return captions.getLabel.call(this);\\n        default:\\n          return null;\\n      }\\n    },\\n    // Set the quality menu\\n    setQualityMenu(options) {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.quality)) {\\n        return;\\n      }\\n      const type = 'quality';\\n      const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Set options if passed and filter based on uniqueness and config\\n      if (is.array(options)) {\\n        this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n      }\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Get the badge HTML for HD, 4K etc\\n      const getBadge = quality => {\\n        const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n        if (!label.length) {\\n          return null;\\n        }\\n        return controls.createBadge.call(this, label);\\n      };\\n\\n      // Sort options by the config and then render options\\n      this.options.quality.sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      }).forEach(quality => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set the looping options\\n    /* setLoopMenu() {\\n          // Menu required\\n          if (!is.element(this.elements.settings.panels.loop)) {\\n              return;\\n          }\\n           const options = ['start', 'end', 'all', 'reset'];\\n          const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n           // Show the pane and tab\\n          toggleHidden(this.elements.settings.buttons.loop, false);\\n          toggleHidden(this.elements.settings.panels.loop, false);\\n           // Toggle the pane and tab\\n          const toggle = !is.empty(this.loop.options);\\n          controls.toggleMenuButton.call(this, 'loop', toggle);\\n           // Empty the menu\\n          emptyElement(list);\\n           options.forEach(option => {\\n              const item = createElement('li');\\n               const button = createElement(\\n                  'button',\\n                  extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                      type: 'button',\\n                      class: this.config.classNames.control,\\n                      'data-plyr-loop-action': option,\\n                  }),\\n                  i18n.get(option, this.config)\\n              );\\n               if (['start', 'end'].includes(option)) {\\n                  const badge = controls.createBadge.call(this, '00:00');\\n                  button.appendChild(badge);\\n              }\\n               item.appendChild(button);\\n              list.appendChild(item);\\n          });\\n      }, */\\n\\n    // Get current selected caption language\\n    // TODO: rework this to user the getter in the API?\\n\\n    // Set a list of available captions languages\\n    setCaptionsMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.captions)) {\\n        return;\\n      }\\n\\n      // TODO: Captions or language? Currently it's mixed\\n      const type = 'captions';\\n      const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n      const tracks = captions.getTracks.call(this);\\n      const toggle = Boolean(tracks.length);\\n\\n      // Toggle the pane and tab\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If there's no captions, bail\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Generate options data\\n      const options = tracks.map((track, value) => ({\\n        value,\\n        checked: this.captions.toggled && this.currentTrack === value,\\n        title: captions.getLabel.call(this, track),\\n        badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n        list,\\n        type: 'language'\\n      }));\\n\\n      // Add the \\\"Disabled\\\" option to turn off captions\\n      options.unshift({\\n        value: -1,\\n        checked: !this.captions.toggled,\\n        title: i18n.get('disabled', this.config),\\n        list,\\n        type: 'language'\\n      });\\n\\n      // Generate options\\n      options.forEach(controls.createMenuItem.bind(this));\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Set a list of available captions languages\\n    setSpeedMenu() {\\n      // Menu required\\n      if (!is.element(this.elements.settings.panels.speed)) {\\n        return;\\n      }\\n      const type = 'speed';\\n      const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n      // Filter out invalid speeds\\n      this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n      // Toggle the pane and tab\\n      const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n      controls.toggleMenuButton.call(this, type, toggle);\\n\\n      // Empty the menu\\n      emptyElement(list);\\n\\n      // Check if we need to toggle the parent\\n      controls.checkMenu.call(this);\\n\\n      // If we're hiding, nothing more to do\\n      if (!toggle) {\\n        return;\\n      }\\n\\n      // Create items\\n      this.options.speed.forEach(speed => {\\n        controls.createMenuItem.call(this, {\\n          value: speed,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'speed', speed)\\n        });\\n      });\\n      controls.updateSetting.call(this, type, list);\\n    },\\n    // Check if we need to hide/show the settings menu\\n    checkMenu() {\\n      const {\\n        buttons\\n      } = this.elements.settings;\\n      const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n      toggleHidden(this.elements.settings.menu, !visible);\\n    },\\n    // Focus the first menu item in a given (or visible) menu\\n    focusFirstMenuItem(pane, focusVisible = false) {\\n      if (this.elements.settings.popup.hidden) {\\n        return;\\n      }\\n      let target = pane;\\n      if (!is.element(target)) {\\n        target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n      }\\n      const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n      setFocus.call(this, firstItem, focusVisible);\\n    },\\n    // Show/hide menu\\n    toggleMenu(input) {\\n      const {\\n        popup\\n      } = this.elements.settings;\\n      const button = this.elements.buttons.settings;\\n\\n      // Menu and button are required\\n      if (!is.element(popup) || !is.element(button)) {\\n        return;\\n      }\\n\\n      // True toggle by default\\n      const {\\n        hidden\\n      } = popup;\\n      let show = hidden;\\n      if (is.boolean(input)) {\\n        show = input;\\n      } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n        show = false;\\n      } else if (is.event(input)) {\\n        // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n        // Element in the shadowDOM. The path, if available, is complete.\\n        const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n        const isMenuItem = popup.contains(target);\\n\\n        // If the click was inside the menu or if the click\\n        // wasn't the button or menu item and we're trying to\\n        // show the menu (a doc click shouldn't show the menu)\\n        if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n          return;\\n        }\\n      }\\n\\n      // Set button attributes\\n      button.setAttribute('aria-expanded', show);\\n\\n      // Show the actual popup\\n      toggleHidden(popup, !show);\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n      // Focus the first item if key interaction\\n      if (show && is.keyboardEvent(input)) {\\n        controls.focusFirstMenuItem.call(this, null, true);\\n      } else if (!show && !hidden) {\\n        // If closing, re-focus the button\\n        setFocus.call(this, button, is.keyboardEvent(input));\\n      }\\n    },\\n    // Get the natural size of a menu panel\\n    getMenuSize(tab) {\\n      const clone = tab.cloneNode(true);\\n      clone.style.position = 'absolute';\\n      clone.style.opacity = 0;\\n      clone.removeAttribute('hidden');\\n\\n      // Append to parent so we get the \\\"real\\\" size\\n      tab.parentNode.appendChild(clone);\\n\\n      // Get the sizes before we remove\\n      const width = clone.scrollWidth;\\n      const height = clone.scrollHeight;\\n\\n      // Remove from the DOM\\n      removeElement(clone);\\n      return {\\n        width,\\n        height\\n      };\\n    },\\n    // Show a panel in the menu\\n    showMenuPanel(type = '', focusVisible = false) {\\n      const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n      // Nothing to show, bail\\n      if (!is.element(target)) {\\n        return;\\n      }\\n\\n      // Hide all other panels\\n      const container = target.parentNode;\\n      const current = Array.from(container.children).find(node => !node.hidden);\\n\\n      // If we can do fancy animations, we'll animate the height/width\\n      if (support.transitions && !support.reducedMotion) {\\n        // Set the current width as a base\\n        container.style.width = `${current.scrollWidth}px`;\\n        container.style.height = `${current.scrollHeight}px`;\\n\\n        // Get potential sizes\\n        const size = controls.getMenuSize.call(this, target);\\n\\n        // Restore auto height/width\\n        const restore = event => {\\n          // We're only bothered about height and width on the container\\n          if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n            return;\\n          }\\n\\n          // Revert back to auto\\n          container.style.width = '';\\n          container.style.height = '';\\n\\n          // Only listen once\\n          off.call(this, container, transitionEndEvent, restore);\\n        };\\n\\n        // Listen for the transition finishing and restore auto height/width\\n        on.call(this, container, transitionEndEvent, restore);\\n\\n        // Set dimensions to target\\n        container.style.width = `${size.width}px`;\\n        container.style.height = `${size.height}px`;\\n      }\\n\\n      // Set attributes on current tab\\n      toggleHidden(current, true);\\n\\n      // Set attributes on target\\n      toggleHidden(target, false);\\n\\n      // Focus the first item\\n      controls.focusFirstMenuItem.call(this, target, focusVisible);\\n    },\\n    // Set the download URL\\n    setDownloadUrl() {\\n      const button = this.elements.buttons.download;\\n\\n      // Bail if no button\\n      if (!is.element(button)) {\\n        return;\\n      }\\n\\n      // Set attribute\\n      button.setAttribute('href', this.download);\\n    },\\n    // Build the default HTML\\n    create(data) {\\n      const {\\n        bindMenuItemShortcuts,\\n        createButton,\\n        createProgress,\\n        createRange,\\n        createTime,\\n        setQualityMenu,\\n        setSpeedMenu,\\n        showMenuPanel\\n      } = controls;\\n      this.elements.controls = null;\\n\\n      // Larger overlaid play button\\n      if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n        this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n      }\\n\\n      // Create the container\\n      const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n      this.elements.controls = container;\\n\\n      // Default item attributes\\n      const defaultAttributes = {\\n        class: 'plyr__controls__item'\\n      };\\n\\n      // Loop through controls in order\\n      dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n        // Restart button\\n        if (control === 'restart') {\\n          container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n        }\\n\\n        // Rewind button\\n        if (control === 'rewind') {\\n          container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n        }\\n\\n        // Play/Pause button\\n        if (control === 'play') {\\n          container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n        }\\n\\n        // Fast forward button\\n        if (control === 'fast-forward') {\\n          container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n        }\\n\\n        // Progress\\n        if (control === 'progress') {\\n          const progressContainer = createElement('div', {\\n            class: `${defaultAttributes.class} plyr__progress__container`\\n          });\\n          const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n          // Seek range slider\\n          progress.appendChild(createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`\\n          }));\\n\\n          // Buffer progress\\n          progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n          // TODO: Add loop display indicator\\n\\n          // Seek tooltip\\n          if (this.config.tooltips.seek) {\\n            const tooltip = createElement('span', {\\n              class: this.config.classNames.tooltip\\n            }, '00:00');\\n            progress.appendChild(tooltip);\\n            this.elements.display.seekTooltip = tooltip;\\n          }\\n          this.elements.progress = progress;\\n          progressContainer.appendChild(this.elements.progress);\\n          container.appendChild(progressContainer);\\n        }\\n\\n        // Media current time display\\n        if (control === 'current-time') {\\n          container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n        }\\n\\n        // Media duration display\\n        if (control === 'duration') {\\n          container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n        }\\n\\n        // Volume controls\\n        if (control === 'mute' || control === 'volume') {\\n          let {\\n            volume\\n          } = this.elements;\\n\\n          // Create the volume container if needed\\n          if (!is.element(volume) || !container.contains(volume)) {\\n            volume = createElement('div', extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim()\\n            }));\\n            this.elements.volume = volume;\\n            container.appendChild(volume);\\n          }\\n\\n          // Toggle mute button\\n          if (control === 'mute') {\\n            volume.appendChild(createButton.call(this, 'mute'));\\n          }\\n\\n          // Volume range control\\n          // Ignored on iOS as it's handled globally\\n          // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n          if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n            // Set the attributes\\n            const attributes = {\\n              max: 1,\\n              step: 0.05,\\n              value: this.config.volume\\n            };\\n\\n            // Create the volume range slider\\n            volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n              id: `plyr-volume-${data.id}`\\n            })));\\n          }\\n        }\\n\\n        // Toggle captions button\\n        if (control === 'captions') {\\n          container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n        }\\n\\n        // Settings button / menu\\n        if (control === 'settings' && !is.empty(this.config.settings)) {\\n          const wrapper = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: ''\\n          }));\\n          wrapper.appendChild(createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false\\n          }));\\n          const popup = createElement('div', {\\n            class: 'plyr__menu__container',\\n            id: `plyr-settings-${data.id}`,\\n            hidden: ''\\n          });\\n          const inner = createElement('div');\\n          const home = createElement('div', {\\n            id: `plyr-settings-${data.id}-home`\\n          });\\n\\n          // Create the menu\\n          const menu = createElement('div', {\\n            role: 'menu'\\n          });\\n          home.appendChild(menu);\\n          inner.appendChild(home);\\n          this.elements.settings.panels.home = home;\\n\\n          // Build the menu items\\n          this.config.settings.forEach(type => {\\n            // TODO: bundle this with the createMenuItem helper and bindings\\n            const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: ''\\n            }));\\n\\n            // Bind menu shortcuts for keyboard users\\n            bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n            // Show menu on click\\n            on.call(this, menuItem, 'click', () => {\\n              showMenuPanel.call(this, type, false);\\n            });\\n            const flex = createElement('span', null, i18n.get(type, this.config));\\n            const value = createElement('span', {\\n              class: this.config.classNames.menu.value\\n            });\\n\\n            // Speed contains HTML entities\\n            value.innerHTML = data[type];\\n            flex.appendChild(value);\\n            menuItem.appendChild(flex);\\n            menu.appendChild(menuItem);\\n\\n            // Build the panes\\n            const pane = createElement('div', {\\n              id: `plyr-settings-${data.id}-${type}`,\\n              hidden: ''\\n            });\\n\\n            // Back button\\n            const backButton = createElement('button', {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n            });\\n\\n            // Visible label\\n            backButton.appendChild(createElement('span', {\\n              'aria-hidden': true\\n            }, i18n.get(type, this.config)));\\n\\n            // Screen reader label\\n            backButton.appendChild(createElement('span', {\\n              class: this.config.classNames.hidden\\n            }, i18n.get('menuBack', this.config)));\\n\\n            // Go back via keyboard\\n            on.call(this, pane, 'keydown', event => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            }, false);\\n\\n            // Go back via button click\\n            on.call(this, backButton, 'click', () => {\\n              showMenuPanel.call(this, 'home', false);\\n            });\\n\\n            // Add to pane\\n            pane.appendChild(backButton);\\n\\n            // Menu\\n            pane.appendChild(createElement('div', {\\n              role: 'menu'\\n            }));\\n            inner.appendChild(pane);\\n            this.elements.settings.buttons[type] = menuItem;\\n            this.elements.settings.panels[type] = pane;\\n          });\\n          popup.appendChild(inner);\\n          wrapper.appendChild(popup);\\n          container.appendChild(wrapper);\\n          this.elements.settings.popup = popup;\\n          this.elements.settings.menu = wrapper;\\n        }\\n\\n        // Picture in picture button\\n        if (control === 'pip' && support.pip) {\\n          container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n        }\\n\\n        // Airplay button\\n        if (control === 'airplay' && support.airplay) {\\n          container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n        }\\n\\n        // Download button\\n        if (control === 'download') {\\n          const attributes = extend({}, defaultAttributes, {\\n            element: 'a',\\n            href: this.download,\\n            target: '_blank'\\n          });\\n\\n          // Set download attribute for HTML5 only\\n          if (this.isHTML5) {\\n            attributes.download = '';\\n          }\\n          const {\\n            download\\n          } = this.config.urls;\\n          if (!is.url(download) && this.isEmbed) {\\n            extend(attributes, {\\n              icon: `logo-${this.provider}`,\\n              label: this.provider\\n            });\\n          }\\n          container.appendChild(createButton.call(this, 'download', attributes));\\n        }\\n\\n        // Toggle fullscreen button\\n        if (control === 'fullscreen') {\\n          container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n        }\\n      });\\n\\n      // Set available quality levels\\n      if (this.isHTML5) {\\n        setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n      }\\n      setSpeedMenu.call(this);\\n      return container;\\n    },\\n    // Insert controls\\n    inject() {\\n      // Sprite\\n      if (this.config.loadSprite) {\\n        const icon = controls.getIconUrl.call(this);\\n\\n        // Only load external sprite using AJAX\\n        if (icon.cors) {\\n          loadSprite(icon.url, 'sprite-plyr');\\n        }\\n      }\\n\\n      // Create a unique ID\\n      this.id = Math.floor(Math.random() * 10000);\\n\\n      // Null by default\\n      let container = null;\\n      this.elements.controls = null;\\n\\n      // Set template properties\\n      const props = {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        title: this.config.title\\n      };\\n      let update = true;\\n\\n      // If function, run it and use output\\n      if (is.function(this.config.controls)) {\\n        this.config.controls = this.config.controls.call(this, props);\\n      }\\n\\n      // Convert falsy controls to empty array (primarily for empty strings)\\n      if (!this.config.controls) {\\n        this.config.controls = [];\\n      }\\n      if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n        // HTMLElement or Non-empty string passed as the option\\n        container = this.config.controls;\\n      } else {\\n        // Create controls\\n        container = controls.create.call(this, {\\n          id: this.id,\\n          seektime: this.config.seekTime,\\n          speed: this.speed,\\n          quality: this.quality,\\n          captions: captions.getLabel.call(this)\\n          // TODO: Looping\\n          // loop: 'None',\\n        });\\n\\n        update = false;\\n      }\\n\\n      // Replace props with their value\\n      const replace = input => {\\n        let result = input;\\n        Object.entries(props).forEach(([key, value]) => {\\n          result = replaceAll(result, `{${key}}`, value);\\n        });\\n        return result;\\n      };\\n\\n      // Update markup\\n      if (update) {\\n        if (is.string(this.config.controls)) {\\n          container = replace(container);\\n        }\\n      }\\n\\n      // Controls container\\n      let target;\\n\\n      // Inject to custom location\\n      if (is.string(this.config.selectors.controls.container)) {\\n        target = document.querySelector(this.config.selectors.controls.container);\\n      }\\n\\n      // Inject into the container by default\\n      if (!is.element(target)) {\\n        target = this.elements.container;\\n      }\\n\\n      // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n      const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n      target[insertMethod]('afterbegin', container);\\n\\n      // Find the elements if need be\\n      if (!is.element(this.elements.controls)) {\\n        controls.findElements.call(this);\\n      }\\n\\n      // Add pressed property to buttons\\n      if (!is.empty(this.elements.buttons)) {\\n        const addProperty = button => {\\n          const className = this.config.classNames.controlPressed;\\n          button.setAttribute('aria-pressed', 'false');\\n          Object.defineProperty(button, 'pressed', {\\n            configurable: true,\\n            enumerable: true,\\n            get() {\\n              return hasClass(button, className);\\n            },\\n            set(pressed = false) {\\n              toggleClass(button, className, pressed);\\n              button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n            }\\n          });\\n        };\\n\\n        // Toggle classname when pressed property is set\\n        Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n      }\\n\\n      // Edge sometimes doesn't finish the paint so force a repaint\\n      if (browser.isEdge) {\\n        repaint(target);\\n      }\\n\\n      // Setup tooltips\\n      if (this.config.tooltips.controls) {\\n        const {\\n          classNames,\\n          selectors\\n        } = this.config;\\n        const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n        const labels = getElements.call(this, selector);\\n        Array.from(labels).forEach(label => {\\n          toggleClass(label, this.config.classNames.hidden, false);\\n          toggleClass(label, this.config.classNames.tooltip, true);\\n        });\\n      }\\n    },\\n    // Set media metadata\\n    setMediaMetadata() {\\n      try {\\n        if ('mediaSession' in navigator) {\\n          navigator.mediaSession.metadata = new window.MediaMetadata({\\n            title: this.config.mediaMetadata.title,\\n            artist: this.config.mediaMetadata.artist,\\n            album: this.config.mediaMetadata.album,\\n            artwork: this.config.mediaMetadata.artwork\\n          });\\n        }\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    },\\n    // Add markers\\n    setMarkers() {\\n      var _this$config$markers2, _this$config$markers3;\\n      if (!this.duration || this.elements.markers) return;\\n\\n      // Get valid points\\n      const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n        time\\n      }) => time > 0 && time < this.duration);\\n      if (!(points !== null && points !== void 0 && points.length)) return;\\n      const containerFragment = document.createDocumentFragment();\\n      const pointsFragment = document.createDocumentFragment();\\n      let tipElement = null;\\n      const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n      const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n      // Inject markers to progress container\\n      points.forEach(point => {\\n        const markerElement = createElement('span', {\\n          class: this.config.classNames.marker\\n        }, '');\\n        const left = `${point.time / this.duration * 100}%`;\\n        if (tipElement) {\\n          // Show on hover\\n          markerElement.addEventListener('mouseenter', () => {\\n            if (point.label) return;\\n            tipElement.style.left = left;\\n            tipElement.innerHTML = point.label;\\n            toggleTip(true);\\n          });\\n\\n          // Hide on leave\\n          markerElement.addEventListener('mouseleave', () => {\\n            toggleTip(false);\\n          });\\n        }\\n        markerElement.addEventListener('click', () => {\\n          this.currentTime = point.time;\\n        });\\n        markerElement.style.left = left;\\n        pointsFragment.appendChild(markerElement);\\n      });\\n      containerFragment.appendChild(pointsFragment);\\n\\n      // Inject a tooltip if needed\\n      if (!this.config.tooltips.seek) {\\n        tipElement = createElement('span', {\\n          class: this.config.classNames.tooltip\\n        }, '');\\n        containerFragment.appendChild(tipElement);\\n      }\\n      this.elements.markers = {\\n        points: pointsFragment,\\n        tip: tipElement\\n      };\\n      this.elements.progress.appendChild(containerFragment);\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  /**\\n   * Parse a string to a URL object\\n   * @param {String} input - the URL to be parsed\\n   * @param {Boolean} safe - failsafe parsing\\n   */\\n  function parseUrl(input, safe = true) {\\n    let url = input;\\n    if (safe) {\\n      const parser = document.createElement('a');\\n      parser.href = url;\\n      url = parser.href;\\n    }\\n    try {\\n      return new URL(url);\\n    } catch (_) {\\n      return null;\\n    }\\n  }\\n\\n  // Convert object to URLSearchParams\\n  function buildUrlParams(input) {\\n    const params = new URLSearchParams();\\n    if (is.object(input)) {\\n      Object.entries(input).forEach(([key, value]) => {\\n        params.set(key, value);\\n      });\\n    }\\n    return params;\\n  }\\n\\n  // ==========================================================================\\n  const captions = {\\n    // Setup captions\\n    setup() {\\n      // Requires UI support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n\\n      // Only Vimeo and HTML5 video supported at this point\\n      if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n        // Clear menu and hide\\n        if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n          controls.setCaptionsMenu.call(this);\\n        }\\n        return;\\n      }\\n\\n      // Inject the container\\n      if (!is.element(this.elements.captions)) {\\n        this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n        this.elements.captions.setAttribute('dir', 'auto');\\n        insertAfter(this.elements.captions, this.elements.wrapper);\\n      }\\n\\n      // Fix IE captions if CORS is used\\n      // Fetch captions and inject as blobs instead (data URIs not supported!)\\n      if (browser.isIE && window.URL) {\\n        const elements = this.media.querySelectorAll('track');\\n        Array.from(elements).forEach(track => {\\n          const src = track.getAttribute('src');\\n          const url = parseUrl(src);\\n          if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n            fetch(src, 'blob').then(blob => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            }).catch(() => {\\n              removeElement(track);\\n            });\\n          }\\n        });\\n      }\\n\\n      // Get and set initial data\\n      // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n      // * languages: Array of user's browser languages.\\n      // * language:  The language preferred by user settings or config\\n      // * active:    The state preferred by user settings or config\\n      // * toggled:   The real captions state\\n\\n      const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n      const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n      let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n      // Use first browser language when language is 'auto'\\n      if (language === 'auto') {\\n        [language] = languages;\\n      }\\n      let active = this.storage.get('captions');\\n      if (!is.boolean(active)) {\\n        ({\\n          active\\n        } = this.config.captions);\\n      }\\n      Object.assign(this.captions, {\\n        toggled: false,\\n        active,\\n        language,\\n        languages\\n      });\\n\\n      // Watch changes to textTracks and update captions menu\\n      if (this.isHTML5) {\\n        const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n        on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n      }\\n\\n      // Update available languages in list next tick (the event must not be triggered before the listeners)\\n      setTimeout(captions.update.bind(this), 0);\\n    },\\n    // Update available language options in settings based on tracks\\n    update() {\\n      const tracks = captions.getTracks.call(this, true);\\n      // Get the wanted language\\n      const {\\n        active,\\n        language,\\n        meta,\\n        currentTrackNode\\n      } = this.captions;\\n      const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n      // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n      if (this.isHTML5 && this.isVideo) {\\n        tracks.filter(track => !meta.get(track)).forEach(track => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing'\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n      }\\n\\n      // Update language first time it matches, or if the previous matching track was removed\\n      if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n        captions.setLanguage.call(this, language);\\n        captions.toggle.call(this, active && languageExists);\\n      }\\n\\n      // Enable or disable captions based on track length\\n      if (this.elements) {\\n        toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n      }\\n\\n      // Update available languages in list\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n    },\\n    // Toggle captions display\\n    // Used internally for the toggleCaptions method, with the passive option forced to false\\n    toggle(input, passive = true) {\\n      // If there's no full support\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      const {\\n        toggled\\n      } = this.captions; // Current state\\n      const activeClass = this.config.classNames.captions.active;\\n      // Get the next state\\n      // If the method is called without parameter, toggle based on current value\\n      const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n      // Update state and trigger event\\n      if (active !== toggled) {\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.active = active;\\n          this.storage.set({\\n            captions: active\\n          });\\n        }\\n\\n        // Force language if the call isn't passive and there is no matching language to toggle to\\n        if (!this.language && active && !passive) {\\n          const tracks = captions.getTracks.call(this);\\n          const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n          // Override user preferences to avoid switching languages if a matching track is added\\n          this.captions.language = track.language;\\n\\n          // Set caption, but don't store in localStorage as user preference\\n          captions.set.call(this, tracks.indexOf(track));\\n          return;\\n        }\\n\\n        // Toggle button if it's enabled\\n        if (this.elements.buttons.captions) {\\n          this.elements.buttons.captions.pressed = active;\\n        }\\n\\n        // Add class hook\\n        toggleClass(this.elements.container, activeClass, active);\\n        this.captions.toggled = active;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // Trigger event (not used internally)\\n        triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n      }\\n\\n      // Wait for the call stack to clear before setting mode='hidden'\\n      // on the active track - forcing the browser to download it\\n      setTimeout(() => {\\n        if (active && this.captions.toggled) {\\n          this.captions.currentTrackNode.mode = 'hidden';\\n        }\\n      });\\n    },\\n    // Set captions by track index\\n    // Used internally for the currentTrack setter with the passive option forced to false\\n    set(index, passive = true) {\\n      const tracks = captions.getTracks.call(this);\\n\\n      // Disable captions if setting to -1\\n      if (index === -1) {\\n        captions.toggle.call(this, false, passive);\\n        return;\\n      }\\n      if (!is.number(index)) {\\n        this.debug.warn('Invalid caption argument', index);\\n        return;\\n      }\\n      if (!(index in tracks)) {\\n        this.debug.warn('Track not found', index);\\n        return;\\n      }\\n      if (this.captions.currentTrack !== index) {\\n        this.captions.currentTrack = index;\\n        const track = tracks[index];\\n        const {\\n          language\\n        } = track || {};\\n\\n        // Store reference to node for invalidation on remove\\n        this.captions.currentTrackNode = track;\\n\\n        // Update settings menu\\n        controls.updateSetting.call(this, 'captions');\\n\\n        // When passive, don't override user preferences\\n        if (!passive) {\\n          this.captions.language = language;\\n          this.storage.set({\\n            language\\n          });\\n        }\\n\\n        // Handle Vimeo captions\\n        if (this.isVimeo) {\\n          this.embed.enableTextTrack(language);\\n        }\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'languagechange');\\n      }\\n\\n      // Show captions\\n      captions.toggle.call(this, true, passive);\\n      if (this.isHTML5 && this.isVideo) {\\n        // If we change the active track while a cue is already displayed we need to update it\\n        captions.updateCues.call(this);\\n      }\\n    },\\n    // Set captions by language\\n    // Used internally for the language setter with the passive option forced to false\\n    setLanguage(input, passive = true) {\\n      if (!is.string(input)) {\\n        this.debug.warn('Invalid language argument', input);\\n        return;\\n      }\\n      // Normalize\\n      const language = input.toLowerCase();\\n      this.captions.language = language;\\n\\n      // Set currentTrack\\n      const tracks = captions.getTracks.call(this);\\n      const track = captions.findTrack.call(this, [language]);\\n      captions.set.call(this, tracks.indexOf(track), passive);\\n    },\\n    // Get current valid caption tracks\\n    // If update is false it will also ignore tracks without metadata\\n    // This is used to \\\"freeze\\\" the language options when captions.update is false\\n    getTracks(update = false) {\\n      // Handle media or textTracks missing or null\\n      const tracks = Array.from((this.media || {}).textTracks || []);\\n      // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n      // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n      return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n    },\\n    // Match tracks based on languages and get the first\\n    findTrack(languages, force = false) {\\n      const tracks = captions.getTracks.call(this);\\n      const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n      const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n      let track;\\n      languages.every(language => {\\n        track = sorted.find(t => t.language === language);\\n        return !track; // Break iteration if there is a match\\n      });\\n\\n      // If no match is found but is required, get first\\n      return track || (force ? sorted[0] : undefined);\\n    },\\n    // Get the current track\\n    getCurrentTrack() {\\n      return captions.getTracks.call(this)[this.currentTrack];\\n    },\\n    // Get UI label for track\\n    getLabel(track) {\\n      let currentTrack = track;\\n      if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n        currentTrack = captions.getCurrentTrack.call(this);\\n      }\\n      if (is.track(currentTrack)) {\\n        if (!is.empty(currentTrack.label)) {\\n          return currentTrack.label;\\n        }\\n        if (!is.empty(currentTrack.language)) {\\n          return track.language.toUpperCase();\\n        }\\n        return i18n.get('enabled', this.config);\\n      }\\n      return i18n.get('disabled', this.config);\\n    },\\n    // Update captions using current track's active cues\\n    // Also optional array argument in case there isn't any track (ex: vimeo)\\n    updateCues(input) {\\n      // Requires UI\\n      if (!this.supported.ui) {\\n        return;\\n      }\\n      if (!is.element(this.elements.captions)) {\\n        this.debug.warn('No captions element to render to');\\n        return;\\n      }\\n\\n      // Only accept array or empty input\\n      if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n        this.debug.warn('updateCues: Invalid input', input);\\n        return;\\n      }\\n      let cues = input;\\n\\n      // Get cues from track\\n      if (!cues) {\\n        const track = captions.getCurrentTrack.call(this);\\n        cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n      }\\n\\n      // Set new caption text\\n      const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n      const changed = content !== this.elements.captions.innerHTML;\\n      if (changed) {\\n        // Empty the container and create a new child element\\n        emptyElement(this.elements.captions);\\n        const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n        caption.innerHTML = content;\\n        this.elements.captions.appendChild(caption);\\n\\n        // Trigger event\\n        triggerEvent.call(this, this.media, 'cuechange');\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr default config\\n  // ==========================================================================\\n\\n  const defaults = {\\n    // Disable\\n    enabled: true,\\n    // Custom media title\\n    title: '',\\n    // Logging to console\\n    debug: false,\\n    // Auto play (if supported)\\n    autoplay: false,\\n    // Only allow one media playing at once (vimeo only)\\n    autopause: true,\\n    // Allow inline playback on iOS\\n    playsinline: true,\\n    // Default time to skip when rewind/fast forward\\n    seekTime: 10,\\n    // Default volume\\n    volume: 1,\\n    muted: false,\\n    // Pass a custom duration\\n    duration: null,\\n    // Display the media duration on load in the current time position\\n    // If you have opted to display both duration and currentTime, this is ignored\\n    displayDuration: true,\\n    // Invert the current time to be a countdown\\n    invertTime: true,\\n    // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n    toggleInvert: true,\\n    // Force an aspect ratio\\n    // The format must be `'w:h'` (e.g. `'16:9'`)\\n    ratio: null,\\n    // Click video container to play/pause\\n    clickToPlay: true,\\n    // Auto hide the controls\\n    hideControls: true,\\n    // Reset to start when playback ended\\n    resetOnEnd: false,\\n    // Disable the standard context menu\\n    disableContextMenu: true,\\n    // Sprite (for icons)\\n    loadSprite: true,\\n    iconPrefix: 'plyr',\\n    iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n    // Blank video (used to prevent errors on source change)\\n    blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n    // Quality default\\n    quality: {\\n      default: 576,\\n      // The options to display in the UI, if available for the source media\\n      options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n      forced: false,\\n      onChange: null\\n    },\\n    // Set loops\\n    loop: {\\n      active: false\\n      // start: null,\\n      // end: null,\\n    },\\n\\n    // Speed default and options to display\\n    speed: {\\n      selected: 1,\\n      // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n      options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n    },\\n    // Keyboard shortcut settings\\n    keyboard: {\\n      focused: true,\\n      global: false\\n    },\\n    // Display tooltips\\n    tooltips: {\\n      controls: false,\\n      seek: true\\n    },\\n    // Captions settings\\n    captions: {\\n      active: false,\\n      language: 'auto',\\n      // Listen to new tracks added after Plyr is initialized.\\n      // This is needed for streaming captions, but may result in unselectable options\\n      update: false\\n    },\\n    // Fullscreen settings\\n    fullscreen: {\\n      enabled: true,\\n      // Allow fullscreen?\\n      fallback: true,\\n      // Fallback using full viewport/window\\n      iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n      // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n      // Non-ancestors of the player element will be ignored\\n      // container: null, // defaults to the player element\\n    },\\n\\n    // Local storage\\n    storage: {\\n      enabled: true,\\n      key: 'plyr'\\n    },\\n    // Default controls\\n    controls: ['play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress', 'current-time',\\n    // 'duration',\\n    'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n    // 'download',\\n    'fullscreen'],\\n    settings: ['captions', 'quality', 'speed'],\\n    // Localisation\\n    i18n: {\\n      restart: 'Restart',\\n      rewind: 'Rewind {seektime}s',\\n      play: 'Play',\\n      pause: 'Pause',\\n      fastForward: 'Forward {seektime}s',\\n      seek: 'Seek',\\n      seekLabel: '{currentTime} of {duration}',\\n      played: 'Played',\\n      buffered: 'Buffered',\\n      currentTime: 'Current time',\\n      duration: 'Duration',\\n      volume: 'Volume',\\n      mute: 'Mute',\\n      unmute: 'Unmute',\\n      enableCaptions: 'Enable captions',\\n      disableCaptions: 'Disable captions',\\n      download: 'Download',\\n      enterFullscreen: 'Enter fullscreen',\\n      exitFullscreen: 'Exit fullscreen',\\n      frameTitle: 'Player for {title}',\\n      captions: 'Captions',\\n      settings: 'Settings',\\n      pip: 'PIP',\\n      menuBack: 'Go back to previous menu',\\n      speed: 'Speed',\\n      normal: 'Normal',\\n      quality: 'Quality',\\n      loop: 'Loop',\\n      start: 'Start',\\n      end: 'End',\\n      all: 'All',\\n      reset: 'Reset',\\n      disabled: 'Disabled',\\n      enabled: 'Enabled',\\n      advertisement: 'Ad',\\n      qualityBadge: {\\n        2160: '4K',\\n        1440: 'HD',\\n        1080: 'HD',\\n        720: 'HD',\\n        576: 'SD',\\n        480: 'SD'\\n      }\\n    },\\n    // URLs\\n    urls: null,\\n    // Custom control listeners\\n    listeners: {\\n      seek: null,\\n      play: null,\\n      pause: null,\\n      restart: null,\\n      rewind: null,\\n      fastForward: null,\\n      mute: null,\\n      volume: null,\\n      captions: null,\\n      download: null,\\n      fullscreen: null,\\n      pip: null,\\n      airplay: null,\\n      speed: null,\\n      quality: null,\\n      loop: null,\\n      language: null\\n    },\\n    // Events to watch and bubble\\n    events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n    // Custom events\\n    'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n    // YouTube\\n    'statechange',\\n    // Quality\\n    'qualitychange',\\n    // Ads\\n    'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n    // Selectors\\n    // Change these to match your template if using custom HTML\\n    selectors: {\\n      editable: 'input, textarea, select, [contenteditable]',\\n      container: '.plyr',\\n      controls: {\\n        container: null,\\n        wrapper: '.plyr__controls'\\n      },\\n      labels: '[data-plyr]',\\n      buttons: {\\n        play: '[data-plyr=\\\"play\\\"]',\\n        pause: '[data-plyr=\\\"pause\\\"]',\\n        restart: '[data-plyr=\\\"restart\\\"]',\\n        rewind: '[data-plyr=\\\"rewind\\\"]',\\n        fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n        mute: '[data-plyr=\\\"mute\\\"]',\\n        captions: '[data-plyr=\\\"captions\\\"]',\\n        download: '[data-plyr=\\\"download\\\"]',\\n        fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n        pip: '[data-plyr=\\\"pip\\\"]',\\n        airplay: '[data-plyr=\\\"airplay\\\"]',\\n        settings: '[data-plyr=\\\"settings\\\"]',\\n        loop: '[data-plyr=\\\"loop\\\"]'\\n      },\\n      inputs: {\\n        seek: '[data-plyr=\\\"seek\\\"]',\\n        volume: '[data-plyr=\\\"volume\\\"]',\\n        speed: '[data-plyr=\\\"speed\\\"]',\\n        language: '[data-plyr=\\\"language\\\"]',\\n        quality: '[data-plyr=\\\"quality\\\"]'\\n      },\\n      display: {\\n        currentTime: '.plyr__time--current',\\n        duration: '.plyr__time--duration',\\n        buffer: '.plyr__progress__buffer',\\n        loop: '.plyr__progress__loop',\\n        // Used later\\n        volume: '.plyr__volume--display'\\n      },\\n      progress: '.plyr__progress',\\n      captions: '.plyr__captions',\\n      caption: '.plyr__caption'\\n    },\\n    // Class hooks added to the player in different states\\n    classNames: {\\n      type: 'plyr--{0}',\\n      provider: 'plyr--{0}',\\n      video: 'plyr__video-wrapper',\\n      embed: 'plyr__video-embed',\\n      videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n      embedContainer: 'plyr__video-embed__container',\\n      poster: 'plyr__poster',\\n      posterEnabled: 'plyr__poster-enabled',\\n      ads: 'plyr__ads',\\n      control: 'plyr__control',\\n      controlPressed: 'plyr__control--pressed',\\n      playing: 'plyr--playing',\\n      paused: 'plyr--paused',\\n      stopped: 'plyr--stopped',\\n      loading: 'plyr--loading',\\n      hover: 'plyr--hover',\\n      tooltip: 'plyr__tooltip',\\n      cues: 'plyr__cues',\\n      marker: 'plyr__progress__marker',\\n      hidden: 'plyr__sr-only',\\n      hideControls: 'plyr--hide-controls',\\n      isTouch: 'plyr--is-touch',\\n      uiSupported: 'plyr--full-ui',\\n      noTransition: 'plyr--no-transition',\\n      display: {\\n        time: 'plyr__time'\\n      },\\n      menu: {\\n        value: 'plyr__menu__value',\\n        badge: 'plyr__badge',\\n        open: 'plyr--menu-open'\\n      },\\n      captions: {\\n        enabled: 'plyr--captions-enabled',\\n        active: 'plyr--captions-active'\\n      },\\n      fullscreen: {\\n        enabled: 'plyr--fullscreen-enabled',\\n        fallback: 'plyr--fullscreen-fallback'\\n      },\\n      pip: {\\n        supported: 'plyr--pip-supported',\\n        active: 'plyr--pip-active'\\n      },\\n      airplay: {\\n        supported: 'plyr--airplay-supported',\\n        active: 'plyr--airplay-active'\\n      },\\n      previewThumbnails: {\\n        // Tooltip thumbs\\n        thumbContainer: 'plyr__preview-thumb',\\n        thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n        imageContainer: 'plyr__preview-thumb__image-container',\\n        timeContainer: 'plyr__preview-thumb__time-container',\\n        // Scrubbing\\n        scrubbingContainer: 'plyr__preview-scrubbing',\\n        scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n      }\\n    },\\n    // Embed attributes\\n    attributes: {\\n      embed: {\\n        provider: 'data-plyr-provider',\\n        id: 'data-plyr-embed-id',\\n        hash: 'data-plyr-embed-hash'\\n      }\\n    },\\n    // Advertisements plugin\\n    // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n    ads: {\\n      enabled: false,\\n      publisherId: '',\\n      tagUrl: ''\\n    },\\n    // Preview Thumbnails plugin\\n    previewThumbnails: {\\n      enabled: false,\\n      src: ''\\n    },\\n    // Vimeo plugin\\n    vimeo: {\\n      byline: false,\\n      portrait: false,\\n      title: false,\\n      speed: true,\\n      transparent: false,\\n      // Custom settings from Plyr\\n      customControls: true,\\n      referrerPolicy: null,\\n      // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n      // Whether the owner of the video has a Pro or Business account\\n      // (which allows us to properly hide controls without CSS hacks, etc)\\n      premium: false\\n    },\\n    // YouTube plugin\\n    youtube: {\\n      rel: 0,\\n      // No related vids\\n      showinfo: 0,\\n      // Hide info\\n      iv_load_policy: 3,\\n      // Hide annotations\\n      modestbranding: 1,\\n      // Hide logos as much as possible (they still show one in the corner when paused)\\n      // Custom settings from Plyr\\n      customControls: true,\\n      noCookie: false // Whether to use an alternative version of YouTube without cookies\\n    },\\n\\n    // Media Metadata\\n    mediaMetadata: {\\n      title: '',\\n      artist: '',\\n      album: '',\\n      artwork: []\\n    },\\n    // Markers\\n    markers: {\\n      enabled: false,\\n      points: []\\n    }\\n  };\\n\\n  // ==========================================================================\\n  // Plyr states\\n  // ==========================================================================\\n\\n  const pip = {\\n    active: 'picture-in-picture',\\n    inactive: 'inline'\\n  };\\n\\n  // ==========================================================================\\n  // Plyr supported types and providers\\n  // ==========================================================================\\n\\n  const providers = {\\n    html5: 'html5',\\n    youtube: 'youtube',\\n    vimeo: 'vimeo'\\n  };\\n  const types = {\\n    audio: 'audio',\\n    video: 'video'\\n  };\\n\\n  /**\\n   * Get provider by URL\\n   * @param {String} url\\n   */\\n  function getProviderByUrl(url) {\\n    // YouTube\\n    if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n      return providers.youtube;\\n    }\\n\\n    // Vimeo\\n    if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n      return providers.vimeo;\\n    }\\n    return null;\\n  }\\n\\n  // ==========================================================================\\n  // Console wrapper\\n  // ==========================================================================\\n\\n  const noop = () => {};\\n  class Console {\\n    constructor(enabled = false) {\\n      this.enabled = window.console && enabled;\\n      if (this.enabled) {\\n        this.log('Debugging enabled');\\n      }\\n    }\\n    get log() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n    }\\n    get warn() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n    }\\n    get error() {\\n      // eslint-disable-next-line no-console\\n      return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n    }\\n  }\\n\\n  class Fullscreen {\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"onChange\\\", () => {\\n        if (!this.supported) return;\\n\\n        // Update toggle button\\n        const button = this.player.elements.buttons.fullscreen;\\n        if (is.element(button)) {\\n          button.pressed = this.active;\\n        }\\n\\n        // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n        const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n        // Trigger an event\\n        triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n      });\\n      _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n        // Store or restore scroll position\\n        if (toggle) {\\n          this.scrollPosition = {\\n            x: window.scrollX ?? 0,\\n            y: window.scrollY ?? 0\\n          };\\n        } else {\\n          window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n        }\\n\\n        // Toggle scroll\\n        document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n        // Toggle class hook\\n        toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n        // Force full viewport on iPhone X+\\n        if (browser.isIos) {\\n          let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n          const property = 'viewport-fit=cover';\\n\\n          // Inject the viewport meta if required\\n          if (!viewport) {\\n            viewport = document.createElement('meta');\\n            viewport.setAttribute('name', 'viewport');\\n          }\\n\\n          // Check if the property already exists\\n          const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n          if (toggle) {\\n            this.cleanupViewport = !hasProperty;\\n            if (!hasProperty) viewport.content += `,${property}`;\\n          } else if (this.cleanupViewport) {\\n            viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n          }\\n        }\\n\\n        // Toggle button and fire events\\n        this.onChange();\\n      });\\n      // Trap focus inside container\\n      _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n        // Bail if iOS/iPadOS, not active, not the tab key\\n        if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n        // Get the current focused element\\n        const focused = document.activeElement;\\n        const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n        const [first] = focusable;\\n        const last = focusable[focusable.length - 1];\\n        if (focused === last && !event.shiftKey) {\\n          // Move focus to first element that can be tabbed if Shift isn't used\\n          first.focus();\\n          event.preventDefault();\\n        } else if (focused === first && event.shiftKey) {\\n          // Move focus to last element that can be tabbed if Shift is used\\n          last.focus();\\n          event.preventDefault();\\n        }\\n      });\\n      // Update UI\\n      _defineProperty$1(this, \\\"update\\\", () => {\\n        if (this.supported) {\\n          let mode;\\n          if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n          this.player.debug.log(`${mode} fullscreen enabled`);\\n        } else {\\n          this.player.debug.log('Fullscreen not supported and fallback disabled');\\n        }\\n\\n        // Add styling hook to show button\\n        toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n      });\\n      // Make an element fullscreen\\n      _defineProperty$1(this, \\\"enter\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen doesn't need the request step\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.requestFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(true);\\n        } else if (!this.prefix) {\\n          this.target.requestFullscreen({\\n            navigationUI: 'hide'\\n          });\\n        } else if (!is.empty(this.prefix)) {\\n          this.target[`${this.prefix}Request${this.property}`]();\\n        }\\n      });\\n      // Bail from fullscreen\\n      _defineProperty$1(this, \\\"exit\\\", () => {\\n        if (!this.supported) return;\\n\\n        // iOS native fullscreen\\n        if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n          if (this.player.isVimeo) {\\n            this.player.embed.exitFullscreen();\\n          } else {\\n            this.target.webkitEnterFullscreen();\\n          }\\n          silencePromise(this.player.play());\\n        } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n          this.toggleFallback(false);\\n        } else if (!this.prefix) {\\n          (document.cancelFullScreen || document.exitFullscreen).call(document);\\n        } else if (!is.empty(this.prefix)) {\\n          const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n          document[`${this.prefix}${action}${this.property}`]();\\n        }\\n      });\\n      // Toggle state\\n      _defineProperty$1(this, \\\"toggle\\\", () => {\\n        if (!this.active) this.enter();else this.exit();\\n      });\\n      // Keep reference to parent\\n      this.player = player;\\n\\n      // Get prefix\\n      this.prefix = Fullscreen.prefix;\\n      this.property = Fullscreen.property;\\n\\n      // Scroll position\\n      this.scrollPosition = {\\n        x: 0,\\n        y: 0\\n      };\\n\\n      // Force the use of 'full window/browser' rather than fullscreen\\n      this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n      // Get the fullscreen element\\n      // Checks container is an ancestor, defaults to null\\n      this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n      // Register event listeners\\n      // Handle event (incase user presses escape etc)\\n      on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      });\\n\\n      // Fullscreen toggle on double click\\n      on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n        // Ignore double click in controls\\n        if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n          return;\\n        }\\n        this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n      });\\n\\n      // Tap focus when in fullscreen\\n      on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n      // Update the UI\\n      this.update();\\n    }\\n\\n    // Determine if native supported\\n    static get nativeSupported() {\\n      return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n    }\\n\\n    // If we're actually using native\\n    get useNative() {\\n      return Fullscreen.nativeSupported && !this.forceFallback;\\n    }\\n\\n    // Get the prefix for handlers\\n    static get prefix() {\\n      // No prefix\\n      if (is.function(document.exitFullscreen)) return '';\\n\\n      // Check for fullscreen support by vendor prefix\\n      let value = '';\\n      const prefixes = ['webkit', 'moz', 'ms'];\\n      prefixes.some(pre => {\\n        if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n          value = pre;\\n          return true;\\n        }\\n        return false;\\n      });\\n      return value;\\n    }\\n    static get property() {\\n      return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n    }\\n\\n    // Determine if fullscreen is supported\\n    get supported() {\\n      return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n    }\\n\\n    // Get active state\\n    get active() {\\n      if (!this.supported) return false;\\n\\n      // Fallback using classname\\n      if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n      }\\n      const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n      return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n    }\\n\\n    // Get target element\\n    get target() {\\n      return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n    }\\n  }\\n\\n  // ==========================================================================\\n  // Load image avoiding xhr/fetch CORS issues\\n  // Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n  // By default it checks if it is at least 1px, but you can add a second argument to change this\\n  // ==========================================================================\\n\\n  function loadImage(src, minWidth = 1) {\\n    return new Promise((resolve, reject) => {\\n      const image = new Image();\\n      const handler = () => {\\n        delete image.onload;\\n        delete image.onerror;\\n        (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n      };\\n      Object.assign(image, {\\n        onload: handler,\\n        onerror: handler,\\n        src\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n  const ui = {\\n    addStyleHook() {\\n      toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n      toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n    },\\n    // Toggle native HTML5 media controls\\n    toggleNativeControls(toggle = false) {\\n      if (toggle && this.isHTML5) {\\n        this.media.setAttribute('controls', '');\\n      } else {\\n        this.media.removeAttribute('controls');\\n      }\\n    },\\n    // Setup the UI\\n    build() {\\n      // Re-attach media element listeners\\n      // TODO: Use event bubbling?\\n      this.listeners.media();\\n\\n      // Don't setup interface if no support\\n      if (!this.supported.ui) {\\n        this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n        // Restore native controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Bail\\n        return;\\n      }\\n\\n      // Inject custom controls if not present\\n      if (!is.element(this.elements.controls)) {\\n        // Inject custom controls\\n        controls.inject.call(this);\\n\\n        // Re-attach control listeners\\n        this.listeners.controls();\\n      }\\n\\n      // Remove native controls\\n      ui.toggleNativeControls.call(this);\\n\\n      // Setup captions for HTML5\\n      if (this.isHTML5) {\\n        captions.setup.call(this);\\n      }\\n\\n      // Reset volume\\n      this.volume = null;\\n\\n      // Reset mute state\\n      this.muted = null;\\n\\n      // Reset loop state\\n      this.loop = null;\\n\\n      // Reset quality setting\\n      this.quality = null;\\n\\n      // Reset speed\\n      this.speed = null;\\n\\n      // Reset volume display\\n      controls.updateVolume.call(this);\\n\\n      // Reset time display\\n      controls.timeUpdate.call(this);\\n\\n      // Reset duration display\\n      controls.durationUpdate.call(this);\\n\\n      // Update the UI\\n      ui.checkPlaying.call(this);\\n\\n      // Check for picture-in-picture support\\n      toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n      // Check for airplay support\\n      toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n      // Add touch class\\n      toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n      // Ready for API calls\\n      this.ready = true;\\n\\n      // Ready event at end of execution stack\\n      setTimeout(() => {\\n        triggerEvent.call(this, this.media, 'ready');\\n      }, 0);\\n\\n      // Set the title\\n      ui.setTitle.call(this);\\n\\n      // Assure the poster image is set, if the property was added before the element was created\\n      if (this.poster) {\\n        ui.setPoster.call(this, this.poster, false).catch(() => {});\\n      }\\n\\n      // Manually set the duration if user has overridden it.\\n      // The event listeners for it doesn't get called if preload is disabled (#701)\\n      if (this.config.duration) {\\n        controls.durationUpdate.call(this);\\n      }\\n\\n      // Media metadata\\n      if (this.config.mediaMetadata) {\\n        controls.setMediaMetadata.call(this);\\n      }\\n    },\\n    // Setup aria attribute for play and iframe title\\n    setTitle() {\\n      // Find the current text\\n      let label = i18n.get('play', this.config);\\n\\n      // If there's a media title set, use that for the label\\n      if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n        label += `, ${this.config.title}`;\\n      }\\n\\n      // If there's a play button, set label\\n      Array.from(this.elements.buttons.play || []).forEach(button => {\\n        button.setAttribute('aria-label', label);\\n      });\\n\\n      // Set iframe title\\n      // https://github.com/sampotts/plyr/issues/124\\n      if (this.isEmbed) {\\n        const iframe = getElement.call(this, 'iframe');\\n        if (!is.element(iframe)) {\\n          return;\\n        }\\n\\n        // Default to media type\\n        const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n        const format = i18n.get('frameTitle', this.config);\\n        iframe.setAttribute('title', format.replace('{title}', title));\\n      }\\n    },\\n    // Toggle poster\\n    togglePoster(enable) {\\n      toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n    },\\n    // Set the poster image (async)\\n    // Used internally for the poster setter, with the passive option forced to false\\n    setPoster(poster, passive = true) {\\n      // Don't override if call is passive\\n      if (passive && this.poster) {\\n        return Promise.reject(new Error('Poster already set'));\\n      }\\n\\n      // Set property synchronously to respect the call order\\n      this.media.setAttribute('data-poster', poster);\\n\\n      // Show the poster\\n      this.elements.poster.removeAttribute('hidden');\\n\\n      // Wait until ui is ready\\n      return ready.call(this)\\n      // Load image\\n      .then(() => loadImage(poster)).catch(error => {\\n        // Hide poster on error unless it's been set by another call\\n        if (poster === this.poster) {\\n          ui.togglePoster.call(this, false);\\n        }\\n        // Rethrow\\n        throw error;\\n      }).then(() => {\\n        // Prevent race conditions\\n        if (poster !== this.poster) {\\n          throw new Error('setPoster cancelled by later call to setPoster');\\n        }\\n      }).then(() => {\\n        Object.assign(this.elements.poster.style, {\\n          backgroundImage: `url('${poster}')`,\\n          // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n          backgroundSize: ''\\n        });\\n        ui.togglePoster.call(this, true);\\n        return poster;\\n      });\\n    },\\n    // Check playing state\\n    checkPlaying(event) {\\n      // Class hooks\\n      toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n      toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n      toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n      // Set state\\n      Array.from(this.elements.buttons.play || []).forEach(target => {\\n        Object.assign(target, {\\n          pressed: this.playing\\n        });\\n        target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n      });\\n\\n      // Only update controls on non timeupdate events\\n      if (is.event(event) && event.type === 'timeupdate') {\\n        return;\\n      }\\n\\n      // Toggle controls\\n      ui.toggleControls.call(this);\\n    },\\n    // Check if media is loading\\n    checkLoading(event) {\\n      this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n      // Clear timer\\n      clearTimeout(this.timers.loading);\\n\\n      // Timer to prevent flicker when seeking\\n      this.timers.loading = setTimeout(() => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      }, this.loading ? 250 : 0);\\n    },\\n    // Toggle controls based on state and `force` argument\\n    toggleControls(force) {\\n      const {\\n        controls: controlsElement\\n      } = this.elements;\\n      if (controlsElement && this.config.hideControls) {\\n        // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n        const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n        // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n        this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n      }\\n    },\\n    // Migrate any custom properties from the media to the parent\\n    migrateStyles() {\\n      // Loop through values (as they are the keys when the object is spread 🤔)\\n      Object.values({\\n        ...this.media.style\\n      })\\n      // We're only fussed about Plyr specific properties\\n      .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n      // Remove attribute if empty\\n      if (is.empty(this.media.style)) {\\n        this.media.removeAttribute('style');\\n      }\\n    }\\n  };\\n\\n  class Listeners {\\n    constructor(_player) {\\n      // Device is touch enabled\\n      _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        player.touch = true;\\n\\n        // Add touch class\\n        toggleClass(elements.container, player.config.classNames.isTouch, true);\\n      });\\n      // Global window & document listeners\\n      _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n        const {\\n          player\\n        } = this;\\n\\n        // Keyboard shortcuts\\n        if (player.config.keyboard.global) {\\n          toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n        }\\n\\n        // Click anywhere closes menu\\n        toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n        // Detect touch by events\\n        once.call(player, document.body, 'touchstart', this.firstTouch);\\n      });\\n      // Container listeners\\n      _defineProperty$1(this, \\\"container\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          config,\\n          elements,\\n          timers\\n        } = player;\\n\\n        // Keyboard shortcuts\\n        if (!config.keyboard.global && config.keyboard.focused) {\\n          on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n        }\\n\\n        // Toggle controls on mouse events and entering fullscreen\\n        on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n          const {\\n            controls: controlsElement\\n          } = elements;\\n\\n          // Remove button states for fullscreen\\n          if (controlsElement && event.type === 'enterfullscreen') {\\n            controlsElement.pressed = false;\\n            controlsElement.hover = false;\\n          }\\n\\n          // Show, then hide after a timeout unless another control event occurs\\n          const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n          let delay = 0;\\n          if (show) {\\n            ui.toggleControls.call(player, true);\\n            // Use longer timeout for touch devices\\n            delay = player.touch ? 3000 : 2000;\\n          }\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Set new timer to prevent flicker when seeking\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Set a gutter for Vimeo\\n        const setGutter = () => {\\n          if (!player.isVimeo || player.config.vimeo.premium) {\\n            return;\\n          }\\n          const target = elements.wrapper;\\n          const {\\n            active\\n          } = player.fullscreen;\\n          const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n          const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n          // If not active, remove styles\\n          if (!active) {\\n            if (useNativeAspectRatio) {\\n              target.style.width = null;\\n              target.style.height = null;\\n            } else {\\n              target.style.maxWidth = null;\\n              target.style.margin = null;\\n            }\\n            return;\\n          }\\n\\n          // Determine which dimension will overflow and constrain view\\n          const [viewportWidth, viewportHeight] = getViewportSize();\\n          const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n          if (useNativeAspectRatio) {\\n            target.style.width = overflow ? 'auto' : '100%';\\n            target.style.height = overflow ? '100%' : 'auto';\\n          } else {\\n            target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n            target.style.margin = overflow ? '0 auto' : null;\\n          }\\n        };\\n\\n        // Handle resizing\\n        const resized = () => {\\n          clearTimeout(timers.resized);\\n          timers.resized = setTimeout(setGutter, 50);\\n        };\\n        on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n          const {\\n            target\\n          } = player.fullscreen;\\n\\n          // Ignore events not from target\\n          if (target !== elements.container) {\\n            return;\\n          }\\n\\n          // If it's not an embed and no ratio specified\\n          if (!player.isEmbed && is.empty(player.config.ratio)) {\\n            return;\\n          }\\n\\n          // Set Vimeo gutter\\n          setGutter();\\n\\n          // Watch for resizes\\n          const method = event.type === 'enterfullscreen' ? on : off;\\n          method.call(player, window, 'resize', resized);\\n        });\\n      });\\n      // Listen for media events\\n      _defineProperty$1(this, \\\"media\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n\\n        // Time change on media\\n        on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n        // Display duration\\n        on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n        // Handle the media finishing\\n        on.call(player, player.media, 'ended', () => {\\n          // Show poster on end\\n          if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n            // Restart\\n            player.restart();\\n\\n            // Call pause otherwise IE11 will start playing the video again\\n            player.pause();\\n          }\\n        });\\n\\n        // Check for buffer progress\\n        on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n        // Handle volume changes\\n        on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n        // Handle play/pause\\n        on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n        // Loading state\\n        on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n        // Click video\\n        if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n          // Re-fetch the wrapper\\n          const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n          // Bail if there's no wrapper (this should never happen)\\n          if (!is.element(wrapper)) {\\n            return;\\n          }\\n\\n          // On click play, pause or restart\\n          on.call(player, elements.container, 'click', event => {\\n            const targets = [elements.container, wrapper];\\n\\n            // Ignore if click if not container or in video wrapper\\n            if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n              return;\\n            }\\n\\n            // Touch devices will just show controls (if hidden)\\n            if (player.touch && player.config.hideControls) {\\n              return;\\n            }\\n            if (player.ended) {\\n              this.proxy(event, player.restart, 'restart');\\n              this.proxy(event, () => {\\n                silencePromise(player.play());\\n              }, 'play');\\n            } else {\\n              this.proxy(event, () => {\\n                silencePromise(player.togglePlay());\\n              }, 'play');\\n            }\\n          });\\n        }\\n\\n        // Disable right click\\n        if (player.supported.ui && player.config.disableContextMenu) {\\n          on.call(player, elements.wrapper, 'contextmenu', event => {\\n            event.preventDefault();\\n          }, false);\\n        }\\n\\n        // Volume change\\n        on.call(player, player.media, 'volumechange', () => {\\n          // Save to storage\\n          player.storage.set({\\n            volume: player.volume,\\n            muted: player.muted\\n          });\\n        });\\n\\n        // Speed change\\n        on.call(player, player.media, 'ratechange', () => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'speed');\\n\\n          // Save to storage\\n          player.storage.set({\\n            speed: player.speed\\n          });\\n        });\\n\\n        // Quality change\\n        on.call(player, player.media, 'qualitychange', event => {\\n          // Update UI\\n          controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n        });\\n\\n        // Update download link when ready and if quality changes\\n        on.call(player, player.media, 'ready qualitychange', () => {\\n          controls.setDownloadUrl.call(player);\\n        });\\n\\n        // Proxy events to container\\n        // Bubble up key events for Edge\\n        const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n        on.call(player, player.media, proxyEvents, event => {\\n          let {\\n            detail = {}\\n          } = event;\\n\\n          // Get error details from media\\n          if (event.type === 'error') {\\n            detail = player.media.error;\\n          }\\n          triggerEvent.call(player, elements.container, event.type, true, detail);\\n        });\\n      });\\n      // Run default and custom handlers\\n      _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        let returned = true;\\n\\n        // Execute custom handler\\n        if (hasCustomHandler) {\\n          returned = customHandler.call(player, event);\\n        }\\n\\n        // Only call default handler if not prevented in custom handler\\n        if (returned !== false && is.function(defaultHandler)) {\\n          defaultHandler.call(player, event);\\n        }\\n      });\\n      // Trigger custom and default handlers\\n      _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n        const {\\n          player\\n        } = this;\\n        const customHandler = player.config.listeners[customHandlerKey];\\n        const hasCustomHandler = is.function(customHandler);\\n        on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n      });\\n      // Listen for control events\\n      _defineProperty$1(this, \\\"controls\\\", () => {\\n        const {\\n          player\\n        } = this;\\n        const {\\n          elements\\n        } = player;\\n        // IE doesn't support input event, so we fallback to change\\n        const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n        // Play/pause toggle\\n        if (elements.buttons.play) {\\n          Array.from(elements.buttons.play).forEach(button => {\\n            this.bind(button, 'click', () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          });\\n        }\\n\\n        // Pause\\n        this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n        // Rewind\\n        this.bind(elements.buttons.rewind, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n          player.lastSeekTime = Date.now();\\n          player.rewind();\\n        }, 'rewind');\\n\\n        // Rewind\\n        this.bind(elements.buttons.fastForward, 'click', () => {\\n          // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n          player.lastSeekTime = Date.now();\\n          player.forward();\\n        }, 'fastForward');\\n\\n        // Mute toggle\\n        this.bind(elements.buttons.mute, 'click', () => {\\n          player.muted = !player.muted;\\n        }, 'mute');\\n\\n        // Captions toggle\\n        this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n        // Download\\n        this.bind(elements.buttons.download, 'click', () => {\\n          triggerEvent.call(player, player.media, 'download');\\n        }, 'download');\\n\\n        // Fullscreen toggle\\n        this.bind(elements.buttons.fullscreen, 'click', () => {\\n          player.fullscreen.toggle();\\n        }, 'fullscreen');\\n\\n        // Picture-in-Picture\\n        this.bind(elements.buttons.pip, 'click', () => {\\n          player.pip = 'toggle';\\n        }, 'pip');\\n\\n        // Airplay\\n        this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n        // Settings menu - click toggle\\n        this.bind(elements.buttons.settings, 'click', event => {\\n          // Prevent the document click listener closing the menu\\n          event.stopPropagation();\\n          event.preventDefault();\\n          controls.toggleMenu.call(player, event);\\n        }, null, false); // Can't be passive as we're preventing default\\n\\n        // Settings menu - keyboard toggle\\n        // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n        // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n        this.bind(elements.buttons.settings, 'keyup', event => {\\n          if (![' ', 'Enter'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Because return triggers a click anyway, all we need to do is set focus\\n          if (event.key === 'Enter') {\\n            controls.focusFirstMenuItem.call(player, null, true);\\n            return;\\n          }\\n\\n          // Prevent scroll\\n          event.preventDefault();\\n\\n          // Prevent playing video (Firefox)\\n          event.stopPropagation();\\n\\n          // Toggle menu\\n          controls.toggleMenu.call(player, event);\\n        }, null, false // Can't be passive as we're preventing default\\n        );\\n\\n        // Escape closes menu\\n        this.bind(elements.settings.menu, 'keydown', event => {\\n          if (event.key === 'Escape') {\\n            controls.toggleMenu.call(player, event);\\n          }\\n        });\\n\\n        // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n        this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n          const rect = elements.progress.getBoundingClientRect();\\n          const percent = 100 / rect.width * (event.pageX - rect.left);\\n          event.currentTarget.setAttribute('seek-value', percent);\\n        });\\n\\n        // Pause while seeking\\n        this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n          const seek = event.currentTarget;\\n          const attribute = 'play-on-seeked';\\n          if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n            return;\\n          }\\n\\n          // Record seek time so we can prevent hiding controls for a few seconds after seek\\n          player.lastSeekTime = Date.now();\\n\\n          // Was playing before?\\n          const play = seek.hasAttribute(attribute);\\n          // Done seeking\\n          const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n          // If we're done seeking and it was playing, resume playback\\n          if (play && done) {\\n            seek.removeAttribute(attribute);\\n            silencePromise(player.play());\\n          } else if (!done && player.playing) {\\n            seek.setAttribute(attribute, '');\\n            player.pause();\\n          }\\n        });\\n\\n        // Fix range inputs on iOS\\n        // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n        // it takes over further interactions on the page. This is a hack\\n        if (browser.isIos) {\\n          const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n          Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n        }\\n\\n        // Seek\\n        this.bind(elements.inputs.seek, inputEvent, event => {\\n          const seek = event.currentTarget;\\n          // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n          let seekTo = seek.getAttribute('seek-value');\\n          if (is.empty(seekTo)) {\\n            seekTo = seek.value;\\n          }\\n          seek.removeAttribute('seek-value');\\n          player.currentTime = seekTo / seek.max * player.duration;\\n        }, 'seek');\\n\\n        // Seek tooltip\\n        this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n        // Preview thumbnails plugin\\n        // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n        this.bind(elements.progress, 'mousemove touchmove', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startMove(event);\\n          }\\n        });\\n\\n        // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n        this.bind(elements.progress, 'mouseleave touchend click', () => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endMove(false, true);\\n          }\\n        });\\n\\n        // Show scrubbing preview\\n        this.bind(elements.progress, 'mousedown touchstart', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.startScrubbing(event);\\n          }\\n        });\\n        this.bind(elements.progress, 'mouseup touchend', event => {\\n          const {\\n            previewThumbnails\\n          } = player;\\n          if (previewThumbnails && previewThumbnails.loaded) {\\n            previewThumbnails.endScrubbing(event);\\n          }\\n        });\\n\\n        // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n        if (browser.isWebKit) {\\n          Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n            this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n          });\\n        }\\n\\n        // Current time invert\\n        // Only if one time element is used for both currentTime and duration\\n        if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n          this.bind(elements.display.currentTime, 'click', () => {\\n            // Do nothing if we're at the start\\n            if (player.currentTime === 0) {\\n              return;\\n            }\\n            player.config.invertTime = !player.config.invertTime;\\n            controls.timeUpdate.call(player);\\n          });\\n        }\\n\\n        // Volume\\n        this.bind(elements.inputs.volume, inputEvent, event => {\\n          player.volume = event.target.value;\\n        }, 'volume');\\n\\n        // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n          elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n        });\\n\\n        // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n        if (elements.fullscreen) {\\n          Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n            this.bind(child, 'mouseenter mouseleave', event => {\\n              if (elements.controls) {\\n                elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n              }\\n            });\\n          });\\n        }\\n\\n        // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n        this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n          elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n        });\\n\\n        // Show controls when they receive focus (e.g., when using keyboard tab key)\\n        this.bind(elements.controls, 'focusin', () => {\\n          const {\\n            config,\\n            timers\\n          } = player;\\n\\n          // Skip transition to prevent focus from scrolling the parent element\\n          toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n          // Toggle\\n          ui.toggleControls.call(player, true);\\n\\n          // Restore transition\\n          setTimeout(() => {\\n            toggleClass(elements.controls, config.classNames.noTransition, false);\\n          }, 0);\\n\\n          // Delay a little more for mouse users\\n          const delay = this.touch ? 3000 : 4000;\\n\\n          // Clear timer\\n          clearTimeout(timers.controls);\\n\\n          // Hide again after delay\\n          timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n        });\\n\\n        // Mouse wheel for volume\\n        this.bind(elements.inputs.volume, 'wheel', event => {\\n          // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n          // Other browsers on OS X will be inverted until support improves\\n          const inverted = event.webkitDirectionInvertedFromDevice;\\n          // Get delta from event. Invert if `inverted` is true\\n          const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n          // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n          const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n          // Change the volume by 2%\\n          player.increaseVolume(direction / 50);\\n\\n          // Don't break page scrolling at max and min\\n          const {\\n            volume\\n          } = player.media;\\n          if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n            event.preventDefault();\\n          }\\n        }, 'volume', false);\\n      });\\n      this.player = _player;\\n      this.lastKey = null;\\n      this.focusTimer = null;\\n      this.lastKeyDown = null;\\n      this.handleKey = this.handleKey.bind(this);\\n      this.toggleMenu = this.toggleMenu.bind(this);\\n      this.firstTouch = this.firstTouch.bind(this);\\n    }\\n\\n    // Handle key presses\\n    handleKey(event) {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      const {\\n        key,\\n        type,\\n        altKey,\\n        ctrlKey,\\n        metaKey,\\n        shiftKey\\n      } = event;\\n      const pressed = type === 'keydown';\\n      const repeat = pressed && key === this.lastKey;\\n\\n      // Bail if a modifier key is set\\n      if (altKey || ctrlKey || metaKey || shiftKey) {\\n        return;\\n      }\\n\\n      // If the event is bubbled from the media element\\n      // Firefox doesn't get the key for whatever reason\\n      if (!key) {\\n        return;\\n      }\\n\\n      // Seek by increment\\n      const seekByIncrement = increment => {\\n        // Divide the max duration into 10th's and times by the number value\\n        player.currentTime = player.duration / 10 * increment;\\n      };\\n\\n      // Handle the key on keydown\\n      // Reset on keyup\\n      if (pressed) {\\n        // Check focused element\\n        // and if the focused element is not editable (e.g. text input)\\n        // and any that accept key input http://webaim.org/techniques/keyboard/\\n        const focused = document.activeElement;\\n        if (is.element(focused)) {\\n          const {\\n            editable\\n          } = player.config.selectors;\\n          const {\\n            seek\\n          } = elements.inputs;\\n          if (focused !== seek && matches(focused, editable)) {\\n            return;\\n          }\\n          if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n            return;\\n          }\\n        }\\n\\n        // Which keys should we prevent default\\n        const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n        // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n        if (preventDefault.includes(key)) {\\n          event.preventDefault();\\n          event.stopPropagation();\\n        }\\n        switch (key) {\\n          case '0':\\n          case '1':\\n          case '2':\\n          case '3':\\n          case '4':\\n          case '5':\\n          case '6':\\n          case '7':\\n          case '8':\\n          case '9':\\n            if (!repeat) {\\n              seekByIncrement(parseInt(key, 10));\\n            }\\n            break;\\n          case ' ':\\n          case 'k':\\n            if (!repeat) {\\n              silencePromise(player.togglePlay());\\n            }\\n            break;\\n          case 'ArrowUp':\\n            player.increaseVolume(0.1);\\n            break;\\n          case 'ArrowDown':\\n            player.decreaseVolume(0.1);\\n            break;\\n          case 'm':\\n            if (!repeat) {\\n              player.muted = !player.muted;\\n            }\\n            break;\\n          case 'ArrowRight':\\n            player.forward();\\n            break;\\n          case 'ArrowLeft':\\n            player.rewind();\\n            break;\\n          case 'f':\\n            player.fullscreen.toggle();\\n            break;\\n          case 'c':\\n            if (!repeat) {\\n              player.toggleCaptions();\\n            }\\n            break;\\n          case 'l':\\n            player.loop = !player.loop;\\n            break;\\n        }\\n\\n        // Escape is handle natively when in full screen\\n        // So we only need to worry about non native\\n        if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n          player.fullscreen.toggle();\\n        }\\n\\n        // Store last key for next cycle\\n        this.lastKey = key;\\n      } else {\\n        this.lastKey = null;\\n      }\\n    }\\n\\n    // Toggle menu\\n    toggleMenu(event) {\\n      controls.toggleMenu.call(this.player, event);\\n    }\\n  }\\n\\n  var loadjs_umd = createCommonjsModule(function (module, exports) {\\n    (function (root, factory) {\\n      {\\n        module.exports = factory();\\n      }\\n    })(commonjsGlobal, function () {\\n      /**\\n       * Global dependencies.\\n       * @global {Object} document - DOM\\n       */\\n\\n      var devnull = function () {},\\n        bundleIdCache = {},\\n        bundleResultCache = {},\\n        bundleCallbackQueue = {};\\n\\n      /**\\n       * Subscribe to bundle load event.\\n       * @param {string[]} bundleIds - Bundle ids\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function subscribe(bundleIds, callbackFn) {\\n        // listify\\n        bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n        var depsNotFound = [],\\n          i = bundleIds.length,\\n          numWaiting = i,\\n          fn,\\n          bundleId,\\n          r,\\n          q;\\n\\n        // define callback function\\n        fn = function (bundleId, pathsNotFound) {\\n          if (pathsNotFound.length) depsNotFound.push(bundleId);\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(depsNotFound);\\n        };\\n\\n        // register callback\\n        while (i--) {\\n          bundleId = bundleIds[i];\\n\\n          // execute callback if in result cache\\n          r = bundleResultCache[bundleId];\\n          if (r) {\\n            fn(bundleId, r);\\n            continue;\\n          }\\n\\n          // add to callback queue\\n          q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n          q.push(fn);\\n        }\\n      }\\n\\n      /**\\n       * Publish bundle load event.\\n       * @param {string} bundleId - Bundle id\\n       * @param {string[]} pathsNotFound - List of files not found\\n       */\\n      function publish(bundleId, pathsNotFound) {\\n        // exit if id isn't defined\\n        if (!bundleId) return;\\n        var q = bundleCallbackQueue[bundleId];\\n\\n        // cache result\\n        bundleResultCache[bundleId] = pathsNotFound;\\n\\n        // exit if queue is empty\\n        if (!q) return;\\n\\n        // empty callback queue\\n        while (q.length) {\\n          q[0](bundleId, pathsNotFound);\\n          q.splice(0, 1);\\n        }\\n      }\\n\\n      /**\\n       * Execute callbacks.\\n       * @param {Object or Function} args - The callback args\\n       * @param {string[]} depsNotFound - List of dependencies not found\\n       */\\n      function executeCallbacks(args, depsNotFound) {\\n        // accept function as argument\\n        if (args.call) args = {\\n          success: args\\n        };\\n\\n        // success and error callbacks\\n        if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n      }\\n\\n      /**\\n       * Load individual file.\\n       * @param {string} path - The file path\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFile(path, callbackFn, args, numTries) {\\n        var doc = document,\\n          async = args.async,\\n          maxTries = (args.numRetries || 0) + 1,\\n          beforeCallbackFn = args.before || devnull,\\n          pathname = path.replace(/[\\\\?|#].*$/, ''),\\n          pathStripped = path.replace(/^(css|img)!/, ''),\\n          isLegacyIECss,\\n          e;\\n        numTries = numTries || 0;\\n        if (/(^css!|\\\\.css$)/.test(pathname)) {\\n          // css\\n          e = doc.createElement('link');\\n          e.rel = 'stylesheet';\\n          e.href = pathStripped;\\n\\n          // tag IE9+\\n          isLegacyIECss = 'hideFocus' in e;\\n\\n          // use preload in IE Edge (to detect load errors)\\n          if (isLegacyIECss && e.relList) {\\n            isLegacyIECss = 0;\\n            e.rel = 'preload';\\n            e.as = 'style';\\n          }\\n        } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n          // image\\n          e = doc.createElement('img');\\n          e.src = pathStripped;\\n        } else {\\n          // javascript\\n          e = doc.createElement('script');\\n          e.src = path;\\n          e.async = async === undefined ? true : async;\\n        }\\n        e.onload = e.onerror = e.onbeforeload = function (ev) {\\n          var result = ev.type[0];\\n\\n          // treat empty stylesheets as failures to get around lack of onerror\\n          // support in IE9-11\\n          if (isLegacyIECss) {\\n            try {\\n              if (!e.sheet.cssText.length) result = 'e';\\n            } catch (x) {\\n              // sheets objects created from load errors don't allow access to\\n              // `cssText` (unless error is Code:18 SecurityError)\\n              if (x.code != 18) result = 'e';\\n            }\\n          }\\n\\n          // handle retries in case of load failure\\n          if (result == 'e') {\\n            // increment counter\\n            numTries += 1;\\n\\n            // exit function and try again\\n            if (numTries < maxTries) {\\n              return loadFile(path, callbackFn, args, numTries);\\n            }\\n          } else if (e.rel == 'preload' && e.as == 'style') {\\n            // activate preloaded stylesheets\\n            return e.rel = 'stylesheet'; // jshint ignore:line\\n          }\\n\\n          // execute callback\\n          callbackFn(path, result, ev.defaultPrevented);\\n        };\\n\\n        // add to document (unless callback returns `false`)\\n        if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n      }\\n\\n      /**\\n       * Load multiple files.\\n       * @param {string[]} paths - The file paths\\n       * @param {Function} callbackFn - The callback function\\n       */\\n      function loadFiles(paths, callbackFn, args) {\\n        // listify paths\\n        paths = paths.push ? paths : [paths];\\n        var numWaiting = paths.length,\\n          x = numWaiting,\\n          pathsNotFound = [],\\n          fn,\\n          i;\\n\\n        // define callback function\\n        fn = function (path, result, defaultPrevented) {\\n          // handle error\\n          if (result == 'e') pathsNotFound.push(path);\\n\\n          // handle beforeload event. If defaultPrevented then that means the load\\n          // will be blocked (ex. Ghostery/ABP on Safari)\\n          if (result == 'b') {\\n            if (defaultPrevented) pathsNotFound.push(path);else return;\\n          }\\n          numWaiting--;\\n          if (!numWaiting) callbackFn(pathsNotFound);\\n        };\\n\\n        // load scripts\\n        for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n      }\\n\\n      /**\\n       * Initiate script load and register bundle.\\n       * @param {(string|string[])} paths - The file paths\\n       * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n       *   callback or (3) object literal with success/error arguments, numRetries,\\n       *   etc.\\n       * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n       *   literal with success/error arguments, numRetries, etc.\\n       */\\n      function loadjs(paths, arg1, arg2) {\\n        var bundleId, args;\\n\\n        // bundleId (if string)\\n        if (arg1 && arg1.trim) bundleId = arg1;\\n\\n        // args (default is {})\\n        args = (bundleId ? arg2 : arg1) || {};\\n\\n        // throw error if bundle is already defined\\n        if (bundleId) {\\n          if (bundleId in bundleIdCache) {\\n            throw \\\"LoadJS\\\";\\n          } else {\\n            bundleIdCache[bundleId] = true;\\n          }\\n        }\\n        function loadFn(resolve, reject) {\\n          loadFiles(paths, function (pathsNotFound) {\\n            // execute callbacks\\n            executeCallbacks(args, pathsNotFound);\\n\\n            // resolve Promise\\n            if (resolve) {\\n              executeCallbacks({\\n                success: resolve,\\n                error: reject\\n              }, pathsNotFound);\\n            }\\n\\n            // publish bundle load event\\n            publish(bundleId, pathsNotFound);\\n          }, args);\\n        }\\n        if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n      }\\n\\n      /**\\n       * Execute callbacks when dependencies have been satisfied.\\n       * @param {(string|string[])} deps - List of bundle ids\\n       * @param {Object} args - success/error arguments\\n       */\\n      loadjs.ready = function ready(deps, args) {\\n        // subscribe to bundle load event\\n        subscribe(deps, function (depsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, depsNotFound);\\n        });\\n        return loadjs;\\n      };\\n\\n      /**\\n       * Manually satisfy bundle dependencies.\\n       * @param {string} bundleId - The bundle id\\n       */\\n      loadjs.done = function done(bundleId) {\\n        publish(bundleId, []);\\n      };\\n\\n      /**\\n       * Reset loadjs dependencies statuses\\n       */\\n      loadjs.reset = function reset() {\\n        bundleIdCache = {};\\n        bundleResultCache = {};\\n        bundleCallbackQueue = {};\\n      };\\n\\n      /**\\n       * Determine if bundle has already been defined\\n       * @param String} bundleId - The bundle id\\n       */\\n      loadjs.isDefined = function isDefined(bundleId) {\\n        return bundleId in bundleIdCache;\\n      };\\n\\n      // export\\n      return loadjs;\\n    });\\n  });\\n\\n  // ==========================================================================\\n  function loadScript(url) {\\n    return new Promise((resolve, reject) => {\\n      loadjs_umd(url, {\\n        success: resolve,\\n        error: reject\\n      });\\n    });\\n  }\\n\\n  // ==========================================================================\\n\\n  // Parse Vimeo ID from URL\\n  function parseId$1(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    if (is.number(Number(url))) {\\n      return url;\\n    }\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Try to extract a hash for private videos from the URL\\n  function parseHash(url) {\\n    /* This regex matches a hexadecimal hash if given in any of these forms:\\n     *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n     *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n     *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n     *  - video/{id}/{hash}\\n     * If matched, the hash is available in capture group 4\\n     */\\n    const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n    const found = url.match(regex);\\n    return found && found.length === 5 ? found[4] : null;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState$1(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  const vimeo = {\\n    setup() {\\n      const player = this;\\n\\n      // Add embed class for responsive\\n      toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n      // Set speed options from config\\n      player.options.speed = player.config.speed.options;\\n\\n      // Set intial ratio\\n      setAspectRatio.call(player);\\n\\n      // Load the SDK if not already\\n      if (!is.object(window.Vimeo)) {\\n        loadScript(player.config.urls.vimeo.sdk).then(() => {\\n          vimeo.ready.call(player);\\n        }).catch(error => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n      } else {\\n        vimeo.ready.call(player);\\n      }\\n    },\\n    // API Ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.vimeo;\\n      const {\\n        premium,\\n        referrerPolicy,\\n        ...frameParams\\n      } = config;\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n      let hash = '';\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(player.config.attributes.embed.id);\\n        // hash can also be set as attribute on the <div>\\n        hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n      } else {\\n        hash = parseHash(source);\\n      }\\n      const hashParam = hash ? {\\n        h: hash\\n      } : {};\\n\\n      // If the owner has a pro or premium account then we can hide controls etc\\n      if (premium) {\\n        Object.assign(frameParams, {\\n          controls: false,\\n          sidedock: false\\n        });\\n      }\\n\\n      // Get Vimeo params for the iframe\\n      const params = buildUrlParams({\\n        loop: player.config.loop.active,\\n        autoplay: player.autoplay,\\n        muted: player.muted,\\n        gesture: 'media',\\n        playsinline: player.config.playsinline,\\n        // hash has to be added to iframe-URL\\n        ...hashParam,\\n        ...frameParams\\n      });\\n      const id = parseId$1(source);\\n      // Build an iframe\\n      const iframe = createElement('iframe');\\n      const src = format(player.config.urls.vimeo.iframe, id, params);\\n      iframe.setAttribute('src', src);\\n      iframe.setAttribute('allowfullscreen', '');\\n      iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n      // Set the referrer policy if required\\n      if (!is.empty(referrerPolicy)) {\\n        iframe.setAttribute('referrerPolicy', referrerPolicy);\\n      }\\n\\n      // Inject the package\\n      if (premium || !config.customControls) {\\n        iframe.setAttribute('data-poster', player.poster);\\n        player.media = replaceElement(iframe, player.media);\\n      } else {\\n        const wrapper = createElement('div', {\\n          class: player.config.classNames.embedContainer,\\n          'data-poster': player.poster\\n        });\\n        wrapper.appendChild(iframe);\\n        player.media = replaceElement(wrapper, player.media);\\n      }\\n\\n      // Get poster image\\n      if (!config.customControls) {\\n        fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n          if (is.empty(response) || !response.thumbnail_url) {\\n            return;\\n          }\\n\\n          // Set and show poster\\n          ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n        });\\n      }\\n\\n      // Setup instance\\n      // https://github.com/vimeo/player.js\\n      player.embed = new window.Vimeo.Player(iframe, {\\n        autopause: player.config.autopause,\\n        muted: player.muted\\n      });\\n      player.media.paused = true;\\n      player.media.currentTime = 0;\\n\\n      // Disable native text track rendering\\n      if (player.supported.ui) {\\n        player.embed.disableTextTrack();\\n      }\\n\\n      // Create a faux HTML5 API using the Vimeo API\\n      player.media.play = () => {\\n        assurePlaybackState$1.call(player, true);\\n        return player.embed.play();\\n      };\\n      player.media.pause = () => {\\n        assurePlaybackState$1.call(player, false);\\n        return player.embed.pause();\\n      };\\n      player.media.stop = () => {\\n        player.pause();\\n        player.currentTime = 0;\\n      };\\n\\n      // Seeking\\n      let {\\n        currentTime\\n      } = player.media;\\n      Object.defineProperty(player.media, 'currentTime', {\\n        get() {\\n          return currentTime;\\n        },\\n        set(time) {\\n          // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n          // Get current paused state and volume etc\\n          const {\\n            embed,\\n            media,\\n            paused,\\n            volume\\n          } = player;\\n          const restorePause = paused && !embed.hasPlayed;\\n\\n          // Set seeking state and trigger event\\n          media.seeking = true;\\n          triggerEvent.call(player, media, 'seeking');\\n\\n          // If paused, mute until seek is complete\\n          Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n            // Do nothing\\n          });\\n        }\\n      });\\n\\n      // Playback speed\\n      let speed = player.config.speed.selected;\\n      Object.defineProperty(player.media, 'playbackRate', {\\n        get() {\\n          return speed;\\n        },\\n        set(input) {\\n          player.embed.setPlaybackRate(input).then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          }).catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n        }\\n      });\\n\\n      // Volume\\n      let {\\n        volume\\n      } = player.config;\\n      Object.defineProperty(player.media, 'volume', {\\n        get() {\\n          return volume;\\n        },\\n        set(input) {\\n          player.embed.setVolume(input).then(() => {\\n            volume = input;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Muted\\n      let {\\n        muted\\n      } = player.config;\\n      Object.defineProperty(player.media, 'muted', {\\n        get() {\\n          return muted;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : false;\\n          player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n            muted = toggle;\\n            triggerEvent.call(player, player.media, 'volumechange');\\n          });\\n        }\\n      });\\n\\n      // Loop\\n      let {\\n        loop\\n      } = player.config;\\n      Object.defineProperty(player.media, 'loop', {\\n        get() {\\n          return loop;\\n        },\\n        set(input) {\\n          const toggle = is.boolean(input) ? input : player.config.loop.active;\\n          player.embed.setLoop(toggle).then(() => {\\n            loop = toggle;\\n          });\\n        }\\n      });\\n\\n      // Source\\n      let currentSrc;\\n      player.embed.getVideoUrl().then(value => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      }).catch(error => {\\n        this.debug.warn(error);\\n      });\\n      Object.defineProperty(player.media, 'currentSrc', {\\n        get() {\\n          return currentSrc;\\n        }\\n      });\\n\\n      // Ended\\n      Object.defineProperty(player.media, 'ended', {\\n        get() {\\n          return player.currentTime === player.duration;\\n        }\\n      });\\n\\n      // Set aspect ratio based on video size\\n      Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n        const [width, height] = dimensions;\\n        player.embed.ratio = roundAspectRatio(width, height);\\n        setAspectRatio.call(this);\\n      });\\n\\n      // Set autopause\\n      player.embed.setAutopause(player.config.autopause).then(state => {\\n        player.config.autopause = state;\\n      });\\n\\n      // Get title\\n      player.embed.getVideoTitle().then(title => {\\n        player.config.title = title;\\n        ui.setTitle.call(this);\\n      });\\n\\n      // Get current time\\n      player.embed.getCurrentTime().then(value => {\\n        currentTime = value;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n\\n      // Get duration\\n      player.embed.getDuration().then(value => {\\n        player.media.duration = value;\\n        triggerEvent.call(player, player.media, 'durationchange');\\n      });\\n\\n      // Get captions\\n      player.embed.getTextTracks().then(tracks => {\\n        player.media.textTracks = tracks;\\n        captions.setup.call(player);\\n      });\\n      player.embed.on('cuechange', ({\\n        cues = []\\n      }) => {\\n        const strippedCues = cues.map(cue => stripHTML(cue.text));\\n        captions.updateCues.call(player, strippedCues);\\n      });\\n      player.embed.on('loaded', () => {\\n        // Assure state and events are updated on autoplay\\n        player.embed.getPaused().then(paused => {\\n          assurePlaybackState$1.call(player, !paused);\\n          if (!paused) {\\n            triggerEvent.call(player, player.media, 'playing');\\n          }\\n        });\\n        if (is.element(player.embed.element) && player.supported.ui) {\\n          const frame = player.embed.element;\\n\\n          // Fix keyboard focus issues\\n          // https://github.com/sampotts/plyr/issues/317\\n          frame.setAttribute('tabindex', -1);\\n        }\\n      });\\n      player.embed.on('bufferstart', () => {\\n        triggerEvent.call(player, player.media, 'waiting');\\n      });\\n      player.embed.on('bufferend', () => {\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('play', () => {\\n        assurePlaybackState$1.call(player, true);\\n        triggerEvent.call(player, player.media, 'playing');\\n      });\\n      player.embed.on('pause', () => {\\n        assurePlaybackState$1.call(player, false);\\n      });\\n      player.embed.on('timeupdate', data => {\\n        player.media.seeking = false;\\n        currentTime = data.seconds;\\n        triggerEvent.call(player, player.media, 'timeupdate');\\n      });\\n      player.embed.on('progress', data => {\\n        player.media.buffered = data.percent;\\n        triggerEvent.call(player, player.media, 'progress');\\n\\n        // Check all loaded\\n        if (parseInt(data.percent, 10) === 1) {\\n          triggerEvent.call(player, player.media, 'canplaythrough');\\n        }\\n\\n        // Get duration as if we do it before load, it gives an incorrect value\\n        // https://github.com/sampotts/plyr/issues/891\\n        player.embed.getDuration().then(value => {\\n          if (value !== player.media.duration) {\\n            player.media.duration = value;\\n            triggerEvent.call(player, player.media, 'durationchange');\\n          }\\n        });\\n      });\\n      player.embed.on('seeked', () => {\\n        player.media.seeking = false;\\n        triggerEvent.call(player, player.media, 'seeked');\\n      });\\n      player.embed.on('ended', () => {\\n        player.media.paused = true;\\n        triggerEvent.call(player, player.media, 'ended');\\n      });\\n      player.embed.on('error', detail => {\\n        player.media.error = detail;\\n        triggerEvent.call(player, player.media, 'error');\\n      });\\n\\n      // Rebuild UI\\n      if (config.customControls) {\\n        setTimeout(() => ui.build.call(player), 0);\\n      }\\n    }\\n  };\\n\\n  // ==========================================================================\\n\\n  // Parse YouTube ID from URL\\n  function parseId(url) {\\n    if (is.empty(url)) {\\n      return null;\\n    }\\n    const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n    return url.match(regex) ? RegExp.$2 : url;\\n  }\\n\\n  // Set playback state and trigger change (only on actual change)\\n  function assurePlaybackState(play) {\\n    if (play && !this.embed.hasPlayed) {\\n      this.embed.hasPlayed = true;\\n    }\\n    if (this.media.paused === play) {\\n      this.media.paused = !play;\\n      triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n    }\\n  }\\n  function getHost(config) {\\n    if (config.noCookie) {\\n      return 'https://www.youtube-nocookie.com';\\n    }\\n    if (window.location.protocol === 'http:') {\\n      return 'http://www.youtube.com';\\n    }\\n\\n    // Use YouTube's default\\n    return undefined;\\n  }\\n  const youtube = {\\n    setup() {\\n      // Add embed class for responsive\\n      toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n      // Setup API\\n      if (is.object(window.YT) && is.function(window.YT.Player)) {\\n        youtube.ready.call(this);\\n      } else {\\n        // Reference current global callback\\n        const callback = window.onYouTubeIframeAPIReady;\\n\\n        // Set callback to process queue\\n        window.onYouTubeIframeAPIReady = () => {\\n          // Call global callback if set\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n          youtube.ready.call(this);\\n        };\\n\\n        // Load the SDK\\n        loadScript(this.config.urls.youtube.sdk).catch(error => {\\n          this.debug.warn('YouTube API failed to load', error);\\n        });\\n      }\\n    },\\n    // Get the media title\\n    getTitle(videoId) {\\n      const url = format(this.config.urls.youtube.api, videoId);\\n      fetch(url).then(data => {\\n        if (is.object(data)) {\\n          const {\\n            title,\\n            height,\\n            width\\n          } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n        setAspectRatio.call(this);\\n      }).catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n    },\\n    // API ready\\n    ready() {\\n      const player = this;\\n      const config = player.config.youtube;\\n      // Ignore already setup (race condition)\\n      const currentId = player.media && player.media.getAttribute('id');\\n      if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n        return;\\n      }\\n\\n      // Get the source URL or ID\\n      let source = player.media.getAttribute('src');\\n\\n      // Get from <div> if needed\\n      if (is.empty(source)) {\\n        source = player.media.getAttribute(this.config.attributes.embed.id);\\n      }\\n\\n      // Replace the <iframe> with a <div> due to YouTube API issues\\n      const videoId = parseId(source);\\n      const id = generateId(player.provider);\\n      // Replace media element\\n      const container = createElement('div', {\\n        id,\\n        'data-poster': config.customControls ? player.poster : undefined\\n      });\\n      player.media = replaceElement(container, player.media);\\n\\n      // Only load the poster when using custom controls\\n      if (config.customControls) {\\n        const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n        // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n        loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        }).catch(() => {});\\n      }\\n\\n      // Setup instance\\n      // https://developers.google.com/youtube/iframe_api_reference\\n      player.embed = new window.YT.Player(player.media, {\\n        videoId,\\n        host: getHost(config),\\n        playerVars: extend({}, {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null\\n        }, config),\\n        events: {\\n          onError(event) {\\n            // YouTube may fire onError twice, so only handle it once\\n            if (!player.media.error) {\\n              const code = event.data;\\n              // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n              const message = {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n              }[code] || 'An unknown error occurred';\\n              player.media.error = {\\n                code,\\n                message\\n              };\\n              triggerEvent.call(player, player.media, 'error');\\n            }\\n          },\\n          onPlaybackRateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get current speed\\n            player.media.playbackRate = instance.getPlaybackRate();\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          },\\n          onReady(event) {\\n            // Bail if onReady has already been called. See issue #1108\\n            if (is.function(player.media.play)) {\\n              return;\\n            }\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Get the title\\n            youtube.getTitle.call(player, videoId);\\n\\n            // Create a faux HTML5 API using the YouTube API\\n            player.media.play = () => {\\n              assurePlaybackState.call(player, true);\\n              instance.playVideo();\\n            };\\n            player.media.pause = () => {\\n              assurePlaybackState.call(player, false);\\n              instance.pauseVideo();\\n            };\\n            player.media.stop = () => {\\n              instance.stopVideo();\\n            };\\n            player.media.duration = instance.getDuration();\\n            player.media.paused = true;\\n\\n            // Seeking\\n            player.media.currentTime = 0;\\n            Object.defineProperty(player.media, 'currentTime', {\\n              get() {\\n                return Number(instance.getCurrentTime());\\n              },\\n              set(time) {\\n                // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n                if (player.paused && !player.embed.hasPlayed) {\\n                  player.embed.mute();\\n                }\\n\\n                // Set seeking state and trigger event\\n                player.media.seeking = true;\\n                triggerEvent.call(player, player.media, 'seeking');\\n\\n                // Seek after events sent\\n                instance.seekTo(time);\\n              }\\n            });\\n\\n            // Playback speed\\n            Object.defineProperty(player.media, 'playbackRate', {\\n              get() {\\n                return instance.getPlaybackRate();\\n              },\\n              set(input) {\\n                instance.setPlaybackRate(input);\\n              }\\n            });\\n\\n            // Volume\\n            let {\\n              volume\\n            } = player.config;\\n            Object.defineProperty(player.media, 'volume', {\\n              get() {\\n                return volume;\\n              },\\n              set(input) {\\n                volume = input;\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Muted\\n            let {\\n              muted\\n            } = player.config;\\n            Object.defineProperty(player.media, 'muted', {\\n              get() {\\n                return muted;\\n              },\\n              set(input) {\\n                const toggle = is.boolean(input) ? input : muted;\\n                muted = toggle;\\n                instance[toggle ? 'mute' : 'unMute']();\\n                instance.setVolume(volume * 100);\\n                triggerEvent.call(player, player.media, 'volumechange');\\n              }\\n            });\\n\\n            // Source\\n            Object.defineProperty(player.media, 'currentSrc', {\\n              get() {\\n                return instance.getVideoUrl();\\n              }\\n            });\\n\\n            // Ended\\n            Object.defineProperty(player.media, 'ended', {\\n              get() {\\n                return player.currentTime === player.duration;\\n              }\\n            });\\n\\n            // Get available speeds\\n            const speeds = instance.getAvailablePlaybackRates();\\n            // Filter based on config\\n            player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n            // Set the tabindex to avoid focus entering iframe\\n            if (player.supported.ui && config.customControls) {\\n              player.media.setAttribute('tabindex', -1);\\n            }\\n            triggerEvent.call(player, player.media, 'timeupdate');\\n            triggerEvent.call(player, player.media, 'durationchange');\\n\\n            // Reset timer\\n            clearInterval(player.timers.buffering);\\n\\n            // Setup buffering\\n            player.timers.buffering = setInterval(() => {\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n\\n              // Trigger progress only when we actually buffer something\\n              if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n                triggerEvent.call(player, player.media, 'progress');\\n              }\\n\\n              // Set last buffer point\\n              player.media.lastBuffered = player.media.buffered;\\n\\n              // Bail if we're at 100%\\n              if (player.media.buffered === 1) {\\n                clearInterval(player.timers.buffering);\\n\\n                // Trigger event\\n                triggerEvent.call(player, player.media, 'canplaythrough');\\n              }\\n            }, 200);\\n\\n            // Rebuild UI\\n            if (config.customControls) {\\n              setTimeout(() => ui.build.call(player), 50);\\n            }\\n          },\\n          onStateChange(event) {\\n            // Get the instance\\n            const instance = event.target;\\n\\n            // Reset timer\\n            clearInterval(player.timers.playing);\\n            const seeked = player.media.seeking && [1, 2].includes(event.data);\\n            if (seeked) {\\n              // Unset seeking and fire seeked event\\n              player.media.seeking = false;\\n              triggerEvent.call(player, player.media, 'seeked');\\n            }\\n\\n            // Handle events\\n            // -1   Unstarted\\n            // 0    Ended\\n            // 1    Playing\\n            // 2    Paused\\n            // 3    Buffering\\n            // 5    Video cued\\n            switch (event.data) {\\n              case -1:\\n                // Update scrubber\\n                triggerEvent.call(player, player.media, 'timeupdate');\\n\\n                // Get loaded % from YouTube\\n                player.media.buffered = instance.getVideoLoadedFraction();\\n                triggerEvent.call(player, player.media, 'progress');\\n                break;\\n              case 0:\\n                assurePlaybackState.call(player, false);\\n\\n                // YouTube doesn't support loop for a single video, so mimick it.\\n                if (player.media.loop) {\\n                  // YouTube needs a call to `stopVideo` before playing again\\n                  instance.stopVideo();\\n                  instance.playVideo();\\n                } else {\\n                  triggerEvent.call(player, player.media, 'ended');\\n                }\\n                break;\\n              case 1:\\n                // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                  player.media.pause();\\n                } else {\\n                  assurePlaybackState.call(player, true);\\n                  triggerEvent.call(player, player.media, 'playing');\\n\\n                  // Poll to get playback progress\\n                  player.timers.playing = setInterval(() => {\\n                    triggerEvent.call(player, player.media, 'timeupdate');\\n                  }, 50);\\n\\n                  // Check duration again due to YouTube bug\\n                  // https://github.com/sampotts/plyr/issues/374\\n                  // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                  if (player.media.duration !== instance.getDuration()) {\\n                    player.media.duration = instance.getDuration();\\n                    triggerEvent.call(player, player.media, 'durationchange');\\n                  }\\n                }\\n                break;\\n              case 2:\\n                // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n                if (!player.muted) {\\n                  player.embed.unMute();\\n                }\\n                assurePlaybackState.call(player, false);\\n                break;\\n              case 3:\\n                // Trigger waiting event to add loading classes to container as the video buffers.\\n                triggerEvent.call(player, player.media, 'waiting');\\n                break;\\n            }\\n            triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n              code: event.data\\n            });\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  // ==========================================================================\\n  const media = {\\n    // Setup media\\n    setup() {\\n      // If there's no media, bail\\n      if (!this.media) {\\n        this.debug.warn('No media element found!');\\n        return;\\n      }\\n\\n      // Add type class\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n      // Add provider class\\n      toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n      // Add video class for embeds\\n      // This will require changes if audio embeds are added\\n      if (this.isEmbed) {\\n        toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n      }\\n\\n      // Inject the player wrapper\\n      if (this.isVideo) {\\n        // Create the wrapper div\\n        this.elements.wrapper = createElement('div', {\\n          class: this.config.classNames.video\\n        });\\n\\n        // Wrap the video in a container\\n        wrap(this.media, this.elements.wrapper);\\n\\n        // Poster image container\\n        this.elements.poster = createElement('div', {\\n          class: this.config.classNames.poster\\n        });\\n        this.elements.wrapper.appendChild(this.elements.poster);\\n      }\\n      if (this.isHTML5) {\\n        html5.setup.call(this);\\n      } else if (this.isYouTube) {\\n        youtube.setup.call(this);\\n      } else if (this.isVimeo) {\\n        vimeo.setup.call(this);\\n      }\\n    }\\n  };\\n\\n  const destroy = instance => {\\n    // Destroy our adsManager\\n    if (instance.manager) {\\n      instance.manager.destroy();\\n    }\\n\\n    // Destroy our adsManager\\n    if (instance.elements.displayContainer) {\\n      instance.elements.displayContainer.destroy();\\n    }\\n    instance.elements.container.remove();\\n  };\\n  class Ads {\\n    /**\\n     * Ads constructor.\\n     * @param {Object} player\\n     * @return {Ads}\\n     */\\n    constructor(player) {\\n      /**\\n       * Load the IMA SDK\\n       */\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Check if the Google IMA3 SDK is loaded or load it ourselves\\n        if (!is.object(window.google) || !is.object(window.google.ima)) {\\n          loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n            this.ready();\\n          }).catch(() => {\\n            // Script failed to load or is blocked\\n            this.trigger('error', new Error('Google IMA SDK failed to load'));\\n          });\\n        } else {\\n          this.ready();\\n        }\\n      });\\n      /**\\n       * Get the ads instance ready\\n       */\\n      _defineProperty$1(this, \\\"ready\\\", () => {\\n        // Double check we're enabled\\n        if (!this.enabled) {\\n          destroy(this);\\n        }\\n\\n        // Start ticking our safety timer. If the whole advertisement\\n        // thing doesn't resolve within our set time; we bail\\n        this.startSafetyTimer(12000, 'ready()');\\n\\n        // Clear the safety timer\\n        this.managerPromise.then(() => {\\n          this.clearSafetyTimer('onAdsManagerLoaded()');\\n        });\\n\\n        // Set listeners on the Plyr instance\\n        this.listeners();\\n\\n        // Setup the IMA SDK\\n        this.setupIMA();\\n      });\\n      /**\\n       * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n       * so here we define our ad container. This div is set up to render on top of the video player.\\n       * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n       * handle to the content video player - the SDK will poll the current time of our player to\\n       * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n       * mobile devices, this initialization is done as the result of a user action.\\n       */\\n      _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n        // Create the container for our advertisements\\n        this.elements.container = createElement('div', {\\n          class: this.player.config.classNames.ads\\n        });\\n        this.player.elements.container.appendChild(this.elements.container);\\n\\n        // So we can run VPAID2\\n        google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n        // Set language\\n        google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n        // Set playback for iOS10+\\n        google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n        // We assume the adContainer is the video container of the plyr element that will house the ads\\n        this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n        // Create ads loader\\n        this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n        // Listen and respond to ads loaded and error events\\n        this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n        this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n        // Request video ads to be pre-loaded\\n        this.requestAds();\\n      });\\n      /**\\n       * Request advertisements\\n       */\\n      _defineProperty$1(this, \\\"requestAds\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        try {\\n          // Request video ads\\n          const request = new google.ima.AdsRequest();\\n          request.adTagUrl = this.tagUrl;\\n\\n          // Specify the linear and nonlinear slot sizes. This helps the SDK\\n          // to select the correct creative if multiple are returned\\n          request.linearAdSlotWidth = container.offsetWidth;\\n          request.linearAdSlotHeight = container.offsetHeight;\\n          request.nonLinearAdSlotWidth = container.offsetWidth;\\n          request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n          // We only overlay ads as we only support video.\\n          request.forceNonLinearFullSlot = false;\\n\\n          // Mute based on current state\\n          request.setAdWillPlayMuted(!this.player.muted);\\n          this.loader.requestAds(request);\\n        } catch (error) {\\n          this.onAdError(error);\\n        }\\n      });\\n      /**\\n       * Update the ad countdown\\n       * @param {Boolean} start\\n       */\\n      _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n        if (!start) {\\n          clearInterval(this.countdownTimer);\\n          this.elements.container.removeAttribute('data-badge-text');\\n          return;\\n        }\\n        const update = () => {\\n          const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n          const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n          this.elements.container.setAttribute('data-badge-text', label);\\n        };\\n        this.countdownTimer = setInterval(update, 100);\\n      });\\n      /**\\n       * This method is called whenever the ads are ready inside the AdDisplayContainer\\n       * @param {Event} event - adsManagerLoadedEvent\\n       */\\n      _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n        // Load could occur after a source change (race condition)\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Get the ads manager\\n        const settings = new google.ima.AdsRenderingSettings();\\n\\n        // Tell the SDK to save and restore content video state on our behalf\\n        settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n        settings.enablePreloading = true;\\n\\n        // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n        // so it can determine when to start the mid- and post-roll\\n        this.manager = event.getAdsManager(this.player, settings);\\n\\n        // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n        this.cuePoints = this.manager.getCuePoints();\\n\\n        // Add listeners to the required events\\n        // Advertisement error events\\n        this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n        // Advertisement regular events\\n        Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n          this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n        });\\n\\n        // Resolve our adsManager\\n        this.trigger('loaded');\\n      });\\n      _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n        // Add advertisement cue's within the time line if available\\n        if (!is.empty(this.cuePoints)) {\\n          this.cuePoints.forEach(cuePoint => {\\n            if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n              const seekElement = this.player.elements.progress;\\n              if (is.element(seekElement)) {\\n                const cuePercentage = 100 / this.player.duration * cuePoint;\\n                const cue = createElement('span', {\\n                  class: this.player.config.classNames.cues\\n                });\\n                cue.style.left = `${cuePercentage.toString()}%`;\\n                seekElement.appendChild(cue);\\n              }\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n       * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n       * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n        // don't have ad object associated\\n        const ad = event.getAd();\\n        const adData = event.getAdData();\\n\\n        // Proxy event\\n        const dispatchEvent = type => {\\n          triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n        };\\n\\n        // Bubble the event\\n        dispatchEvent(event.type);\\n        switch (event.type) {\\n          case google.ima.AdEvent.Type.LOADED:\\n            // This is the first event sent for an ad - it is possible to determine whether the\\n            // ad is a video ad or an overlay\\n            this.trigger('loaded');\\n\\n            // Start countdown\\n            this.pollCountdown(true);\\n            if (!ad.isLinear()) {\\n              // Position AdDisplayContainer correctly for overlay\\n              ad.width = container.offsetWidth;\\n              ad.height = container.offsetHeight;\\n            }\\n\\n            // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n            // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n            break;\\n          case google.ima.AdEvent.Type.STARTED:\\n            // Set volume to match player\\n            this.manager.setVolume(this.player.volume);\\n            break;\\n          case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n            // All ads for the current videos are done. We can now request new advertisements\\n            // in case the video is re-played\\n\\n            // TODO: Example for what happens when a next video in a playlist would be loaded.\\n            // So here we load a new video when all ads are done.\\n            // Then we load new ads within a new adsManager. When the video\\n            // Is started - after - the ads are loaded, then we get ads.\\n            // You can also easily test cancelling and reloading by running\\n            // player.ads.cancel() and player.ads.play from the console I guess.\\n            // this.player.source = {\\n            //     type: 'video',\\n            //     title: 'View From A Blue Moon',\\n            //     sources: [{\\n            //         src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n            // 'video/mp4', }], poster:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n            // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n            // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n            // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n            // };\\n\\n            // TODO: So there is still this thing where a video should only be allowed to start\\n            // playing when the IMA SDK is ready or has failed\\n\\n            if (this.player.ended) {\\n              this.loadAds();\\n            } else {\\n              // The SDK won't allow new ads to be called without receiving a contentComplete()\\n              this.loader.contentComplete();\\n            }\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n            // This event indicates the ad has started - the video player can adjust the UI,\\n            // for example display a pause button and remaining time. Fired when content should\\n            // be paused. This usually happens right before an ad is about to cover the content\\n\\n            this.pauseContent();\\n            break;\\n          case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n            // This event indicates the ad has finished - the video player can perform\\n            // appropriate UI actions, such as removing the timer for remaining time detection.\\n            // Fired when content should be resumed. This usually happens when an ad finishes\\n            // or collapses\\n\\n            this.pollCountdown();\\n            this.resumeContent();\\n            break;\\n          case google.ima.AdEvent.Type.LOG:\\n            if (adData.adError) {\\n              this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n            }\\n            break;\\n        }\\n      });\\n      /**\\n       * Any ad error handling comes through here\\n       * @param {Event} event\\n       */\\n      _defineProperty$1(this, \\\"onAdError\\\", event => {\\n        this.cancel();\\n        this.player.debug.warn('Ads error', event);\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events. This ensures\\n       * the mid- and post-roll launch at the correct time. And\\n       * resize the advertisement when the player resizes\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        let time;\\n        this.player.on('canplay', () => {\\n          this.addCuePoints();\\n        });\\n        this.player.on('ended', () => {\\n          this.loader.contentComplete();\\n        });\\n        this.player.on('timeupdate', () => {\\n          time = this.player.currentTime;\\n        });\\n        this.player.on('seeked', () => {\\n          const seekedTime = this.player.currentTime;\\n          if (is.empty(this.cuePoints)) {\\n            return;\\n          }\\n          this.cuePoints.forEach((cuePoint, index) => {\\n            if (time < cuePoint && cuePoint < seekedTime) {\\n              this.manager.discardAdBreak();\\n              this.cuePoints.splice(index, 1);\\n            }\\n          });\\n        });\\n\\n        // Listen to the resizing of the window. And resize ad accordingly\\n        // TODO: eventually implement ResizeObserver\\n        window.addEventListener('resize', () => {\\n          if (this.manager) {\\n            this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n          }\\n        });\\n      });\\n      /**\\n       * Initialize the adsManager and start playing advertisements\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        const {\\n          container\\n        } = this.player.elements;\\n        if (!this.managerPromise) {\\n          this.resumeContent();\\n        }\\n\\n        // Play the requested advertisement whenever the adsManager is ready\\n        this.managerPromise.then(() => {\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n\\n          // Initialize the container. Must be done via a user action on mobile devices\\n          this.elements.displayContainer.initialize();\\n          try {\\n            if (!this.initialized) {\\n              // Initialize the ads manager. Ad rules playlist will start at this time\\n              this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n              // Call play to start showing the ad. Single video and overlay ads will\\n              // start at this time; the call will be ignored for ad rules\\n              this.manager.start();\\n            }\\n            this.initialized = true;\\n          } catch (adError) {\\n            // An error may be thrown if there was a problem with the\\n            // VAST response\\n            this.onAdError(adError);\\n          }\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Resume our video\\n       */\\n      _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n        // Hide the advertisement container\\n        this.elements.container.style.zIndex = '';\\n\\n        // Ad is stopped\\n        this.playing = false;\\n\\n        // Play video\\n        silencePromise(this.player.media.play());\\n      });\\n      /**\\n       * Pause our video\\n       */\\n      _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n        // Show the advertisement container\\n        this.elements.container.style.zIndex = 3;\\n\\n        // Ad is playing\\n        this.playing = true;\\n\\n        // Pause our video.\\n        this.player.media.pause();\\n      });\\n      /**\\n       * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n       * allowed to call new ads based on google policies, as they interpret this as an accidental\\n       * video requests. https://developers.google.com/interactive-\\n       * media-ads/docs/sdks/android/faq#8\\n       */\\n      _defineProperty$1(this, \\\"cancel\\\", () => {\\n        // Pause our video\\n        if (this.initialized) {\\n          this.resumeContent();\\n        }\\n\\n        // Tell our instance that we're done for now\\n        this.trigger('error');\\n\\n        // Re-create our adsManager\\n        this.loadAds();\\n      });\\n      /**\\n       * Re-create our adsManager\\n       */\\n      _defineProperty$1(this, \\\"loadAds\\\", () => {\\n        // Tell our adsManager to go bye bye\\n        this.managerPromise.then(() => {\\n          // Destroy our adsManager\\n          if (this.manager) {\\n            this.manager.destroy();\\n          }\\n\\n          // Re-set our adsManager promises\\n          this.managerPromise = new Promise(resolve => {\\n            this.on('loaded', resolve);\\n            this.player.debug.log(this.manager);\\n          });\\n          // Now that the manager has been destroyed set it to also be un-initialized\\n          this.initialized = false;\\n\\n          // Now request some new advertisements\\n          this.requestAds();\\n        }).catch(() => {});\\n      });\\n      /**\\n       * Handles callbacks after an ad event was invoked\\n       * @param {String} event - Event type\\n       * @param args\\n       */\\n      _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n        const handlers = this.events[event];\\n        if (is.array(handlers)) {\\n          handlers.forEach(handler => {\\n            if (is.function(handler)) {\\n              handler.apply(this, args);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       * @return {Ads}\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        if (!is.array(this.events[event])) {\\n          this.events[event] = [];\\n        }\\n        this.events[event].push(callback);\\n        return this;\\n      });\\n      /**\\n       * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n       * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n       * advertisement is playing, or when a user action is required to start, then we clear the\\n       * timer on ad ready\\n       * @param {Number} time\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n        this.player.debug.log(`Safety timer invoked from: ${from}`);\\n        this.safetyTimer = setTimeout(() => {\\n          this.cancel();\\n          this.clearSafetyTimer('startSafetyTimer()');\\n        }, time);\\n      });\\n      /**\\n       * Clear our safety timer(s)\\n       * @param {String} from\\n       */\\n      _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n        if (!is.nullOrUndefined(this.safetyTimer)) {\\n          this.player.debug.log(`Safety timer cleared from: ${from}`);\\n          clearTimeout(this.safetyTimer);\\n          this.safetyTimer = null;\\n        }\\n      });\\n      this.player = player;\\n      this.config = player.config.ads;\\n      this.playing = false;\\n      this.initialized = false;\\n      this.elements = {\\n        container: null,\\n        displayContainer: null\\n      };\\n      this.manager = null;\\n      this.loader = null;\\n      this.cuePoints = null;\\n      this.events = {};\\n      this.safetyTimer = null;\\n      this.countdownTimer = null;\\n\\n      // Setup a promise to resolve when the IMA manager is ready\\n      this.managerPromise = new Promise((resolve, reject) => {\\n        // The ad is loaded and ready\\n        this.on('loaded', resolve);\\n\\n        // Ads failed\\n        this.on('error', reject);\\n      });\\n      this.load();\\n    }\\n    get enabled() {\\n      const {\\n        config\\n      } = this;\\n      return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n    }\\n    // Build the tag URL\\n    get tagUrl() {\\n      const {\\n        config\\n      } = this;\\n      if (is.url(config.tagUrl)) {\\n        return config.tagUrl;\\n      }\\n      const params = {\\n        AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n        AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n        AV_URL: window.location.hostname,\\n        cb: Date.now(),\\n        AV_WIDTH: 640,\\n        AV_HEIGHT: 480,\\n        AV_CDIM2: config.publisherId\\n      };\\n      const base = 'https://go.aniview.com/api/adserver6/vast/';\\n      return `${base}?${buildUrlParams(params)}`;\\n    }\\n  }\\n\\n  /**\\n   * Returns a number whose value is limited to the given range.\\n   *\\n   * Example: limit the output of this computation to between 0 and 255\\n   * (x * 255).clamp(0, 255)\\n   *\\n   * @param {Number} input\\n   * @param {Number} min The lower boundary of the output range\\n   * @param {Number} max The upper boundary of the output range\\n   * @returns A number within the bounds of min and max\\n   * @type Number\\n   */\\n  function clamp(input = 0, min = 0, max = 255) {\\n    return Math.min(Math.max(input, min), max);\\n  }\\n\\n  // Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\n  const parseVtt = vttDataString => {\\n    const processedList = [];\\n    const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n    frames.forEach(frame => {\\n      const result = {};\\n      const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n      lines.forEach(line => {\\n        if (!is.number(result.startTime)) {\\n          // The line with start and end times on it is the first line of interest\\n          const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n          if (matchTimes) {\\n            result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n            result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n          }\\n        } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n          // If we already have the startTime, then we're definitely up to the text line(s)\\n          const lineSplit = line.trim().split('#xywh=');\\n          [result.text] = lineSplit;\\n\\n          // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n          if (lineSplit[1]) {\\n            [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n          }\\n        }\\n      });\\n      if (result.text) {\\n        processedList.push(result);\\n      }\\n    });\\n    return processedList;\\n  };\\n\\n  /**\\n   * Preview thumbnails for seek hover and scrubbing\\n   * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n   * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n   *\\n   * Notes:\\n   * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n   * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n   * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n   */\\n\\n  const fitRatio = (ratio, outer) => {\\n    const targetRatio = outer.width / outer.height;\\n    const result = {};\\n    if (ratio > targetRatio) {\\n      result.width = outer.width;\\n      result.height = 1 / ratio * outer.width;\\n    } else {\\n      result.height = outer.height;\\n      result.width = ratio * outer.height;\\n    }\\n    return result;\\n  };\\n  class PreviewThumbnails {\\n    /**\\n     * PreviewThumbnails constructor.\\n     * @param {Plyr} player\\n     * @return {PreviewThumbnails}\\n     */\\n    constructor(player) {\\n      _defineProperty$1(this, \\\"load\\\", () => {\\n        // Toggle the regular seek tooltip\\n        if (this.player.elements.display.seekTooltip) {\\n          this.player.elements.display.seekTooltip.hidden = this.enabled;\\n        }\\n        if (!this.enabled) return;\\n        this.getThumbnails().then(() => {\\n          if (!this.enabled) {\\n            return;\\n          }\\n\\n          // Render DOM elements\\n          this.render();\\n\\n          // Check to see if thumb container size was specified manually in CSS\\n          this.determineContainerAutoSizing();\\n\\n          // Set up listeners\\n          this.listeners();\\n          this.loaded = true;\\n        });\\n      });\\n      // Download VTT files and parse them\\n      _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n        return new Promise(resolve => {\\n          const {\\n            src\\n          } = this.player.config.previewThumbnails;\\n          if (is.empty(src)) {\\n            throw new Error('Missing previewThumbnails.src config attribute');\\n          }\\n\\n          // Resolve promise\\n          const sortAndResolve = () => {\\n            // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n            this.thumbnails.sort((x, y) => x.height - y.height);\\n            this.player.debug.log('Preview thumbnails', this.thumbnails);\\n            resolve();\\n          };\\n\\n          // Via callback()\\n          if (is.function(src)) {\\n            src(thumbnails => {\\n              this.thumbnails = thumbnails;\\n              sortAndResolve();\\n            });\\n          }\\n          // VTT urls\\n          else {\\n            // If string, convert into single-element list\\n            const urls = is.string(src) ? [src] : src;\\n            // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n            const promises = urls.map(u => this.getThumbnail(u));\\n            // Resolve\\n            Promise.all(promises).then(sortAndResolve);\\n          }\\n        });\\n      });\\n      // Process individual VTT file\\n      _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n        return new Promise(resolve => {\\n          fetch(url).then(response => {\\n            const thumbnail = {\\n              frames: parseVtt(response),\\n              height: null,\\n              urlPrefix: ''\\n            };\\n\\n            // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n            // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n            // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n            if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n              thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n            }\\n\\n            // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n            const tempImage = new Image();\\n            tempImage.onload = () => {\\n              thumbnail.height = tempImage.naturalHeight;\\n              thumbnail.width = tempImage.naturalWidth;\\n              this.thumbnails.push(thumbnail);\\n              resolve();\\n            };\\n            tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n          });\\n        });\\n      });\\n      _defineProperty$1(this, \\\"startMove\\\", event => {\\n        if (!this.loaded) return;\\n        if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n        // Wait until media has a duration\\n        if (!this.player.media.duration) return;\\n        if (event.type === 'touchmove') {\\n          // Calculate seek hover position as approx video seconds\\n          this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n        } else {\\n          var _this$player$config$m, _this$player$config$m2;\\n          // Calculate seek hover position as approx video seconds\\n          const clientRect = this.player.elements.progress.getBoundingClientRect();\\n          const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n          this.seekTime = this.player.media.duration * (percentage / 100);\\n          if (this.seekTime < 0) {\\n            // The mousemove fires for 10+px out to the left\\n            this.seekTime = 0;\\n          }\\n          if (this.seekTime > this.player.media.duration - 1) {\\n            // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n            this.seekTime = this.player.media.duration - 1;\\n          }\\n          this.mousePosX = event.pageX;\\n\\n          // Set time text inside image container\\n          this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n          // Get marker point for time\\n          const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n            time: t\\n          }) => t === Math.round(this.seekTime));\\n\\n          // Append the point label to the tooltip\\n          if (point) {\\n            // this.elements.thumb.time.innerText.concat('\\\\n');\\n            this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n          }\\n        }\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      });\\n      _defineProperty$1(this, \\\"endMove\\\", () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n        // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n        if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n          this.mouseDown = true;\\n\\n          // Wait until media has a duration\\n          if (this.player.media.duration) {\\n            this.toggleScrubbingContainer(true);\\n            this.toggleThumbContainer(false, true);\\n\\n            // Download and show image\\n            this.showImageAtCurrentTime();\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n        this.mouseDown = false;\\n\\n        // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n        if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n          // The video was already seeked/loaded at the chosen time - hide immediately\\n          this.toggleScrubbingContainer(false);\\n        } else {\\n          // The video hasn't seeked yet. Wait for that\\n          once.call(this.player, this.player.media, 'timeupdate', () => {\\n            // Re-check mousedown - we might have already started scrubbing again\\n            if (!this.mouseDown) {\\n              this.toggleScrubbingContainer(false);\\n            }\\n          });\\n        }\\n      });\\n      /**\\n       * Setup hooks for Plyr and window events\\n       */\\n      _defineProperty$1(this, \\\"listeners\\\", () => {\\n        // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n        this.player.on('play', () => {\\n          this.toggleThumbContainer(false, true);\\n        });\\n        this.player.on('seeked', () => {\\n          this.toggleThumbContainer(false);\\n        });\\n        this.player.on('timeupdate', () => {\\n          this.lastTime = this.player.media.currentTime;\\n        });\\n      });\\n      /**\\n       * Create HTML elements for image containers\\n       */\\n      _defineProperty$1(this, \\\"render\\\", () => {\\n        // Create HTML element: plyr__preview-thumbnail-container\\n        this.elements.thumb.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.thumbContainer\\n        });\\n\\n        // Wrapper for the image for styling\\n        this.elements.thumb.imageContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.imageContainer\\n        });\\n        this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n        // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n        const timeContainer = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.timeContainer\\n        });\\n        this.elements.thumb.time = createElement('span', {}, '00:00');\\n        timeContainer.appendChild(this.elements.thumb.time);\\n        this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n        // Inject the whole thumb\\n        if (is.element(this.player.elements.progress)) {\\n          this.player.elements.progress.appendChild(this.elements.thumb.container);\\n        }\\n\\n        // Create HTML element: plyr__preview-scrubbing-container\\n        this.elements.scrubbing.container = createElement('div', {\\n          class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n        });\\n        this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n      });\\n      _defineProperty$1(this, \\\"destroy\\\", () => {\\n        if (this.elements.thumb.container) {\\n          this.elements.thumb.container.remove();\\n        }\\n        if (this.elements.scrubbing.container) {\\n          this.elements.scrubbing.container.remove();\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n        if (this.mouseDown) {\\n          this.setScrubbingContainerSize();\\n        } else {\\n          this.setThumbContainerSizeAndPos();\\n        }\\n\\n        // Find the desired thumbnail index\\n        // TODO: Handle a video longer than the thumbs where thumbNum is null\\n        const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n        const hasThumb = thumbNum >= 0;\\n        let qualityIndex = 0;\\n\\n        // Show the thumb container if we're not scrubbing\\n        if (!this.mouseDown) {\\n          this.toggleThumbContainer(hasThumb);\\n        }\\n\\n        // No matching thumb found\\n        if (!hasThumb) {\\n          return;\\n        }\\n\\n        // Check to see if we've already downloaded higher quality versions of this image\\n        this.thumbnails.forEach((thumbnail, index) => {\\n          if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n            qualityIndex = index;\\n          }\\n        });\\n\\n        // Only proceed if either thumb num or thumbfilename has changed\\n        if (thumbNum !== this.showingThumb) {\\n          this.showingThumb = thumbNum;\\n          this.loadImage(qualityIndex);\\n        }\\n      });\\n      // Show the image that's currently specified in this.showingThumb\\n      _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n        const thumbNum = this.showingThumb;\\n        const thumbnail = this.thumbnails[qualityIndex];\\n        const {\\n          urlPrefix\\n        } = thumbnail;\\n        const frame = thumbnail.frames[thumbNum];\\n        const thumbFilename = thumbnail.frames[thumbNum].text;\\n        const thumbUrl = urlPrefix + thumbFilename;\\n        if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n          // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n          // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n          if (this.loadingImage && this.usingSprites) {\\n            this.loadingImage.onload = null;\\n          }\\n\\n          // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n          // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n          // images causes a flicker. Putting a new image over the top does not\\n          const previewImage = new Image();\\n          previewImage.src = thumbUrl;\\n          previewImage.dataset.index = thumbNum;\\n          previewImage.dataset.filename = thumbFilename;\\n          this.showingThumbFilename = thumbFilename;\\n          this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n          // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n          previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n          this.loadingImage = previewImage;\\n          this.removeOldImages(previewImage);\\n        } else {\\n          // Update the existing image\\n          this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n          this.currentImageElement.dataset.index = thumbNum;\\n          this.removeOldImages(this.currentImageElement);\\n        }\\n      });\\n      _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n        this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n        this.setImageSizeAndOffset(previewImage, frame);\\n        if (newImage) {\\n          this.currentImageContainer.appendChild(previewImage);\\n          this.currentImageElement = previewImage;\\n          if (!this.loadedImages.includes(thumbFilename)) {\\n            this.loadedImages.push(thumbFilename);\\n          }\\n        }\\n\\n        // Preload images before and after the current one\\n        // Show higher quality of the same frame\\n        // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n        this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n      });\\n      // Remove all preview images that aren't the designated current image\\n      _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n        // Get a list of all images, convert it from a DOM list to an array\\n        Array.from(this.currentImageContainer.children).forEach(image => {\\n          if (image.tagName.toLowerCase() !== 'img') {\\n            return;\\n          }\\n          const removeDelay = this.usingSprites ? 500 : 1000;\\n          if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n            // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n            // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n            // eslint-disable-next-line no-param-reassign\\n            image.dataset.deleting = true;\\n\\n            // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n            const {\\n              currentImageContainer\\n            } = this;\\n            setTimeout(() => {\\n              currentImageContainer.removeChild(image);\\n              this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n            }, removeDelay);\\n          }\\n        });\\n      });\\n      // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n      // This will only preload the lowest quality\\n      _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n        return new Promise(resolve => {\\n          setTimeout(() => {\\n            const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n            if (this.showingThumbFilename === oldThumbFilename) {\\n              // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n              let thumbnailsClone;\\n              if (forward) {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n              } else {\\n                thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n              }\\n              let foundOne = false;\\n              thumbnailsClone.forEach(frame => {\\n                const newThumbFilename = frame.text;\\n                if (newThumbFilename !== oldThumbFilename) {\\n                  // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                  if (!this.loadedImages.includes(newThumbFilename)) {\\n                    foundOne = true;\\n                    this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                    const {\\n                      urlPrefix\\n                    } = this.thumbnails[0];\\n                    const thumbURL = urlPrefix + newThumbFilename;\\n                    const previewImage = new Image();\\n                    previewImage.src = thumbURL;\\n                    previewImage.onload = () => {\\n                      this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                      if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                      // We don't resolve until the thumb is loaded\\n                      resolve();\\n                    };\\n                  }\\n                }\\n              });\\n\\n              // If there are none to preload then we want to resolve immediately\\n              if (!foundOne) {\\n                resolve();\\n              }\\n            }\\n          }, 300);\\n        });\\n      });\\n      // If user has been hovering current image for half a second, look for a higher quality one\\n      _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n        if (currentQualityIndex < this.thumbnails.length - 1) {\\n          // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n          let previewImageHeight = previewImage.naturalHeight;\\n          if (this.usingSprites) {\\n            previewImageHeight = frame.h;\\n          }\\n          if (previewImageHeight < this.thumbContainerHeight) {\\n            // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n            setTimeout(() => {\\n              // Make sure the mouse hasn't already moved on and started hovering at another image\\n              if (this.showingThumbFilename === thumbFilename) {\\n                this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n                this.loadImage(currentQualityIndex + 1);\\n              }\\n            }, 300);\\n          }\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n        this.elements.thumb.container.classList.toggle(className, toggle);\\n        if (!toggle && clearShowing) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n        const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n        this.elements.scrubbing.container.classList.toggle(className, toggle);\\n        if (!toggle) {\\n          this.showingThumb = null;\\n          this.showingThumbFilename = null;\\n        }\\n      });\\n      _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n        if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n          // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n          this.sizeSpecifiedInCSS = true;\\n        }\\n      });\\n      // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n      _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n        const {\\n          imageContainer\\n        } = this.elements.thumb;\\n        if (!this.sizeSpecifiedInCSS) {\\n          const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n          imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n          const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n          imageContainer.style.width = `${thumbWidth}px`;\\n        } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n          const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n          imageContainer.style.height = `${thumbHeight}px`;\\n        }\\n        this.setThumbContainerPos();\\n      });\\n      _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n        const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n        const containerRect = this.player.elements.container.getBoundingClientRect();\\n        const {\\n          container\\n        } = this.elements.thumb;\\n        // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n        const min = containerRect.left - scrubberRect.left + 10;\\n        const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n        // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n        const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n        const clamped = clamp(position, min, max);\\n\\n        // Move the popover position\\n        container.style.left = `${clamped}px`;\\n\\n        // The arrow can follow the cursor\\n        container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n      });\\n      // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n      _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n        const {\\n          width,\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        this.elements.scrubbing.container.style.width = `${width}px`;\\n        this.elements.scrubbing.container.style.height = `${height}px`;\\n      });\\n      // Sprites need to be offset to the correct location\\n      _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n        if (!this.usingSprites) return;\\n\\n        // Find difference between height and preview container height\\n        const multiplier = this.thumbContainerHeight / frame.h;\\n\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.left = `-${frame.x * multiplier}px`;\\n        // eslint-disable-next-line no-param-reassign\\n        previewImage.style.top = `-${frame.y * multiplier}px`;\\n      });\\n      this.player = player;\\n      this.thumbnails = [];\\n      this.loaded = false;\\n      this.lastMouseMoveTime = Date.now();\\n      this.mouseDown = false;\\n      this.loadedImages = [];\\n      this.elements = {\\n        thumb: {},\\n        scrubbing: {}\\n      };\\n      this.load();\\n    }\\n    get enabled() {\\n      return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n    }\\n    get currentImageContainer() {\\n      return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n    }\\n    get usingSprites() {\\n      return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n    }\\n    get thumbAspectRatio() {\\n      if (this.usingSprites) {\\n        return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n      }\\n      return this.thumbnails[0].width / this.thumbnails[0].height;\\n    }\\n    get thumbContainerHeight() {\\n      if (this.mouseDown) {\\n        const {\\n          height\\n        } = fitRatio(this.thumbAspectRatio, {\\n          width: this.player.media.clientWidth,\\n          height: this.player.media.clientHeight\\n        });\\n        return height;\\n      }\\n\\n      // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n      if (this.sizeSpecifiedInCSS) {\\n        return this.elements.thumb.imageContainer.clientHeight;\\n      }\\n      return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n    }\\n    get currentImageElement() {\\n      return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n    }\\n    set currentImageElement(element) {\\n      if (this.mouseDown) {\\n        this.currentScrubbingImageElement = element;\\n      } else {\\n        this.currentThumbnailImageElement = element;\\n      }\\n    }\\n  }\\n\\n  // ==========================================================================\\n  const source = {\\n    // Add elements to HTML5 media (source, tracks, etc)\\n    insertElements(type, attributes) {\\n      if (is.string(attributes)) {\\n        insertElement(type, this.media, {\\n          src: attributes\\n        });\\n      } else if (is.array(attributes)) {\\n        attributes.forEach(attribute => {\\n          insertElement(type, this.media, attribute);\\n        });\\n      }\\n    },\\n    // Update source\\n    // Sources are not checked for support so be careful\\n    change(input) {\\n      if (!getDeep(input, 'sources.length')) {\\n        this.debug.warn('Invalid source format');\\n        return;\\n      }\\n\\n      // Cancel current network requests\\n      html5.cancelRequests.call(this);\\n\\n      // Destroy instance and re-setup\\n      this.destroy.call(this, () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const {\\n          sources,\\n          type\\n        } = input;\\n        const [{\\n          provider = providers.html5,\\n          src\\n        }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : {\\n          src\\n        };\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes)\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      }, true);\\n    }\\n  };\\n\\n  // Private properties\\n  // TODO: Use a WeakMap for private globals\\n  // const globals = new WeakMap();\\n\\n  // Plyr instance\\n  class Plyr {\\n    constructor(target, options) {\\n      /**\\n       * Play the media, or play the advertisement (if they are not blocked)\\n       */\\n      _defineProperty$1(this, \\\"play\\\", () => {\\n        if (!is.function(this.media.play)) {\\n          return null;\\n        }\\n\\n        // Intecept play with ads\\n        if (this.ads && this.ads.enabled) {\\n          this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n        }\\n\\n        // Return the promise (for HTML5)\\n        return this.media.play();\\n      });\\n      /**\\n       * Pause the media\\n       */\\n      _defineProperty$1(this, \\\"pause\\\", () => {\\n        if (!this.playing || !is.function(this.media.pause)) {\\n          return null;\\n        }\\n        return this.media.pause();\\n      });\\n      /**\\n       * Toggle playback based on current status\\n       * @param {Boolean} input\\n       */\\n      _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n        // Toggle based on current state if nothing passed\\n        const toggle = is.boolean(input) ? input : !this.playing;\\n        if (toggle) {\\n          return this.play();\\n        }\\n        return this.pause();\\n      });\\n      /**\\n       * Stop playback\\n       */\\n      _defineProperty$1(this, \\\"stop\\\", () => {\\n        if (this.isHTML5) {\\n          this.pause();\\n          this.restart();\\n        } else if (is.function(this.media.stop)) {\\n          this.media.stop();\\n        }\\n      });\\n      /**\\n       * Restart playback\\n       */\\n      _defineProperty$1(this, \\\"restart\\\", () => {\\n        this.currentTime = 0;\\n      });\\n      /**\\n       * Rewind\\n       * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n        this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Fast forward\\n       * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n       */\\n      _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n        this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n      });\\n      /**\\n       * Increase volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n        const volume = this.media.muted ? 0 : this.volume;\\n        this.volume = volume + (is.number(step) ? step : 0);\\n      });\\n      /**\\n       * Decrease volume\\n       * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n       */\\n      _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n        this.increaseVolume(-step);\\n      });\\n      /**\\n       * Trigger the airplay dialog\\n       * TODO: update player with state, support, enabled\\n       */\\n      _defineProperty$1(this, \\\"airplay\\\", () => {\\n        // Show dialog if supported\\n        if (support.airplay) {\\n          this.media.webkitShowPlaybackTargetPicker();\\n        }\\n      });\\n      /**\\n       * Toggle the player controls\\n       * @param {Boolean} [toggle] - Whether to show the controls\\n       */\\n      _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n        // Don't toggle if missing UI support or if it's audio\\n        if (this.supported.ui && !this.isAudio) {\\n          // Get state before change\\n          const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n          // Negate the argument if not undefined since adding the class to hides the controls\\n          const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n          // Apply and get updated state\\n          const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n          // Close menu\\n          if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n            controls.toggleMenu.call(this, false);\\n          }\\n\\n          // Trigger event on change\\n          if (hiding !== isHidden) {\\n            const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n            triggerEvent.call(this, this.media, eventName);\\n          }\\n          return !hiding;\\n        }\\n        return false;\\n      });\\n      /**\\n       * Add event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n        on.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Add event listeners once\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n        once.call(this, this.elements.container, event, callback);\\n      });\\n      /**\\n       * Remove event listeners\\n       * @param {String} event - Event type\\n       * @param {Function} callback - Callback for when event occurs\\n       */\\n      _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n        off(this.elements.container, event, callback);\\n      });\\n      /**\\n       * Destroy an instance\\n       * Event listeners are removed when elements are removed\\n       * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n       * @param {Function} callback - Callback for when destroy is complete\\n       * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n       */\\n      _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n        if (!this.ready) {\\n          return;\\n        }\\n        const done = () => {\\n          // Reset overflow (incase destroyed while in fullscreen)\\n          document.body.style.overflow = '';\\n\\n          // GC for embed\\n          this.embed = null;\\n\\n          // If it's a soft destroy, make minimal changes\\n          if (soft) {\\n            if (Object.keys(this.elements).length) {\\n              // Remove elements\\n              removeElement(this.elements.buttons.play);\\n              removeElement(this.elements.captions);\\n              removeElement(this.elements.controls);\\n              removeElement(this.elements.wrapper);\\n\\n              // Clear for GC\\n              this.elements.buttons.play = null;\\n              this.elements.captions = null;\\n              this.elements.controls = null;\\n              this.elements.wrapper = null;\\n            }\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback();\\n            }\\n          } else {\\n            // Unbind listeners\\n            unbindListeners.call(this);\\n\\n            // Cancel current network requests\\n            html5.cancelRequests.call(this);\\n\\n            // Replace the container with the original element provided\\n            replaceElement(this.elements.original, this.elements.container);\\n\\n            // Event\\n            triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n            // Callback\\n            if (is.function(callback)) {\\n              callback.call(this.elements.original);\\n            }\\n\\n            // Reset state\\n            this.ready = false;\\n\\n            // Clear for garbage collection\\n            setTimeout(() => {\\n              this.elements = null;\\n              this.media = null;\\n            }, 200);\\n          }\\n        };\\n\\n        // Stop playback\\n        this.stop();\\n\\n        // Clear timeouts\\n        clearTimeout(this.timers.loading);\\n        clearTimeout(this.timers.controls);\\n        clearTimeout(this.timers.resized);\\n\\n        // Provider specific stuff\\n        if (this.isHTML5) {\\n          // Restore native video controls\\n          ui.toggleNativeControls.call(this, true);\\n\\n          // Clean up\\n          done();\\n        } else if (this.isYouTube) {\\n          // Clear timers\\n          clearInterval(this.timers.buffering);\\n          clearInterval(this.timers.playing);\\n\\n          // Destroy YouTube API\\n          if (this.embed !== null && is.function(this.embed.destroy)) {\\n            this.embed.destroy();\\n          }\\n\\n          // Clean up\\n          done();\\n        } else if (this.isVimeo) {\\n          // Destroy Vimeo API\\n          // then clean up (wait, to prevent postmessage errors)\\n          if (this.embed !== null) {\\n            this.embed.unload().then(done);\\n          }\\n\\n          // Vimeo does not always return\\n          setTimeout(done, 200);\\n        }\\n      });\\n      /**\\n       * Check for support for a mime type (HTML5 only)\\n       * @param {String} type - Mime type\\n       */\\n      _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n      this.timers = {};\\n\\n      // State\\n      this.ready = false;\\n      this.loading = false;\\n      this.failed = false;\\n\\n      // Touch device\\n      this.touch = support.touch;\\n\\n      // Set the media element\\n      this.media = target;\\n\\n      // String selector passed\\n      if (is.string(this.media)) {\\n        this.media = document.querySelectorAll(this.media);\\n      }\\n\\n      // jQuery, NodeList or Array passed, use first element\\n      if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n        // eslint-disable-next-line\\n        this.media = this.media[0];\\n      }\\n\\n      // Set config\\n      this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })());\\n\\n      // Elements cache\\n      this.elements = {\\n        container: null,\\n        fullscreen: null,\\n        captions: null,\\n        buttons: {},\\n        display: {},\\n        progress: {},\\n        inputs: {},\\n        settings: {\\n          popup: null,\\n          menu: null,\\n          panels: {},\\n          buttons: {}\\n        }\\n      };\\n\\n      // Captions\\n      this.captions = {\\n        active: null,\\n        currentTrack: -1,\\n        meta: new WeakMap()\\n      };\\n\\n      // Fullscreen\\n      this.fullscreen = {\\n        active: false\\n      };\\n\\n      // Options\\n      this.options = {\\n        speed: [],\\n        quality: []\\n      };\\n\\n      // Debugging\\n      // TODO: move to globals\\n      this.debug = new Console(this.config.debug);\\n\\n      // Log config options and support\\n      this.debug.log('Config', this.config);\\n      this.debug.log('Support', support);\\n\\n      // We need an element to setup\\n      if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n        this.debug.error('Setup failed: no suitable element passed');\\n        return;\\n      }\\n\\n      // Bail if the element is initialized\\n      if (this.media.plyr) {\\n        this.debug.warn('Target already setup');\\n        return;\\n      }\\n\\n      // Bail if not enabled\\n      if (!this.config.enabled) {\\n        this.debug.error('Setup failed: disabled by config');\\n        return;\\n      }\\n\\n      // Bail if disabled or no basic support\\n      // You may want to disable certain UAs etc\\n      if (!support.check().api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n\\n      // Cache original element state for .destroy()\\n      const clone = this.media.cloneNode(true);\\n      clone.autoplay = false;\\n      this.elements.original = clone;\\n\\n      // Set media type based on tag or data attribute\\n      // Supported: video, audio, vimeo, youtube\\n      const _type = this.media.tagName.toLowerCase();\\n      // Embed properties\\n      let iframe = null;\\n      let url = null;\\n\\n      // Different setup based on type\\n      switch (_type) {\\n        case 'div':\\n          // Find the frame\\n          iframe = this.media.querySelector('iframe');\\n\\n          // <iframe> type\\n          if (is.element(iframe)) {\\n            // Detect provider\\n            url = parseUrl(iframe.getAttribute('src'));\\n            this.provider = getProviderByUrl(url.toString());\\n\\n            // Rework elements\\n            this.elements.container = this.media;\\n            this.media = iframe;\\n\\n            // Reset classname\\n            this.elements.container.className = '';\\n\\n            // Get attributes from URL and set config\\n            if (url.search.length) {\\n              const truthy = ['1', 'true'];\\n              if (truthy.includes(url.searchParams.get('autoplay'))) {\\n                this.config.autoplay = true;\\n              }\\n              if (truthy.includes(url.searchParams.get('loop'))) {\\n                this.config.loop.active = true;\\n              }\\n\\n              // TODO: replace fullscreen.iosNative with this playsinline config option\\n              // YouTube requires the playsinline in the URL\\n              if (this.isYouTube) {\\n                this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n                this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n              } else {\\n                this.config.playsinline = true;\\n              }\\n            }\\n          } else {\\n            // <div> with attributes\\n            this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n            // Remove attribute\\n            this.media.removeAttribute(this.config.attributes.embed.provider);\\n          }\\n\\n          // Unsupported or missing provider\\n          if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n            this.debug.error('Setup failed: Invalid provider');\\n            return;\\n          }\\n\\n          // Audio will come later for external providers\\n          this.type = types.video;\\n          break;\\n        case 'video':\\n        case 'audio':\\n          this.type = _type;\\n          this.provider = providers.html5;\\n\\n          // Get config from attributes\\n          if (this.media.hasAttribute('crossorigin')) {\\n            this.config.crossorigin = true;\\n          }\\n          if (this.media.hasAttribute('autoplay')) {\\n            this.config.autoplay = true;\\n          }\\n          if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n            this.config.playsinline = true;\\n          }\\n          if (this.media.hasAttribute('muted')) {\\n            this.config.muted = true;\\n          }\\n          if (this.media.hasAttribute('loop')) {\\n            this.config.loop.active = true;\\n          }\\n          break;\\n        default:\\n          this.debug.error('Setup failed: unsupported type');\\n          return;\\n      }\\n\\n      // Check for support again but with type\\n      this.supported = support.check(this.type, this.provider);\\n\\n      // If no support for even API, bail\\n      if (!this.supported.api) {\\n        this.debug.error('Setup failed: no support');\\n        return;\\n      }\\n      this.eventListeners = [];\\n\\n      // Create listeners\\n      this.listeners = new Listeners(this);\\n\\n      // Setup local storage for user settings\\n      this.storage = new Storage(this);\\n\\n      // Store reference\\n      this.media.plyr = this;\\n\\n      // Wrap media\\n      if (!is.element(this.elements.container)) {\\n        this.elements.container = createElement('div');\\n        wrap(this.media, this.elements.container);\\n      }\\n\\n      // Migrate custom properties from media to container (so they work 😉)\\n      ui.migrateStyles.call(this);\\n\\n      // Add style hook\\n      ui.addStyleHook.call(this);\\n\\n      // Setup media\\n      media.setup.call(this);\\n\\n      // Listen for events if debugging\\n      if (this.config.debug) {\\n        on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n          this.debug.log(`event: ${event.type}`);\\n        });\\n      }\\n\\n      // Setup fullscreen\\n      this.fullscreen = new Fullscreen(this);\\n\\n      // Setup interface\\n      // If embed but not fully supported, build interface now to avoid flash of controls\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        ui.build.call(this);\\n      }\\n\\n      // Container listeners\\n      this.listeners.container();\\n\\n      // Global listeners\\n      this.listeners.global();\\n\\n      // Setup ads if provided\\n      if (this.config.ads.enabled) {\\n        this.ads = new Ads(this);\\n      }\\n\\n      // Autoplay if required\\n      if (this.isHTML5 && this.config.autoplay) {\\n        this.once('canplay', () => silencePromise(this.play()));\\n      }\\n\\n      // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n      this.lastSeekTime = 0;\\n\\n      // Setup preview thumbnails if enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n\\n    // ---------------------------------------\\n    // API\\n    // ---------------------------------------\\n\\n    /**\\n     * Types and provider helpers\\n     */\\n    get isHTML5() {\\n      return this.provider === providers.html5;\\n    }\\n    get isEmbed() {\\n      return this.isYouTube || this.isVimeo;\\n    }\\n    get isYouTube() {\\n      return this.provider === providers.youtube;\\n    }\\n    get isVimeo() {\\n      return this.provider === providers.vimeo;\\n    }\\n    get isVideo() {\\n      return this.type === types.video;\\n    }\\n    get isAudio() {\\n      return this.type === types.audio;\\n    }\\n    /**\\n     * Get playing state\\n     */\\n    get playing() {\\n      return Boolean(this.ready && !this.paused && !this.ended);\\n    }\\n\\n    /**\\n     * Get paused state\\n     */\\n    get paused() {\\n      return Boolean(this.media.paused);\\n    }\\n\\n    /**\\n     * Get stopped state\\n     */\\n    get stopped() {\\n      return Boolean(this.paused && this.currentTime === 0);\\n    }\\n\\n    /**\\n     * Get ended state\\n     */\\n    get ended() {\\n      return Boolean(this.media.ended);\\n    }\\n    /**\\n     * Seek to a time\\n     * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n     */\\n    set currentTime(input) {\\n      // Bail if media duration isn't available yet\\n      if (!this.duration) {\\n        return;\\n      }\\n\\n      // Validate input\\n      const inputIsValid = is.number(input) && input > 0;\\n\\n      // Set\\n      this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n      // Logging\\n      this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n    }\\n\\n    /**\\n     * Get current time\\n     */\\n    get currentTime() {\\n      return Number(this.media.currentTime);\\n    }\\n\\n    /**\\n     * Get buffered\\n     */\\n    get buffered() {\\n      const {\\n        buffered\\n      } = this.media;\\n\\n      // YouTube / Vimeo return a float between 0-1\\n      if (is.number(buffered)) {\\n        return buffered;\\n      }\\n\\n      // HTML5\\n      // TODO: Handle buffered chunks of the media\\n      // (i.e. seek to another section buffers only that section)\\n      if (buffered && buffered.length && this.duration > 0) {\\n        return buffered.end(0) / this.duration;\\n      }\\n      return 0;\\n    }\\n\\n    /**\\n     * Get seeking status\\n     */\\n    get seeking() {\\n      return Boolean(this.media.seeking);\\n    }\\n\\n    /**\\n     * Get the duration of the current media\\n     */\\n    get duration() {\\n      // Faux duration set via config\\n      const fauxDuration = parseFloat(this.config.duration);\\n      // Media duration can be NaN or Infinity before the media has loaded\\n      const realDuration = (this.media || {}).duration;\\n      const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n      // If config duration is funky, use regular duration\\n      return fauxDuration || duration;\\n    }\\n\\n    /**\\n     * Set the player volume\\n     * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n     */\\n    set volume(value) {\\n      let volume = value;\\n      const max = 1;\\n      const min = 0;\\n      if (is.string(volume)) {\\n        volume = Number(volume);\\n      }\\n\\n      // Load volume from storage if no value specified\\n      if (!is.number(volume)) {\\n        volume = this.storage.get('volume');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.number(volume)) {\\n        ({\\n          volume\\n        } = this.config);\\n      }\\n\\n      // Maximum is volumeMax\\n      if (volume > max) {\\n        volume = max;\\n      }\\n      // Minimum is volumeMin\\n      if (volume < min) {\\n        volume = min;\\n      }\\n\\n      // Update config\\n      this.config.volume = volume;\\n\\n      // Set the player volume\\n      this.media.volume = volume;\\n\\n      // If muted, and we're increasing volume manually, reset muted state\\n      if (!is.empty(value) && this.muted && volume > 0) {\\n        this.muted = false;\\n      }\\n    }\\n\\n    /**\\n     * Get the current player volume\\n     */\\n    get volume() {\\n      return Number(this.media.volume);\\n    }\\n    /**\\n     * Set muted state\\n     * @param {Boolean} mute\\n     */\\n    set muted(mute) {\\n      let toggle = mute;\\n\\n      // Load muted state from storage\\n      if (!is.boolean(toggle)) {\\n        toggle = this.storage.get('muted');\\n      }\\n\\n      // Use config if all else fails\\n      if (!is.boolean(toggle)) {\\n        toggle = this.config.muted;\\n      }\\n\\n      // Update config\\n      this.config.muted = toggle;\\n\\n      // Set mute on the player\\n      this.media.muted = toggle;\\n    }\\n\\n    /**\\n     * Get current muted state\\n     */\\n    get muted() {\\n      return Boolean(this.media.muted);\\n    }\\n\\n    /**\\n     * Check if the media has audio\\n     */\\n    get hasAudio() {\\n      // Assume yes for all non HTML5 (as we can't tell...)\\n      if (!this.isHTML5) {\\n        return true;\\n      }\\n      if (this.isAudio) {\\n        return true;\\n      }\\n\\n      // Get audio tracks\\n      return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n    }\\n\\n    /**\\n     * Set playback speed\\n     * @param {Number} input - the speed of playback (0.5-2.0)\\n     */\\n    set speed(input) {\\n      let speed = null;\\n      if (is.number(input)) {\\n        speed = input;\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.storage.get('speed');\\n      }\\n      if (!is.number(speed)) {\\n        speed = this.config.speed.selected;\\n      }\\n\\n      // Clamp to min/max\\n      const {\\n        minimumSpeed: min,\\n        maximumSpeed: max\\n      } = this;\\n      speed = clamp(speed, min, max);\\n\\n      // Update config\\n      this.config.speed.selected = speed;\\n\\n      // Set media speed\\n      setTimeout(() => {\\n        if (this.media) {\\n          this.media.playbackRate = speed;\\n        }\\n      }, 0);\\n    }\\n\\n    /**\\n     * Get current playback speed\\n     */\\n    get speed() {\\n      return Number(this.media.playbackRate);\\n    }\\n\\n    /**\\n     * Get the minimum allowed speed\\n     */\\n    get minimumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.min(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 0.5;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 0.0625;\\n    }\\n\\n    /**\\n     * Get the maximum allowed speed\\n     */\\n    get maximumSpeed() {\\n      if (this.isYouTube) {\\n        // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n        return Math.max(...this.options.speed);\\n      }\\n      if (this.isVimeo) {\\n        // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n        return 2;\\n      }\\n\\n      // https://stackoverflow.com/a/32320020/1191319\\n      return 16;\\n    }\\n\\n    /**\\n     * Set playback quality\\n     * Currently HTML5 & YouTube only\\n     * @param {Number} input - Quality level\\n     */\\n    set quality(input) {\\n      const config = this.config.quality;\\n      const options = this.options.quality;\\n      if (!options.length) {\\n        return;\\n      }\\n      let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n      let updateStorage = true;\\n      if (!options.includes(quality)) {\\n        const value = closest(options, quality);\\n        this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n        quality = value;\\n\\n        // Don't update storage if quality is not supported\\n        updateStorage = false;\\n      }\\n\\n      // Update config\\n      config.selected = quality;\\n\\n      // Set quality\\n      this.media.quality = quality;\\n\\n      // Save to storage\\n      if (updateStorage) {\\n        this.storage.set({\\n          quality\\n        });\\n      }\\n    }\\n\\n    /**\\n     * Get current quality level\\n     */\\n    get quality() {\\n      return this.media.quality;\\n    }\\n\\n    /**\\n     * Toggle loop\\n     * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n     * @param {Boolean} input - Whether to loop or not\\n     */\\n    set loop(input) {\\n      const toggle = is.boolean(input) ? input : this.config.loop.active;\\n      this.config.loop.active = toggle;\\n      this.media.loop = toggle;\\n\\n      // Set default to be a true toggle\\n      /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n           switch (type) {\\n              case 'start':\\n                  if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                      this.config.loop.end = null;\\n                  }\\n                  this.config.loop.start = this.currentTime;\\n                  // this.config.loop.indicator.start = this.elements.display.played.value;\\n                  break;\\n               case 'end':\\n                  if (this.config.loop.start >= this.currentTime) {\\n                      return this;\\n                  }\\n                  this.config.loop.end = this.currentTime;\\n                  // this.config.loop.indicator.end = this.elements.display.played.value;\\n                  break;\\n               case 'all':\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = this.duration - 2;\\n                  this.config.loop.indicator.start = 0;\\n                  this.config.loop.indicator.end = 100;\\n                  break;\\n               case 'toggle':\\n                  if (this.config.loop.active) {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = null;\\n                  } else {\\n                      this.config.loop.start = 0;\\n                      this.config.loop.end = this.duration - 2;\\n                  }\\n                  break;\\n               default:\\n                  this.config.loop.start = 0;\\n                  this.config.loop.end = null;\\n                  break;\\n          } */\\n    }\\n\\n    /**\\n     * Get current loop state\\n     */\\n    get loop() {\\n      return Boolean(this.media.loop);\\n    }\\n\\n    /**\\n     * Set new media source\\n     * @param {Object} input - The new source object (see docs)\\n     */\\n    set source(input) {\\n      source.change.call(this, input);\\n    }\\n\\n    /**\\n     * Get current source\\n     */\\n    get source() {\\n      return this.media.currentSrc;\\n    }\\n\\n    /**\\n     * Get a download URL (either source or custom)\\n     */\\n    get download() {\\n      const {\\n        download\\n      } = this.config.urls;\\n      return is.url(download) ? download : this.source;\\n    }\\n\\n    /**\\n     * Set the download URL\\n     */\\n    set download(input) {\\n      if (!is.url(input)) {\\n        return;\\n      }\\n      this.config.urls.download = input;\\n      controls.setDownloadUrl.call(this);\\n    }\\n\\n    /**\\n     * Set the poster image for a video\\n     * @param {String} input - the URL for the new poster image\\n     */\\n    set poster(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Poster can only be set for video');\\n        return;\\n      }\\n      ui.setPoster.call(this, input, false).catch(() => {});\\n    }\\n\\n    /**\\n     * Get the current poster image\\n     */\\n    get poster() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n    }\\n\\n    /**\\n     * Get the current aspect ratio in use\\n     */\\n    get ratio() {\\n      if (!this.isVideo) {\\n        return null;\\n      }\\n      const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n      return is.array(ratio) ? ratio.join(':') : ratio;\\n    }\\n\\n    /**\\n     * Set video aspect ratio\\n     */\\n    set ratio(input) {\\n      if (!this.isVideo) {\\n        this.debug.warn('Aspect ratio can only be set for video');\\n        return;\\n      }\\n      if (!is.string(input) || !validateAspectRatio(input)) {\\n        this.debug.error(`Invalid aspect ratio specified (${input})`);\\n        return;\\n      }\\n      this.config.ratio = reduceAspectRatio(input);\\n      setAspectRatio.call(this);\\n    }\\n\\n    /**\\n     * Set the autoplay state\\n     * @param {Boolean} input - Whether to autoplay or not\\n     */\\n    set autoplay(input) {\\n      this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n    }\\n\\n    /**\\n     * Get the current autoplay state\\n     */\\n    get autoplay() {\\n      return Boolean(this.config.autoplay);\\n    }\\n\\n    /**\\n     * Toggle captions\\n     * @param {Boolean} input - Whether to enable captions\\n     */\\n    toggleCaptions(input) {\\n      captions.toggle.call(this, input, false);\\n    }\\n\\n    /**\\n     * Set the caption track by index\\n     * @param {Number} input - Caption index\\n     */\\n    set currentTrack(input) {\\n      captions.set.call(this, input, false);\\n      captions.setup.call(this);\\n    }\\n\\n    /**\\n     * Get the current caption track index (-1 if disabled)\\n     */\\n    get currentTrack() {\\n      const {\\n        toggled,\\n        currentTrack\\n      } = this.captions;\\n      return toggled ? currentTrack : -1;\\n    }\\n\\n    /**\\n     * Set the wanted language for captions\\n     * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n     * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n     */\\n    set language(input) {\\n      captions.setLanguage.call(this, input, false);\\n    }\\n\\n    /**\\n     * Get the current track's language\\n     */\\n    get language() {\\n      return (captions.getCurrentTrack.call(this) || {}).language;\\n    }\\n\\n    /**\\n     * Toggle picture-in-picture playback on WebKit/MacOS\\n     * TODO: update player with state, support, enabled\\n     * TODO: detect outside changes\\n     */\\n    set pip(input) {\\n      // Bail if no support\\n      if (!support.pip) {\\n        return;\\n      }\\n\\n      // Toggle based on current state if not passed\\n      const toggle = is.boolean(input) ? input : !this.pip;\\n\\n      // Toggle based on current state\\n      // Safari\\n      if (is.function(this.media.webkitSetPresentationMode)) {\\n        this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n      }\\n\\n      // Chrome\\n      if (is.function(this.media.requestPictureInPicture)) {\\n        if (!this.pip && toggle) {\\n          this.media.requestPictureInPicture();\\n        } else if (this.pip && !toggle) {\\n          document.exitPictureInPicture();\\n        }\\n      }\\n    }\\n\\n    /**\\n     * Get the current picture-in-picture state\\n     */\\n    get pip() {\\n      if (!support.pip) {\\n        return null;\\n      }\\n\\n      // Safari\\n      if (!is.empty(this.media.webkitPresentationMode)) {\\n        return this.media.webkitPresentationMode === pip.active;\\n      }\\n\\n      // Chrome\\n      return this.media === document.pictureInPictureElement;\\n    }\\n\\n    /**\\n     * Sets the preview thumbnails for the current source\\n     */\\n    setPreviewThumbnails(thumbnailSource) {\\n      if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n        this.previewThumbnails.destroy();\\n        this.previewThumbnails = null;\\n      }\\n      Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n      // Create new instance if it is still enabled\\n      if (this.config.previewThumbnails.enabled) {\\n        this.previewThumbnails = new PreviewThumbnails(this);\\n      }\\n    }\\n    /**\\n     * Check for support\\n     * @param {String} type - Player type (audio/video)\\n     * @param {String} provider - Provider (html5/youtube/vimeo)\\n     */\\n    static supported(type, provider) {\\n      return support.check(type, provider);\\n    }\\n\\n    /**\\n     * Load an SVG sprite into the page\\n     * @param {String} url - URL for the SVG sprite\\n     * @param {String} [id] - Unique ID\\n     */\\n    static loadSprite(url, id) {\\n      return loadSprite(url, id);\\n    }\\n\\n    /**\\n     * Setup multiple instances\\n     * @param {*} selector\\n     * @param {Object} options\\n     */\\n    static setup(selector, options = {}) {\\n      let targets = null;\\n      if (is.string(selector)) {\\n        targets = Array.from(document.querySelectorAll(selector));\\n      } else if (is.nodeList(selector)) {\\n        targets = Array.from(selector);\\n      } else if (is.array(selector)) {\\n        targets = selector.filter(is.element);\\n      }\\n      if (is.empty(targets)) {\\n        return null;\\n      }\\n      return targets.map(t => new Plyr(t, options));\\n    }\\n  }\\n  Plyr.defaults = cloneDeep(defaults);\\n\\n  // ==========================================================================\\n\\n  return Plyr;\\n\\n}));\\n//# sourceMappingURL=plyr.polyfilled.js.map\\n\",\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function() {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n\\n  try {\\n    var ce = new window.CustomEvent('test', { cancelable: true });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function(event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(\\n        event,\\n        params.bubbles,\\n        params.cancelable,\\n        params.detail\\n      );\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function() {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function() {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"(function(global) {\\r\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\r\\n\\r\\n  var checkIfIteratorIsSupported = function() {\\r\\n    try {\\r\\n      return !!Symbol.iterator;\\r\\n    } catch (error) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var iteratorSupported = checkIfIteratorIsSupported();\\r\\n\\r\\n  var createIterator = function(items) {\\r\\n    var iterator = {\\r\\n      next: function() {\\r\\n        var value = items.shift();\\r\\n        return { done: value === void 0, value: value };\\r\\n      }\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      iterator[Symbol.iterator] = function() {\\r\\n        return iterator;\\r\\n      };\\r\\n    }\\r\\n\\r\\n    return iterator;\\r\\n  };\\r\\n\\r\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\r\\n  var serializeParam = function(value) {\\r\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\r\\n  };\\r\\n\\r\\n  var deserializeParam = function(value) {\\r\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\r\\n  };\\r\\n\\r\\n  var polyfillURLSearchParams = function() {\\r\\n\\r\\n    var URLSearchParams = function(searchString) {\\r\\n      Object.defineProperty(this, '_entries', { writable: true, value: {} });\\r\\n      var typeofSearchString = typeof searchString;\\r\\n\\r\\n      if (typeofSearchString === 'undefined') {\\r\\n        // do nothing\\r\\n      } else if (typeofSearchString === 'string') {\\r\\n        if (searchString !== '') {\\r\\n          this._fromString(searchString);\\r\\n        }\\r\\n      } else if (searchString instanceof URLSearchParams) {\\r\\n        var _this = this;\\r\\n        searchString.forEach(function(value, name) {\\r\\n          _this.append(name, value);\\r\\n        });\\r\\n      } else if ((searchString !== null) && (typeofSearchString === 'object')) {\\r\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\r\\n          for (var i = 0; i < searchString.length; i++) {\\r\\n            var entry = searchString[i];\\r\\n            if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) {\\r\\n              this.append(entry[0], entry[1]);\\r\\n            } else {\\r\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\r\\n            }\\r\\n          }\\r\\n        } else {\\r\\n          for (var key in searchString) {\\r\\n            if (searchString.hasOwnProperty(key)) {\\r\\n              this.append(key, searchString[key]);\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      } else {\\r\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\r\\n      }\\r\\n    };\\r\\n\\r\\n    var proto = URLSearchParams.prototype;\\r\\n\\r\\n    proto.append = function(name, value) {\\r\\n      if (name in this._entries) {\\r\\n        this._entries[name].push(String(value));\\r\\n      } else {\\r\\n        this._entries[name] = [String(value)];\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.delete = function(name) {\\r\\n      delete this._entries[name];\\r\\n    };\\r\\n\\r\\n    proto.get = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name][0] : null;\\r\\n    };\\r\\n\\r\\n    proto.getAll = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name].slice(0) : [];\\r\\n    };\\r\\n\\r\\n    proto.has = function(name) {\\r\\n      return (name in this._entries);\\r\\n    };\\r\\n\\r\\n    proto.set = function(name, value) {\\r\\n      this._entries[name] = [String(value)];\\r\\n    };\\r\\n\\r\\n    proto.forEach = function(callback, thisArg) {\\r\\n      var entries;\\r\\n      for (var name in this._entries) {\\r\\n        if (this._entries.hasOwnProperty(name)) {\\r\\n          entries = this._entries[name];\\r\\n          for (var i = 0; i < entries.length; i++) {\\r\\n            callback.call(thisArg, entries[i], name, this);\\r\\n          }\\r\\n        }\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.keys = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push(name);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.values = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value) {\\r\\n        items.push(value);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.entries = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      proto[Symbol.iterator] = proto.entries;\\r\\n    }\\r\\n\\r\\n    proto.toString = function() {\\r\\n      var searchArray = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\r\\n      });\\r\\n      return searchArray.join('&');\\r\\n    };\\r\\n\\r\\n\\r\\n    global.URLSearchParams = URLSearchParams;\\r\\n  };\\r\\n\\r\\n  var checkIfURLSearchParamsSupported = function() {\\r\\n    try {\\r\\n      var URLSearchParams = global.URLSearchParams;\\r\\n\\r\\n      return (\\r\\n        (new URLSearchParams('?a=1').toString() === 'a=1') &&\\r\\n        (typeof URLSearchParams.prototype.set === 'function') &&\\r\\n        (typeof URLSearchParams.prototype.entries === 'function')\\r\\n      );\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLSearchParamsSupported()) {\\r\\n    polyfillURLSearchParams();\\r\\n  }\\r\\n\\r\\n  var proto = global.URLSearchParams.prototype;\\r\\n\\r\\n  if (typeof proto.sort !== 'function') {\\r\\n    proto.sort = function() {\\r\\n      var _this = this;\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n        if (!_this._entries) {\\r\\n          _this.delete(name);\\r\\n        }\\r\\n      });\\r\\n      items.sort(function(a, b) {\\r\\n        if (a[0] < b[0]) {\\r\\n          return -1;\\r\\n        } else if (a[0] > b[0]) {\\r\\n          return +1;\\r\\n        } else {\\r\\n          return 0;\\r\\n        }\\r\\n      });\\r\\n      if (_this._entries) { // force reset because IE keeps keys index\\r\\n        _this._entries = {};\\r\\n      }\\r\\n      for (var i = 0; i < items.length; i++) {\\r\\n        this.append(items[i][0], items[i][1]);\\r\\n      }\\r\\n    };\\r\\n  }\\r\\n\\r\\n  if (typeof proto._fromString !== 'function') {\\r\\n    Object.defineProperty(proto, '_fromString', {\\r\\n      enumerable: false,\\r\\n      configurable: false,\\r\\n      writable: false,\\r\\n      value: function(searchString) {\\r\\n        if (this._entries) {\\r\\n          this._entries = {};\\r\\n        } else {\\r\\n          var keys = [];\\r\\n          this.forEach(function(value, name) {\\r\\n            keys.push(name);\\r\\n          });\\r\\n          for (var i = 0; i < keys.length; i++) {\\r\\n            this.delete(keys[i]);\\r\\n          }\\r\\n        }\\r\\n\\r\\n        searchString = searchString.replace(/^\\\\?/, '');\\r\\n        var attributes = searchString.split('&');\\r\\n        var attribute;\\r\\n        for (var i = 0; i < attributes.length; i++) {\\r\\n          attribute = attributes[i].split('=');\\r\\n          this.append(\\r\\n            deserializeParam(attribute[0]),\\r\\n            (attribute.length > 1) ? deserializeParam(attribute[1]) : ''\\r\\n          );\\r\\n        }\\r\\n      }\\r\\n    });\\r\\n  }\\r\\n\\r\\n  // HTMLAnchorElement\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\\r\\n(function(global) {\\r\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\r\\n\\r\\n  var checkIfURLIsSupported = function() {\\r\\n    try {\\r\\n      var u = new global.URL('b', 'http://a');\\r\\n      u.pathname = 'c d';\\r\\n      return (u.href === 'http://a/c%20d') && u.searchParams;\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var polyfillURL = function() {\\r\\n    var _URL = global.URL;\\r\\n\\r\\n    var URL = function(url, base) {\\r\\n      if (typeof url !== 'string') url = String(url);\\r\\n      if (base && typeof base !== 'string') base = String(base);\\r\\n\\r\\n      // Only create another document if the base is different from current location.\\r\\n      var doc = document, baseElement;\\r\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\r\\n        base = base.toLowerCase();\\r\\n        doc = document.implementation.createHTMLDocument('');\\r\\n        baseElement = doc.createElement('base');\\r\\n        baseElement.href = base;\\r\\n        doc.head.appendChild(baseElement);\\r\\n        try {\\r\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\r\\n        } catch (err) {\\r\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\r\\n        }\\r\\n      }\\r\\n\\r\\n      var anchorElement = doc.createElement('a');\\r\\n      anchorElement.href = url;\\r\\n      if (baseElement) {\\r\\n        doc.body.appendChild(anchorElement);\\r\\n        anchorElement.href = anchorElement.href; // force href to refresh\\r\\n      }\\r\\n\\r\\n      var inputElement = doc.createElement('input');\\r\\n      inputElement.type = 'url';\\r\\n      inputElement.value = url;\\r\\n\\r\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || (!inputElement.checkValidity() && !base)) {\\r\\n        throw new TypeError('Invalid URL');\\r\\n      }\\r\\n\\r\\n      Object.defineProperty(this, '_anchorElement', {\\r\\n        value: anchorElement\\r\\n      });\\r\\n\\r\\n\\r\\n      // create a linked searchParams which reflect its changes on URL\\r\\n      var searchParams = new global.URLSearchParams(this.search);\\r\\n      var enableSearchUpdate = true;\\r\\n      var enableSearchParamsUpdate = true;\\r\\n      var _this = this;\\r\\n      ['append', 'delete', 'set'].forEach(function(methodName) {\\r\\n        var method = searchParams[methodName];\\r\\n        searchParams[methodName] = function() {\\r\\n          method.apply(searchParams, arguments);\\r\\n          if (enableSearchUpdate) {\\r\\n            enableSearchParamsUpdate = false;\\r\\n            _this.search = searchParams.toString();\\r\\n            enableSearchParamsUpdate = true;\\r\\n          }\\r\\n        };\\r\\n      });\\r\\n\\r\\n      Object.defineProperty(this, 'searchParams', {\\r\\n        value: searchParams,\\r\\n        enumerable: true\\r\\n      });\\r\\n\\r\\n      var search = void 0;\\r\\n      Object.defineProperty(this, '_updateSearchParams', {\\r\\n        enumerable: false,\\r\\n        configurable: false,\\r\\n        writable: false,\\r\\n        value: function() {\\r\\n          if (this.search !== search) {\\r\\n            search = this.search;\\r\\n            if (enableSearchParamsUpdate) {\\r\\n              enableSearchUpdate = false;\\r\\n              this.searchParams._fromString(this.search);\\r\\n              enableSearchUpdate = true;\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      });\\r\\n    };\\r\\n\\r\\n    var proto = URL.prototype;\\r\\n\\r\\n    var linkURLWithAnchorAttribute = function(attributeName) {\\r\\n      Object.defineProperty(proto, attributeName, {\\r\\n        get: function() {\\r\\n          return this._anchorElement[attributeName];\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement[attributeName] = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      });\\r\\n    };\\r\\n\\r\\n    ['hash', 'host', 'hostname', 'port', 'protocol']\\r\\n      .forEach(function(attributeName) {\\r\\n        linkURLWithAnchorAttribute(attributeName);\\r\\n      });\\r\\n\\r\\n    Object.defineProperty(proto, 'search', {\\r\\n      get: function() {\\r\\n        return this._anchorElement['search'];\\r\\n      },\\r\\n      set: function(value) {\\r\\n        this._anchorElement['search'] = value;\\r\\n        this._updateSearchParams();\\r\\n      },\\r\\n      enumerable: true\\r\\n    });\\r\\n\\r\\n    Object.defineProperties(proto, {\\r\\n\\r\\n      'toString': {\\r\\n        get: function() {\\r\\n          var _this = this;\\r\\n          return function() {\\r\\n            return _this.href;\\r\\n          };\\r\\n        }\\r\\n      },\\r\\n\\r\\n      'href': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.href = value;\\r\\n          this._updateSearchParams();\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'pathname': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.pathname = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'origin': {\\r\\n        get: function() {\\r\\n          // get expected port from protocol\\r\\n          var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol];\\r\\n          // add port to origin if, expected port is different than actual port\\r\\n          // and it is not empty f.e http://foo:8080\\r\\n          // 8080 != 80 && 8080 != ''\\r\\n          var addPortToOrigin = this._anchorElement.port != expectedPort &&\\r\\n            this._anchorElement.port !== '';\\r\\n\\r\\n          return this._anchorElement.protocol +\\r\\n            '//' +\\r\\n            this._anchorElement.hostname +\\r\\n            (addPortToOrigin ? (':' + this._anchorElement.port) : '');\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'password': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'username': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n    });\\r\\n\\r\\n    URL.createObjectURL = function(blob) {\\r\\n      return _URL.createObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    URL.revokeObjectURL = function(url) {\\r\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    global.URL = URL;\\r\\n\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLIsSupported()) {\\r\\n    polyfillURL();\\r\\n  }\\r\\n\\r\\n  if ((global.location !== void 0) && !('origin' in global.location)) {\\r\\n    var getOrigin = function() {\\r\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');\\r\\n    };\\r\\n\\r\\n    try {\\r\\n      Object.defineProperty(global.location, 'origin', {\\r\\n        get: getOrigin,\\r\\n        enumerable: true\\r\\n      });\\r\\n    } catch (e) {\\r\\n      setInterval(function() {\\r\\n        global.location.origin = getOrigin();\\r\\n      }, 100);\\r\\n    }\\r\\n  }\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: null,\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.min.mjs b/node_modules/plyr/dist/plyr.polyfilled.min.mjs\nindex 14df60e..059217f 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.min.mjs\n+++ b/node_modules/plyr/dist/plyr.polyfilled.min.mjs\n@@ -1 +1 @@\n-!function(){if(\"undefined\"!=typeof window)try{var e=new window.CustomEvent(\"test\",{cancelable:!0});if(e.preventDefault(),!0!==e.defaultPrevented)throw new Error(\"Could not prevent default\")}catch(e){var t=function(e,t){var i,s;return(t=t||{}).bubbles=!!t.bubbles,t.cancelable=!!t.cancelable,(i=document.createEvent(\"CustomEvent\")).initCustomEvent(e,t.bubbles,t.cancelable,t.detail),s=i.preventDefault,i.preventDefault=function(){s.call(this);try{Object.defineProperty(this,\"defaultPrevented\",{get:function(){return!0}})}catch(e){this.defaultPrevented=!0}},i};t.prototype=window.Event.prototype,window.CustomEvent=t}}();var commonjsGlobal=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}function _defineProperty$1(e,t,i){return(t=_toPropertyKey(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function _toPrimitive(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}function _toPropertyKey(e){var t=_toPrimitive(e,\"string\");return\"symbol\"==typeof t?t:String(t)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function _defineProperties(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,i){return t&&_defineProperties(e.prototype,t),i&&_defineProperties(e,i),e}function _defineProperty(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(i),!0).forEach((function(t){_defineProperty(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}!function(e){var t=function(){try{return!!Symbol.iterator}catch(e){return!1}}(),i=function(e){var i={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return t&&(i[Symbol.iterator]=function(){return i}),i},s=function(e){return encodeURIComponent(e).replace(/%20/g,\"+\")},n=function(e){return decodeURIComponent(String(e).replace(/\\+/g,\" \"))};(function(){try{var t=e.URLSearchParams;return\"a=1\"===new t(\"?a=1\").toString()&&\"function\"==typeof t.prototype.set&&\"function\"==typeof t.prototype.entries}catch(e){return!1}})()||function(){var n=function(e){Object.defineProperty(this,\"_entries\",{writable:!0,value:{}});var t=typeof e;if(\"undefined\"===t);else if(\"string\"===t)\"\"!==e&&this._fromString(e);else if(e instanceof n){var i=this;e.forEach((function(e,t){i.append(t,e)}))}else{if(null===e||\"object\"!==t)throw new TypeError(\"Unsupported input's type for URLSearchParams\");if(\"[object Array]\"===Object.prototype.toString.call(e))for(var s=0;s<e.length;s++){var r=e[s];if(\"[object Array]\"!==Object.prototype.toString.call(r)&&2===r.length)throw new TypeError(\"Expected [string, any] as entry at index \"+s+\" of URLSearchParams's input\");this.append(r[0],r[1])}else for(var a in e)e.hasOwnProperty(a)&&this.append(a,e[a])}},r=n.prototype;r.append=function(e,t){e in this._entries?this._entries[e].push(String(t)):this._entries[e]=[String(t)]},r.delete=function(e){delete this._entries[e]},r.get=function(e){return e in this._entries?this._entries[e][0]:null},r.getAll=function(e){return e in this._entries?this._entries[e].slice(0):[]},r.has=function(e){return e in this._entries},r.set=function(e,t){this._entries[e]=[String(t)]},r.forEach=function(e,t){var i;for(var s in this._entries)if(this._entries.hasOwnProperty(s)){i=this._entries[s];for(var n=0;n<i.length;n++)e.call(t,i[n],s,this)}},r.keys=function(){var e=[];return this.forEach((function(t,i){e.push(i)})),i(e)},r.values=function(){var e=[];return this.forEach((function(t){e.push(t)})),i(e)},r.entries=function(){var e=[];return this.forEach((function(t,i){e.push([i,t])})),i(e)},t&&(r[Symbol.iterator]=r.entries),r.toString=function(){var e=[];return this.forEach((function(t,i){e.push(s(i)+\"=\"+s(t))})),e.join(\"&\")},e.URLSearchParams=n}();var r=e.URLSearchParams.prototype;\"function\"!=typeof r.sort&&(r.sort=function(){var e=this,t=[];this.forEach((function(i,s){t.push([s,i]),e._entries||e.delete(s)})),t.sort((function(e,t){return e[0]<t[0]?-1:e[0]>t[0]?1:0})),e._entries&&(e._entries={});for(var i=0;i<t.length;i++)this.append(t[i][0],t[i][1])}),\"function\"!=typeof r._fromString&&Object.defineProperty(r,\"_fromString\",{enumerable:!1,configurable:!1,writable:!1,value:function(e){if(this._entries)this._entries={};else{var t=[];this.forEach((function(e,i){t.push(i)}));for(var i=0;i<t.length;i++)this.delete(t[i])}var s,r=(e=e.replace(/^\\?/,\"\")).split(\"&\");for(i=0;i<r.length;i++)s=r[i].split(\"=\"),this.append(n(s[0]),s.length>1?n(s[1]):\"\")}})}(void 0!==commonjsGlobal?commonjsGlobal:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:commonjsGlobal),function(e){if(function(){try{var t=new e.URL(\"b\",\"http://a\");return t.pathname=\"c d\",\"http://a/c%20d\"===t.href&&t.searchParams}catch(e){return!1}}()||function(){var t=e.URL,i=function(t,i){\"string\"!=typeof t&&(t=String(t)),i&&\"string\"!=typeof i&&(i=String(i));var s,n=document;if(i&&(void 0===e.location||i!==e.location.href)){i=i.toLowerCase(),(s=(n=document.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=i,n.head.appendChild(s);try{if(0!==s.href.indexOf(i))throw new Error(s.href)}catch(e){throw new Error(\"URL unable to set base \"+i+\" due to \"+e)}}var r=n.createElement(\"a\");r.href=t,s&&(n.body.appendChild(r),r.href=r.href);var a=n.createElement(\"input\");if(a.type=\"url\",a.value=t,\":\"===r.protocol||!/:/.test(r.href)||!a.checkValidity()&&!i)throw new TypeError(\"Invalid URL\");Object.defineProperty(this,\"_anchorElement\",{value:r});var o=new e.URLSearchParams(this.search),l=!0,c=!0,u=this;[\"append\",\"delete\",\"set\"].forEach((function(e){var t=o[e];o[e]=function(){t.apply(o,arguments),l&&(c=!1,u.search=o.toString(),c=!0)}})),Object.defineProperty(this,\"searchParams\",{value:o,enumerable:!0});var h=void 0;Object.defineProperty(this,\"_updateSearchParams\",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==h&&(h=this.search,c&&(l=!1,this.searchParams._fromString(this.search),l=!0))}})},s=i.prototype;[\"hash\",\"host\",\"hostname\",\"port\",\"protocol\"].forEach((function(e){!function(e){Object.defineProperty(s,e,{get:function(){return this._anchorElement[e]},set:function(t){this._anchorElement[e]=t},enumerable:!0})}(e)})),Object.defineProperty(s,\"search\",{get:function(){return this._anchorElement.search},set:function(e){this._anchorElement.search=e,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\\?$/,\"\")},set:function(e){this._anchorElement.href=e,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\\/?)/,\"/\")},set:function(e){this._anchorElement.pathname=e},enumerable:!0},origin:{get:function(){var e={\"http:\":80,\"https:\":443,\"ftp:\":21}[this._anchorElement.protocol],t=this._anchorElement.port!=e&&\"\"!==this._anchorElement.port;return this._anchorElement.protocol+\"//\"+this._anchorElement.hostname+(t?\":\"+this._anchorElement.port:\"\")},enumerable:!0},password:{get:function(){return\"\"},set:function(e){},enumerable:!0},username:{get:function(){return\"\"},set:function(e){},enumerable:!0}}),i.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)},i.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)},e.URL=i}(),void 0!==e.location&&!(\"origin\"in e.location)){var t=function(){return e.location.protocol+\"//\"+e.location.hostname+(e.location.port?\":\"+e.location.port:\"\")};try{Object.defineProperty(e.location,\"origin\",{get:t,enumerable:!0})}catch(i){setInterval((function(){e.location.origin=t()}),100)}}}(void 0!==commonjsGlobal?commonjsGlobal:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:commonjsGlobal);var defaults$1={addCSS:!0,thumbWidth:15,watch:!0};function matches$1(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}var getConstructor$1=function(e){return null!=e?e.constructor:null},instanceOf$1=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined$1=function(e){return null==e},isObject$1=function(e){return getConstructor$1(e)===Object},isNumber$1=function(e){return getConstructor$1(e)===Number&&!Number.isNaN(e)},isString$1=function(e){return getConstructor$1(e)===String},isBoolean$1=function(e){return getConstructor$1(e)===Boolean},isFunction$1=function(e){return getConstructor$1(e)===Function},isArray$1=function(e){return Array.isArray(e)},isNodeList$1=function(e){return instanceOf$1(e,NodeList)},isElement$1=function(e){return instanceOf$1(e,Element)},isEvent$1=function(e){return instanceOf$1(e,Event)},isEmpty$1=function(e){return isNullOrUndefined$1(e)||(isString$1(e)||isArray$1(e)||isNodeList$1(e))&&!e.length||isObject$1(e)&&!Object.keys(e).length},is$1={nullOrUndefined:isNullOrUndefined$1,object:isObject$1,number:isNumber$1,string:isString$1,boolean:isBoolean$1,function:isFunction$1,array:isArray$1,nodeList:isNodeList$1,element:isElement$1,event:isEvent$1,empty:isEmpty$1};function getDecimalPlaces(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var i=getDecimalPlaces(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,i){_classCallCheck(this,e),is$1.element(t)?this.element=t:is$1.string(t)&&(this.element=document.querySelector(t)),is$1.element(this.element)&&is$1.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults$1,{},i),this.init())}return _createClass(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!is$1.event(t))return null;var i,s=t.target,n=t.changedTouches[0],r=parseFloat(s.getAttribute(\"min\"))||0,a=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),r+round(i/100*(a-r),o)}},{key:\"set\",value:function(t){e.enabled&&is$1.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(is$1.empty(t)||is$1.string(t)?s=Array.from(document.querySelectorAll(is$1.string(t)?t:'input[type=\"range\"]')):is$1.element(t)?s=[t]:is$1.nodeList(t)?s=Array.from(t):is$1.array(t)&&(s=t.filter(is$1.element)),is$1.empty(s))return null;var n=_objectSpread2({},defaults$1,{},i);if(is$1.string(t)&&n.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){is$1.element(i)&&matches$1(i,t)&&new e(i,n)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const getConstructor=e=>null!=e?e.constructor:null,instanceOf=(e,t)=>Boolean(e&&t&&e instanceof t),isNullOrUndefined=e=>null==e,isObject=e=>getConstructor(e)===Object,isNumber=e=>getConstructor(e)===Number&&!Number.isNaN(e),isString=e=>getConstructor(e)===String,isBoolean=e=>getConstructor(e)===Boolean,isFunction=e=>\"function\"==typeof e,isArray=e=>Array.isArray(e),isWeakMap=e=>instanceOf(e,WeakMap),isNodeList=e=>instanceOf(e,NodeList),isTextNode=e=>getConstructor(e)===Text,isEvent=e=>instanceOf(e,Event),isKeyboardEvent=e=>instanceOf(e,KeyboardEvent),isCue=e=>instanceOf(e,window.TextTrackCue)||instanceOf(e,window.VTTCue),isTrack=e=>instanceOf(e,TextTrack)||!isNullOrUndefined(e)&&isString(e.kind),isPromise=e=>instanceOf(e,Promise)&&isFunction(e.then),isElement=e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,isEmpty=e=>isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length,isUrl=e=>{if(instanceOf(e,window.URL))return!0;if(!isString(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!isEmpty(new URL(t).hostname)}catch(e){return!1}};var is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,weakMap:isWeakMap,nodeList:isNodeList,element:isElement,textNode:isTextNode,event:isEvent,keyboardEvent:isKeyboardEvent,cue:isCue,track:isTrack,promise:isPromise,url:isUrl,empty:isEmpty};const transitionEndEvent=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!is.string(i)&&t[i]})();function repaint(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}const isIE=Boolean(window.document.documentMode),isEdge=/Edge/g.test(navigator.userAgent),isWebKit=\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone=/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS=\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos=/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1;var browser={isIE:isIE,isEdge:isEdge,isWebKit:isWebKit,isIPhone:isIPhone,isIPadOS:isIPadOS,isIos:isIos};function cloneDeep(e){return JSON.parse(JSON.stringify(e))}function getDeep(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function extend(e={},...t){if(!t.length)return e;const i=t.shift();return is.object(i)?(Object.keys(i).forEach((t=>{is.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),extend(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),extend(e,...t)):e}function wrap(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,r=e.nextSibling;s.appendChild(e),r?n.insertBefore(s,r):n.appendChild(s)}))}function setAttributes(e,t){is.element(e)&&!is.empty(t)&&Object.entries(t).filter((([,e])=>!is.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function createElement(e,t,i){const s=document.createElement(e);return is.object(t)&&setAttributes(s,t),is.string(i)&&(s.innerText=i),s}function insertAfter(e,t){is.element(e)&&is.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)}function insertElement(e,t,i,s){is.element(t)&&t.appendChild(createElement(e,i,s))}function removeElement(e){is.nodeList(e)||is.array(e)?Array.from(e).forEach(removeElement):is.element(e)&&is.element(e.parentNode)&&e.parentNode.removeChild(e)}function emptyElement(e){if(!is.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function replaceElement(e,t){return is.element(t)&&is.element(t.parentNode)&&is.element(e)?(t.parentNode.replaceChild(e,t),e):null}function getAttributesFromSelector(e,t){if(!is.string(e)||is.empty(e))return{};const i={},s=extend({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),r=t.replace(/[[\\]]/g,\"\").split(\"=\"),[a]=r,o=r.length>1?r[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":is.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[a]=o}})),extend(s,i)}function toggleHidden(e,t){if(!is.element(e))return;let i=t;is.boolean(i)||(i=!e.hidden),e.hidden=i}function toggleClass(e,t,i){if(is.nodeList(e))return Array.from(e).map((e=>toggleClass(e,t,i)));if(is.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function hasClass(e,t){return is.element(e)&&e.classList.contains(t)}function matches(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function closest$1(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(matches.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}function getElements(e){return this.elements.container.querySelectorAll(e)}function getElement(e){return this.elements.container.querySelector(e)}function setFocus(e=null,t=!1){is.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const defaultCodecs={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},support={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=support[e]||\"html5\"!==t;return{api:i,ui:i&&support.rangeInput}},pip:!(browser.isIPhone||!is.function(createElement(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||createElement(\"video\").disablePictureInPicture)),airplay:is.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(is.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(defaultCodecs).includes(i)&&(i+=`; codecs=\"${defaultCodecs[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==transitionEndEvent,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},supportsPassiveListeners=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function toggleListener(e,t,i,s=!1,n=!0,r=!1){if(!e||!(\"addEventListener\"in e)||is.empty(t)||!is.function(i))return;const a=t.split(\" \");let o=r;supportsPassiveListeners&&(o={passive:n,capture:r}),a.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function on(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!0,s,n)}function off(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!1,s,n)}function once(e,t=\"\",i,s=!0,n=!1){const r=(...a)=>{off(e,t,r,s,n),i.apply(this,a)};toggleListener.call(this,e,t,r,!0,s,n)}function triggerEvent(e,t=\"\",i=!1,s={}){if(!is.element(e)||is.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function unbindListeners(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ready(){return new Promise((e=>this.ready?setTimeout(e,0):on.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function silencePromise(e){is.promise(e)&&e.then(null,(()=>{}))}function dedupe(e){return is.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function closest(e,t){return is.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function supportsCSS(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const standardRatios=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function validateAspectRatio(e){if(!(is.array(e)||is.string(e)&&e.includes(\":\")))return!1;return(is.array(e)?e:e.split(\":\")).map(Number).every(is.number)}function reduceAspectRatio(e){if(!is.array(e)||!e.every(is.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function getAspectRatio(e){const t=e=>validateAspectRatio(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!is.empty(this.embed)&&is.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return reduceAspectRatio(i)}function setAspectRatio(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=getAspectRatio.call(this,e);if(!is.array(i))return{};const[s,n]=reduceAspectRatio(i),r=100/s*n;if(supportsCSS(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${r}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-r)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:r,ratio:i}}function roundAspectRatio(e,t,i=.05){const s=e/t,n=closest(Object.keys(standardRatios),s);return Math.abs(n-s)<=i?standardRatios[n]:[e,t]}function getViewportSize(){return[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)]}const html5={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!is.empty(t)||support.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:html5.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,is.empty(this.config.ratio)||setAspectRatio.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=html5.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&is.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=html5.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:r,readyState:a,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==r||a)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||silencePromise(e.play())})),e.media.load())}triggerEvent.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(removeElement(html5.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function generateId(e){return`${e}-${Math.floor(1e4*Math.random())}`}function format(e,...t){return is.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}function getPercentage(e,t){return 0===e||0===t||Number.isNaN(e)||Number.isNaN(t)?0:(e/t*100).toFixed(2)}const replaceAll=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),toTitleCase=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function toPascalCase(e=\"\"){let t=e.toString();return t=replaceAll(t,\"-\",\" \"),t=replaceAll(t,\"_\",\" \"),t=toTitleCase(t),replaceAll(t,\" \",\"\")}function toCamelCase(e=\"\"){let t=e.toString();return t=toPascalCase(t),t.charAt(0).toLowerCase()+t.slice(1)}function stripHTML(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}function getHTML(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const resources={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},i18n={get(e=\"\",t={}){if(is.empty(e)||is.empty(t))return\"\";let i=getDeep(t.i18n,e);if(is.empty(i))return Object.keys(resources).includes(e)?resources[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=replaceAll(i,e,t)})),i}};class Storage{constructor(e){_defineProperty$1(this,\"get\",(e=>{if(!Storage.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(is.empty(t))return null;const i=JSON.parse(t);return is.string(e)&&e.length?i[e]:i})),_defineProperty$1(this,\"set\",(e=>{if(!Storage.supported||!this.enabled)return;if(!is.object(e))return;let t=this.get();is.empty(t)&&(t={}),extend(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function fetch(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function loadSprite(e,t){if(!is.string(e))return;const i=\"cache\",s=is.string(t);let n=!1;const r=()=>null!==document.getElementById(t),a=(e,t)=>{e.innerHTML=t,s&&r()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!r()){const r=Storage.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),r){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);a(o,t.content)}}fetch(e).then((e=>{if(!is.empty(e)){if(r)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}a(o,e)}})).catch((()=>{}))}}const getHours=e=>Math.trunc(e/60/60%60,10),getMinutes=e=>Math.trunc(e/60%60,10),getSeconds=e=>Math.trunc(e%60,10);function formatTime(e=0,t=!1,i=!1){if(!is.number(e))return formatTime(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=getHours(e);const r=getMinutes(e),a=getSeconds(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(r)}:${s(a)}`}const controls={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||browser.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=getElement.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:getElements.call(this,this.config.selectors.buttons.play),pause:getElement.call(this,this.config.selectors.buttons.pause),restart:getElement.call(this,this.config.selectors.buttons.restart),rewind:getElement.call(this,this.config.selectors.buttons.rewind),fastForward:getElement.call(this,this.config.selectors.buttons.fastForward),mute:getElement.call(this,this.config.selectors.buttons.mute),pip:getElement.call(this,this.config.selectors.buttons.pip),airplay:getElement.call(this,this.config.selectors.buttons.airplay),settings:getElement.call(this,this.config.selectors.buttons.settings),captions:getElement.call(this,this.config.selectors.buttons.captions),fullscreen:getElement.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=getElement.call(this,this.config.selectors.progress),this.elements.inputs={seek:getElement.call(this,this.config.selectors.inputs.seek),volume:getElement.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:getElement.call(this,this.config.selectors.display.buffer),currentTime:getElement.call(this,this.config.selectors.display.currentTime),duration:getElement.call(this,this.config.selectors.display.duration)},is.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=controls.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,r=document.createElementNS(i,\"svg\");setAttributes(r,extend(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const a=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in a&&a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),r.appendChild(a),r},createLabel(e,t={}){const i=i18n.get(e,this.config);return createElement(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(is.empty(e))return null;const t=createElement(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(createElement(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=extend({},t);let s=toCamelCase(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||extend(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:is.empty(n.label)&&(n.label=s),is.empty(n.icon)&&(n.icon=e)}const r=createElement(n.element);return n.toggle?(r.appendChild(controls.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),r.appendChild(controls.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),r.appendChild(controls.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),r.appendChild(controls.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(r.appendChild(controls.createIcon.call(this,n.icon)),r.appendChild(controls.createLabel.call(this,n.label))),extend(i,getAttributesFromSelector(this.config.selectors.buttons[s],i)),setAttributes(r,i),\"play\"===s?(is.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(r)):this.elements.buttons[s]=r,r},createRange(e,t){const i=createElement(\"input\",extend(getAttributesFromSelector(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":i18n.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,controls.updateRangeFill.call(this,i),RangeTouch.setup(i),i},createProgress(e,t){const i=createElement(\"progress\",extend(getAttributesFromSelector(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(createElement(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?i18n.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=getAttributesFromSelector(this.config.selectors.display[e],t),s=createElement(\"div\",extend(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":i18n.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){on.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=matches(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))controls.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,is.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,is.element(t)||(t=e.parentNode.lastElementChild)),setFocus.call(this,t,!0))}}),!1),on.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&controls.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:r=!1}){const a=getAttributesFromSelector(this.config.selectors.inputs[i]),o=createElement(\"button\",extend(a,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${a.class?a.class:\"\"}`.trim(),\"aria-checked\":r,value:e})),l=createElement(\"span\");l.innerHTML=s,is.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>matches(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!is.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}controls.showMenuPanel.call(this,\"home\",is.keyboardEvent(t))}}),i,!1),controls.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!is.number(e))return e;return formatTime(e,getHours(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){is.element(e)&&is.number(t)&&(e.innerText=controls.formatTime(t,i))},updateVolume(){this.supported.ui&&(is.element(this.elements.inputs.volume)&&controls.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),is.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){is.element(e)&&(e.value=t,controls.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!is.event(e))return;let t=0;const i=(e,t)=>{const i=is.number(t)?t:0,s=is.element(e)?e:this.elements.display.buffer;if(is.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];is.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":t=getPercentage(this.currentTime,this.duration),\"timeupdate\"===e.type&&controls.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}},updateRangeFill(e){const t=is.event(e)?e.target:e;if(is.element(t)&&\"range\"===t.getAttribute(\"type\")){if(matches(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=controls.formatTime(this.currentTime),i=controls.formatTime(this.duration),s=i18n.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(matches(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(browser.isWebKit||browser.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!is.element(this.elements.inputs.seek)||!is.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,r=e=>toggleClass(s,n,e);if(this.touch)return void r(!1);let a=0;const o=this.elements.progress.getBoundingClientRect();if(is.event(e))a=100/o.width*(e.pageX-o.left);else{if(!hasClass(s,n))return;a=parseFloat(s.style.left,10)}a<0?a=0:a>100&&(a=100);const l=this.duration/100*a;s.innerText=controls.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${a}%`,is.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&r(\"mouseenter\"===e.type)},timeUpdate(e){const t=!is.element(this.elements.display.duration)&&this.config.invertTime;controls.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||controls.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return toggleHidden(this.elements.display.currentTime,!0),void toggleHidden(this.elements.progress,!0);is.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=is.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&controls.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&controls.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&controls.setMarkers.call(this),controls.updateSeekTooltip.call(this)},toggleMenuButton(e,t){toggleHidden(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,r=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=is.empty(i)?this[e]:i,is.empty(n)&&(n=this.config[e].default),!is.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(is.element(r)||(r=s&&s.querySelector('[role=\"menu\"]')),!is.element(r))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=controls.getLabel.call(this,e,n);const a=r&&r.querySelector(`[value=\"${n}\"]`);is.element(a)&&(a.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?i18n.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(is.number(t)){const e=i18n.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return toTitleCase(t);case\"captions\":return captions.getLabel.call(this);default:return null}},setQualityMenu(e){if(!is.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');is.array(e)&&(this.options.quality=dedupe(e).filter((e=>this.config.quality.options.includes(e))));const s=!is.empty(this.options.quality)&&this.options.quality.length>1;if(controls.toggleMenuButton.call(this,t,s),emptyElement(i),controls.checkMenu.call(this),!s)return;const n=e=>{const t=i18n.get(`qualityBadge.${e}`,this.config);return t.length?controls.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{controls.createMenuItem.call(this,{value:e,list:i,type:t,title:controls.getLabel.call(this,\"quality\",e),badge:n(e)})})),controls.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!is.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=captions.getTracks.call(this),s=Boolean(i.length);if(controls.toggleMenuButton.call(this,e,s),emptyElement(t),controls.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:captions.getLabel.call(this,e),badge:e.language&&controls.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:i18n.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(controls.createMenuItem.bind(this)),controls.updateSetting.call(this,e,t)},setSpeedMenu(){if(!is.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!is.empty(this.options.speed)&&this.options.speed.length>1;controls.toggleMenuButton.call(this,e,i),emptyElement(t),controls.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{controls.createMenuItem.call(this,{value:i,list:t,type:e,title:controls.getLabel.call(this,\"speed\",i)})})),controls.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!is.empty(e)&&Object.values(e).some((e=>!e.hidden));toggleHidden(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;is.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');setFocus.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!is.element(t)||!is.element(i))return;const{hidden:s}=t;let n=s;if(is.boolean(e))n=e;else if(is.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(is.event(e)){const s=is.function(e.composedPath)?e.composedPath()[0]:e.target,r=t.contains(s);if(r||!r&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),toggleHidden(t,!n),toggleClass(this.elements.container,this.config.classNames.menu.open,n),n&&is.keyboardEvent(e)?controls.focusFirstMenuItem.call(this,null,!0):n||s||setFocus.call(this,i,is.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return removeElement(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!is.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(support.transitions&&!support.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=controls.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",off.call(this,s,transitionEndEvent,t))};on.call(this,s,transitionEndEvent,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}toggleHidden(n,!0),toggleHidden(i,!1),controls.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;is.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:r,setQualityMenu:a,setSpeedMenu:o,showMenuPanel:l}=controls;this.elements.controls=null,is.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=createElement(\"div\",getAttributesFromSelector(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return dedupe(is.array(this.config.controls)?this.config.controls:[]).forEach((a=>{if(\"restart\"===a&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===a&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===a&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===a&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===a){const t=createElement(\"div\",{class:`${u.class} plyr__progress__container`}),i=createElement(\"div\",getAttributesFromSelector(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=createElement(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===a&&c.appendChild(r.call(this,\"currentTime\",u)),\"duration\"===a&&c.appendChild(r.call(this,\"duration\",u)),\"mute\"===a||\"volume\"===a){let{volume:t}=this.elements;if(is.element(t)&&c.contains(t)||(t=createElement(\"div\",extend({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===a&&t.appendChild(i.call(this,\"mute\")),\"volume\"===a&&!browser.isIos&&!browser.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",extend(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===a&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===a&&!is.empty(this.config.settings)){const s=createElement(\"div\",extend({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=createElement(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),r=createElement(\"div\"),a=createElement(\"div\",{id:`plyr-settings-${e.id}-home`}),o=createElement(\"div\",{role:\"menu\"});a.appendChild(o),r.appendChild(a),this.elements.settings.panels.home=a,this.config.settings.forEach((i=>{const s=createElement(\"button\",extend(getAttributesFromSelector(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),on.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=createElement(\"span\",null,i18n.get(i,this.config)),a=createElement(\"span\",{class:this.config.classNames.menu.value});a.innerHTML=e[i],n.appendChild(a),s.appendChild(n),o.appendChild(s);const c=createElement(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=createElement(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(createElement(\"span\",{\"aria-hidden\":!0},i18n.get(i,this.config))),u.appendChild(createElement(\"span\",{class:this.config.classNames.hidden},i18n.get(\"menuBack\",this.config))),on.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),on.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(createElement(\"div\",{role:\"menu\"})),r.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(r),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===a&&support.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===a&&support.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===a){const e=extend({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!is.url(t)&&this.isEmbed&&extend(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===a&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&a.call(this,html5.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=controls.getIconUrl.call(this);e.cors&&loadSprite(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;is.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),is.element(this.config.controls)||is.string(this.config.controls)?e=this.config.controls:(e=controls.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:captions.getLabel.call(this)}),i=!1);let s;i&&is.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=replaceAll(i,`{${e}}`,t)})),i})(e)),is.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),is.element(s)||(s=this.elements.container);if(s[is.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),is.element(this.elements.controls)||controls.findElements.call(this),!is.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>hasClass(e,t),set(i=!1){toggleClass(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{is.array(t)||is.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(browser.isEdge&&repaint(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=getElements.call(this,i);Array.from(s).forEach((e=>{toggleClass(e,this.config.classNames.hidden,!1),toggleClass(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let r=null;const a=`${this.config.classNames.tooltip}--visible`,o=e=>toggleClass(r,a,e);i.forEach((e=>{const t=createElement(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";r&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(r.style.left=i,r.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(r=createElement(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(r)),this.elements.markers={points:n,tip:r},this.elements.progress.appendChild(s)}};function parseUrl(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function buildUrlParams(e){const t=new URLSearchParams;return is.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const captions={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!support.textTracks)return void(is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this));if(is.element(this.elements.captions)||(this.elements.captions=createElement(\"div\",getAttributesFromSelector(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),insertAfter(this.elements.captions,this.elements.wrapper)),browser.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=parseUrl(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&fetch(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{removeElement(e)}))}))}const e=dedupe((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let t=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===t&&([t]=e);let i=this.storage.get(\"captions\");if(is.boolean(i)||({active:i}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:i,language:t,languages:e}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";on.call(this,this.media.textTracks,e,captions.update.bind(this))}setTimeout(captions.update.bind(this),0)},update(){const e=captions.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,r=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),on.call(this,e,\"cuechange\",(()=>captions.updateCues.call(this)))})),(r&&this.language!==i||!e.includes(n))&&(captions.setLanguage.call(this,i),captions.toggle.call(this,t&&r)),this.elements&&toggleClass(this.elements.container,this.config.classNames.captions.enabled,!is.empty(e)),is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=is.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=captions.getTracks.call(this),t=captions.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void captions.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),toggleClass(this.elements.container,s,n),this.captions.toggled=n,controls.updateSetting.call(this,\"captions\"),triggerEvent.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=captions.getTracks.call(this);if(-1!==e)if(is.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,controls.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),triggerEvent.call(this,this.media,\"languagechange\")}captions.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&captions.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else captions.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!is.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=captions.getTracks.call(this),n=captions.findTrack.call(this,[i]);captions.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=captions.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let r;return e.every((e=>(r=n.find((t=>t.language===e)),!r))),r||(t?n[0]:void 0)},getCurrentTrack(){return captions.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!is.track(t)&&support.textTracks&&this.captions.toggled&&(t=captions.getCurrentTrack.call(this)),is.track(t)?is.empty(t.label)?is.empty(t.language)?i18n.get(\"enabled\",this.config):e.language.toUpperCase():t.label:i18n.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!is.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!is.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=captions.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(getHTML)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){emptyElement(this.elements.captions);const e=createElement(\"span\",getAttributesFromSelector(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),triggerEvent.call(this,this.media,\"cuechange\")}}},defaults={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:{download:null,vimeo:{sdk:\"https://player.vimeo.com/api/player.js\",iframe:\"https://player.vimeo.com/video/{0}?{1}\",api:\"https://vimeo.com/api/oembed.json?url={0}\"},youtube:{sdk:\"https://www.youtube.com/iframe_api\",api:\"https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}\"},googleIMA:{sdk:\"https://imasdk.googleapis.com/js/sdkloader/ima3.js\"}},listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},pip={active:\"picture-in-picture\",inactive:\"inline\"},providers={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},types={audio:\"audio\",video:\"video\"};function getProviderByUrl(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?providers.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?providers.vimeo:null}const noop=()=>{};class Console{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):noop}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):noop}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):noop}}class Fullscreen{constructor(e){_defineProperty$1(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;is.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;triggerEvent.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),_defineProperty$1(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",toggleClass(this.target,this.player.config.classNames.fullscreen.fallback,e),browser.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=is.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),_defineProperty$1(this,\"trapFocus\",(e=>{if(browser.isIos||browser.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=getElements.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),_defineProperty$1(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":Fullscreen.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");toggleClass(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),_defineProperty$1(this,\"enter\",(()=>{this.supported&&(browser.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!Fullscreen.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?is.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),_defineProperty$1(this,\"exit\",(()=>{if(this.supported)if(browser.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),silencePromise(this.player.play());else if(!Fullscreen.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!is.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),_defineProperty$1(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=Fullscreen.prefix,this.property=Fullscreen.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&closest$1(this.player.elements.container,e.config.fullscreen.container),on.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),on.call(this.player,this.player.elements.container,\"dblclick\",(e=>{is.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),on.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return Fullscreen.nativeSupported&&!this.forceFallback}static get prefix(){if(is.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!is.function(document[`${t}ExitFullscreen`])&&!is.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,Fullscreen.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||Fullscreen.nativeSupported||!browser.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!Fullscreen.nativeSupported||this.forceFallback)return hasClass(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return browser.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function loadImage(e,t=1){return new Promise(((i,s)=>{const n=new Image,r=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:r,onerror:r,src:e})}))}const ui={addStyleHook(){toggleClass(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),toggleClass(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void ui.toggleNativeControls.call(this,!0);is.element(this.elements.controls)||(controls.inject.call(this),this.listeners.controls()),ui.toggleNativeControls.call(this),this.isHTML5&&captions.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,controls.updateVolume.call(this),controls.timeUpdate.call(this),controls.durationUpdate.call(this),ui.checkPlaying.call(this),toggleClass(this.elements.container,this.config.classNames.pip.supported,support.pip&&this.isHTML5&&this.isVideo),toggleClass(this.elements.container,this.config.classNames.airplay.supported,support.airplay&&this.isHTML5),toggleClass(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{triggerEvent.call(this,this.media,\"ready\")}),0),ui.setTitle.call(this),this.poster&&ui.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&controls.durationUpdate.call(this),this.config.mediaMetadata&&controls.setMediaMetadata.call(this)},setTitle(){let e=i18n.get(\"play\",this.config);if(is.string(this.config.title)&&!is.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=getElement.call(this,\"iframe\");if(!is.element(e))return;const t=is.empty(this.config.title)?\"video\":this.config.title,i=i18n.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){toggleClass(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ready.call(this).then((()=>loadImage(e))).catch((t=>{throw e===this.poster&&ui.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),ui.togglePoster.call(this,!0),e))))},checkPlaying(e){toggleClass(this.elements.container,this.config.classNames.playing,this.playing),toggleClass(this.elements.container,this.config.classNames.paused,this.paused),toggleClass(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",i18n.get(this.playing?\"pause\":\"play\",this.config))})),is.event(e)&&\"timeupdate\"===e.type||ui.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{toggleClass(this.elements.container,this.config.classNames.loading,this.loading),ui.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!is.empty(e)&&is.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),is.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Listeners{constructor(e){_defineProperty$1(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,toggleClass(t.container,e.config.classNames.isTouch,!0)})),_defineProperty$1(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&toggleListener.call(t,window,\"keydown keyup\",this.handleKey,e,!1),toggleListener.call(t,document.body,\"click\",this.toggleMenu,e),once.call(t,document.body,\"touchstart\",this.firstTouch)})),_defineProperty$1(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&on.call(e,i.container,\"keydown keyup\",this.handleKey,!1),on.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let r=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(ui.toggleControls.call(e,!0),r=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),r)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,r]=getAspectRatio.call(e),a=supportsCSS(`aspect-ratio: ${n} / ${r}`);if(!s)return void(a?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=getViewportSize(),c=o/l>n/r;a?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/r*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},r=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};on.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&is.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?on:off).call(e,window,\"resize\",r)}))})),_defineProperty$1(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(on.call(e,e.media,\"timeupdate seeking seeked\",(t=>controls.timeUpdate.call(e,t))),on.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>controls.durationUpdate.call(e,t))),on.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),on.call(e,e.media,\"progress playing seeking seeked\",(t=>controls.updateProgress.call(e,t))),on.call(e,e.media,\"volumechange\",(t=>controls.updateVolume.call(e,t))),on.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>ui.checkPlaying.call(e,t))),on.call(e,e.media,\"waiting canplay seeked playing\",(t=>ui.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=getElement.call(e,`.${e.config.classNames.video}`);if(!is.element(i))return;on.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{silencePromise(e.play())}),\"play\")):this.proxy(s,(()=>{silencePromise(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&on.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),on.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),on.call(e,e.media,\"ratechange\",(()=>{controls.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),on.call(e,e.media,\"qualitychange\",(t=>{controls.updateSetting.call(e,\"quality\",null,t.detail.quality)})),on.call(e,e.media,\"ready qualitychange\",(()=>{controls.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");on.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),triggerEvent.call(e,t.container,i.type,!0,s)}))})),_defineProperty$1(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let r=!0;is.function(n)&&(r=n.call(s,e)),!1!==r&&is.function(t)&&t.call(s,e)})),_defineProperty$1(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:r}=this,a=r.config.listeners[s],o=is.function(a);on.call(r,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),_defineProperty$1(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=browser.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{silencePromise(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{triggerEvent.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),controls.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),controls.toggleMenu.call(e,t)):controls.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&controls.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(is.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),r=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&r?(i.removeAttribute(s),silencePromise(e.play())):!r&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),browser.isIos){const t=getElements.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>repaint(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");is.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>controls.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),browser.isWebKit&&Array.from(getElements.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>controls.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!is.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,controls.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;toggleClass(t.controls,i.classNames.noTransition,!0),ui.toggleControls.call(e,!0),setTimeout((()=>{toggleClass(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),r=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(r/50);const{volume:a}=e.media;(1===r&&a<1||-1===r&&a>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:r,ctrlKey:a,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(r||a||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(is.element(n)){const{editable:s}=t.config.selectors,{seek:r}=i.inputs;if(n!==r&&matches(n,s))return;if(\" \"===e.key&&matches(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||silencePromise(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){controls.toggleMenu.call(this.player,e)}}var loadjs_umd=createCommonjsModule((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,r,a,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)r=e[l],(a=i[r])?n(r,a):(s[r]=s[r]||[]).push(n)}function r(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function a(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var r,a,l=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),p=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((a=l.createElement(\"link\")).rel=\"stylesheet\",a.href=p,(r=\"hideFocus\"in a)&&a.relList&&(r=0,a.rel=\"preload\",a.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(a=l.createElement(\"img\")).src=p:((a=l.createElement(\"script\")).src=t,a.async=void 0===c||c),a.onload=a.onerror=a.onbeforeload=function(e){var l=e.type[0];if(r)try{a.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==a.rel&&\"style\"==a.as)return a.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==h(t,a)&&l.head.appendChild(a)}function l(e,t,i){var s,n,r=(e=e.push?e:[e]).length,a=r,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--r||t(l)},n=0;n<a;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){a(o,e),t&&a({success:t,error:i},e),r(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){a(t,e)})),c},c.done=function(e){r(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function loadScript(e){return new Promise(((t,i)=>{loadjs_umd(e,{success:t,error:i})}))}function parseId$1(e){if(is.empty(e))return null;if(is.number(Number(e)))return e;return e.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:e}function parseHash(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}function assurePlaybackState$1(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}const vimeo={setup(){const e=this;toggleClass(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,setAspectRatio.call(e),is.object(window.Vimeo)?vimeo.ready.call(e):loadScript(e.config.urls.vimeo.sdk).then((()=>{vimeo.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let r=e.media.getAttribute(\"src\"),a=\"\";is.empty(r)?(r=e.media.getAttribute(e.config.attributes.embed.id),a=e.media.getAttribute(e.config.attributes.embed.hash)):a=parseHash(r);const o=a?{h:a}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=buildUrlParams({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=parseId$1(r),u=createElement(\"iframe\"),h=format(e.config.urls.vimeo.iframe,c,l);if(u.setAttribute(\"src\",h),u.setAttribute(\"allowfullscreen\",\"\"),u.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),is.empty(s)||u.setAttribute(\"referrerPolicy\",s),i||!t.customControls)u.setAttribute(\"data-poster\",e.poster),e.media=replaceElement(u,e.media);else{const t=createElement(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(u),e.media=replaceElement(t,e.media)}t.customControls||fetch(format(e.config.urls.vimeo.api,h)).then((t=>{!is.empty(t)&&t.thumbnail_url&&ui.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(u,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(assurePlaybackState$1.call(e,!0),e.embed.play()),e.media.pause=()=>(assurePlaybackState$1.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:d}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>d,set(t){const{embed:i,media:s,paused:n,volume:r}=e,a=n&&!i.hasPlayed;s.seeking=!0,triggerEvent.call(e,s,\"seeking\"),Promise.resolve(a&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>a&&i.pause())).then((()=>a&&i.setVolume(r))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,triggerEvent.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:m}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>m,set(t){e.embed.setVolume(t).then((()=>{m=t,triggerEvent.call(e,e.media,\"volumechange\")}))}});let{muted:g}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>g,set(t){const i=!!is.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{g=i,triggerEvent.call(e,e.media,\"volumechange\")}))}});let f,{loop:y}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>y,set(t){const i=is.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{y=i}))}}),e.embed.getVideoUrl().then((t=>{f=t,controls.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>f}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=roundAspectRatio(i,s),setAspectRatio.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,ui.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{d=t,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,captions.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>stripHTML(e.text)));captions.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{assurePlaybackState$1.call(e,!t),t||triggerEvent.call(e,e.media,\"playing\")})),is.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{triggerEvent.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{assurePlaybackState$1.call(e,!0),triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{assurePlaybackState$1.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,d=t.seconds,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,triggerEvent.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&triggerEvent.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,triggerEvent.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,triggerEvent.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>ui.build.call(e)),0)}};function parseId(e){if(is.empty(e))return null;return e.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:e}function assurePlaybackState(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}function getHost(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const youtube={setup(){if(toggleClass(this.elements.wrapper,this.config.classNames.embed,!0),is.object(window.YT)&&is.function(window.YT.Player))youtube.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{is.function(e)&&e(),youtube.ready.call(this)},loadScript(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){fetch(format(this.config.urls.youtube.api,e)).then((e=>{if(is.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,ui.setTitle.call(this),this.embed.ratio=roundAspectRatio(s,i)}setAspectRatio.call(this)})).catch((()=>{setAspectRatio.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!is.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");is.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=parseId(s),r=createElement(\"div\",{id:generateId(e.provider),\"data-poster\":t.customControls?e.poster:void 0});if(e.media=replaceElement(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;loadImage(t(\"maxres\"),121).catch((()=>loadImage(t(\"sd\"),121))).catch((()=>loadImage(t(\"hq\")))).then((t=>ui.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:getHost(t),playerVars:extend({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},triggerEvent.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),triggerEvent.call(e,e.media,\"ratechange\")},onReady(i){if(is.function(e.media.play))return;const s=i.target;youtube.getTitle.call(e,n),e.media.play=()=>{assurePlaybackState.call(e,!0),s.playVideo()},e.media.pause=()=>{assurePlaybackState.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,triggerEvent.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:r}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>r,set(t){r=t,s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}});let{muted:a}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>a,set(t){const i=is.boolean(t)?t:a;a=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),triggerEvent.call(e,e.media,\"timeupdate\"),triggerEvent.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&triggerEvent.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),triggerEvent.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>ui.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")),i.data){case-1:triggerEvent.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),triggerEvent.call(e,e.media,\"progress\");break;case 0:assurePlaybackState.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):triggerEvent.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(assurePlaybackState.call(e,!0),triggerEvent.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{triggerEvent.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),triggerEvent.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),assurePlaybackState.call(e,!1);break;case 3:triggerEvent.call(e,e.media,\"waiting\")}triggerEvent.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},media={setup(){this.media?(toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),toggleClass(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=createElement(\"div\",{class:this.config.classNames.video}),wrap(this.media,this.elements.wrapper),this.elements.poster=createElement(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?html5.setup.call(this):this.isYouTube?youtube.setup.call(this):this.isVimeo&&vimeo.setup.call(this)):this.debug.warn(\"No media element found!\")}},destroy=e=>{e.manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()};class Ads{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.enabled&&(is.object(window.google)&&is.object(window.google.ima)?this.ready():loadScript(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),_defineProperty$1(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),_defineProperty$1(this,\"setupIMA\",(()=>{this.elements.container=createElement(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),_defineProperty$1(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),_defineProperty$1(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=formatTime(Math.max(this.manager.getRemainingTime(),0)),t=`${i18n.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),_defineProperty$1(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),_defineProperty$1(this,\"addCuePoints\",(()=>{is.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(is.element(t)){const i=100/this.player.duration*e,s=createElement(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),_defineProperty$1(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{triggerEvent.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),_defineProperty$1(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),_defineProperty$1(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;is.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),_defineProperty$1(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),_defineProperty$1(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,silencePromise(this.player.media.play())})),_defineProperty$1(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),_defineProperty$1(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),_defineProperty$1(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),_defineProperty$1(this,\"trigger\",((e,...t)=>{const i=this.events[e];is.array(i)&&i.forEach((e=>{is.function(e)&&e.apply(this,t)}))})),_defineProperty$1(this,\"on\",((e,t)=>(is.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),_defineProperty$1(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),_defineProperty$1(this,\"clearSafetyTimer\",(e=>{is.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!is.empty(e.publisherId)||is.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(is.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${buildUrlParams({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function clamp(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const parseVtt=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(is.number(i.startTime)){if(!is.empty(e.trim())&&is.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},fitRatio=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class PreviewThumbnails{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),_defineProperty$1(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(is.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(is.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(is.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),_defineProperty$1(this,\"getThumbnail\",(e=>new Promise((t=>{fetch(e).then((i=>{const s={frames:parseVtt(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),_defineProperty$1(this,\"startMove\",(e=>{if(this.loaded&&is.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=formatTime(this.seekTime);const r=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));r&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${r.label}<br>`)}this.showImageAtCurrentTime()}})),_defineProperty$1(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),_defineProperty$1(this,\"startScrubbing\",(e=>{(is.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),_defineProperty$1(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):once.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),_defineProperty$1(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),_defineProperty$1(this,\"render\",(()=>{this.elements.thumb.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=createElement(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),is.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),_defineProperty$1(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),_defineProperty$1(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),_defineProperty$1(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],r=i.frames[t].text,a=s+r;if(this.currentImageElement&&this.currentImageElement.dataset.filename===r)this.showImage(this.currentImageElement,n,e,t,r,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=a,i.dataset.index=t,i.dataset.filename=r,this.showingThumbFilename=r,this.player.debug.log(`Loading image: ${a}`),i.onload=()=>this.showImage(i,n,e,t,r,!0),this.loadingImage=i,this.removeOldImages(i)}})),_defineProperty$1(this,\"showImage\",((e,t,i,s,n,r=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${r}`),this.setImageSizeAndOffset(e,t),r&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),_defineProperty$1(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),_defineProperty$1(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let r=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){r=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),r||i()}}),300)})))),_defineProperty$1(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),_defineProperty$1(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),_defineProperty$1(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),_defineProperty$1(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,r=this.mousePosX-e.left-i.clientWidth/2,a=clamp(r,s,n);i.style.left=`${a}px`,i.style.setProperty(\"--preview-arrow-offset\",r-a+\"px\")})),_defineProperty$1(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),_defineProperty$1(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const source={insertElements(e,t){is.string(t)?insertElement(e,this.media,{src:t}):is.array(t)&&t.forEach((t=>{insertElement(e,this.media,t)}))},change(e){getDeep(e,\"sources.length\")?(html5.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],removeElement(this.media),this.media=null,is.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=providers.html5,src:n}]=t,r=\"html5\"===s?i:\"div\",a=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:support.check(i,s,this.config.playsinline),media:createElement(r,a)}),this.elements.container.appendChild(this.media),is.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),is.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),ui.addStyleHook.call(this),this.isHTML5&&source.insertElements.call(this,\"source\",t),this.config.title=e.title,media.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&source.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.isHTML5&&this.media.load(),is.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class Plyr{constructor(e,t){if(_defineProperty$1(this,\"play\",(()=>is.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>silencePromise(this.media.play()))),this.media.play()):null)),_defineProperty$1(this,\"pause\",(()=>this.playing&&is.function(this.media.pause)?this.media.pause():null)),_defineProperty$1(this,\"togglePlay\",(e=>(is.boolean(e)?e:!this.playing)?this.play():this.pause())),_defineProperty$1(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):is.function(this.media.stop)&&this.media.stop()})),_defineProperty$1(this,\"restart\",(()=>{this.currentTime=0})),_defineProperty$1(this,\"rewind\",(e=>{this.currentTime-=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"forward\",(e=>{this.currentTime+=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(is.number(e)?e:0)})),_defineProperty$1(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),_defineProperty$1(this,\"airplay\",(()=>{support.airplay&&this.media.webkitShowPlaybackTargetPicker()})),_defineProperty$1(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=hasClass(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=toggleClass(this.elements.container,this.config.classNames.hideControls,i);if(s&&is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!is.empty(this.config.settings)&&controls.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";triggerEvent.call(this,this.media,e)}return!s}return!1})),_defineProperty$1(this,\"on\",((e,t)=>{on.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"once\",((e,t)=>{once.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"off\",((e,t)=>{off(this.elements.container,e,t)})),_defineProperty$1(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(removeElement(this.elements.buttons.play),removeElement(this.elements.captions),removeElement(this.elements.controls),removeElement(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),is.function(e)&&e()):(unbindListeners.call(this),html5.cancelRequests.call(this),replaceElement(this.elements.original,this.elements.container),triggerEvent.call(this,this.elements.original,\"destroyed\",!0),is.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(ui.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&is.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),_defineProperty$1(this,\"supports\",(e=>support.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=support.touch,this.media=e,is.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||is.nodeList(this.media)||is.array(this.media))&&(this.media=this.media[0]),this.config=extend({},defaults,Plyr.defaults,t||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new Console(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",support),is.nullOrUndefined(this.media)||!is.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!support.check().api)return void this.debug.error(\"Setup failed: no support\");const i=this.media.cloneNode(!0);i.autoplay=!1,this.elements.original=i;const s=this.media.tagName.toLowerCase();let n=null,r=null;switch(s){case\"div\":if(n=this.media.querySelector(\"iframe\"),is.element(n)){if(r=parseUrl(n.getAttribute(\"src\")),this.provider=getProviderByUrl(r.toString()),this.elements.container=this.media,this.media=n,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(is.empty(this.provider)||!Object.values(providers).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=types.video;break;case\"video\":case\"audio\":this.type=s,this.provider=providers.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=support.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Listeners(this),this.storage=new Storage(this),this.media.plyr=this,is.element(this.elements.container)||(this.elements.container=createElement(\"div\"),wrap(this.media,this.elements.container)),ui.migrateStyles.call(this),ui.addStyleHook.call(this),media.setup.call(this),this.config.debug&&on.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new Fullscreen(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ads(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>silencePromise(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===providers.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===providers.youtube}get isVimeo(){return this.provider===providers.vimeo}get isVideo(){return this.type===types.video}get isAudio(){return this.type===types.audio}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=is.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return is.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=is.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;is.string(t)&&(t=Number(t)),is.number(t)||(t=this.storage.get(\"volume\")),is.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!is.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;is.boolean(t)||(t=this.storage.get(\"muted\")),is.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;is.number(e)&&(t=e),is.number(t)||(t=this.storage.get(\"speed\")),is.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=clamp(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!is.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(is.number),n=!0;if(!i.includes(s)){const e=closest(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=is.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){source.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return is.url(e)?e:this.source}set download(e){is.url(e)&&(this.config.urls.download=e,controls.setDownloadUrl.call(this))}set poster(e){this.isVideo?ui.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=reduceAspectRatio(getAspectRatio.call(this));return is.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?is.string(e)&&validateAspectRatio(e)?(this.config.ratio=reduceAspectRatio(e),setAspectRatio.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=is.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){captions.toggle.call(this,e,!1)}set currentTrack(e){captions.set.call(this,e,!1),captions.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){captions.setLanguage.call(this,e,!1)}get language(){return(captions.getCurrentTrack.call(this)||{}).language}set pip(e){if(!support.pip)return;const t=is.boolean(e)?e:!this.pip;is.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?pip.active:pip.inactive),is.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return support.pip?is.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===pip.active:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))}static supported(e,t){return support.check(e,t)}static loadSprite(e,t){return loadSprite(e,t)}static setup(e,t={}){let i=null;return is.string(e)?i=Array.from(document.querySelectorAll(e)):is.nodeList(e)?i=Array.from(e):is.array(e)&&(i=e.filter(is.element)),is.empty(i)?null:i.map((e=>new Plyr(e,t)))}}Plyr.defaults=cloneDeep(defaults);export{Plyr as default};\n\\ No newline at end of file\n+!function(){if(\"undefined\"!=typeof window)try{var e=new window.CustomEvent(\"test\",{cancelable:!0});if(e.preventDefault(),!0!==e.defaultPrevented)throw new Error(\"Could not prevent default\")}catch(e){var t=function(e,t){var i,s;return(t=t||{}).bubbles=!!t.bubbles,t.cancelable=!!t.cancelable,(i=document.createEvent(\"CustomEvent\")).initCustomEvent(e,t.bubbles,t.cancelable,t.detail),s=i.preventDefault,i.preventDefault=function(){s.call(this);try{Object.defineProperty(this,\"defaultPrevented\",{get:function(){return!0}})}catch(e){this.defaultPrevented=!0}},i};t.prototype=window.Event.prototype,window.CustomEvent=t}}();var commonjsGlobal=\"undefined\"!=typeof globalThis?globalThis:\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:{};function createCommonjsModule(e,t){return e(t={exports:{}},t.exports),t.exports}function _defineProperty$1(e,t,i){return(t=_toPropertyKey(t))in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function _toPrimitive(e,t){if(\"object\"!=typeof e||null===e)return e;var i=e[Symbol.toPrimitive];if(void 0!==i){var s=i.call(e,t||\"default\");if(\"object\"!=typeof s)return s;throw new TypeError(\"@@toPrimitive must return a primitive value.\")}return(\"string\"===t?String:Number)(e)}function _toPropertyKey(e){var t=_toPrimitive(e,\"string\");return\"symbol\"==typeof t?t:String(t)}function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\"Cannot call a class as a function\")}function _defineProperties(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,\"value\"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,i){return t&&_defineProperties(e.prototype,t),i&&_defineProperties(e,i),e}function _defineProperty(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function ownKeys(e,t){var i=Object.keys(e);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);t&&(s=s.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),i.push.apply(i,s)}return i}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(i),!0).forEach((function(t){_defineProperty(e,t,i[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(i)):ownKeys(Object(i)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(i,t))}))}return e}!function(e){var t=function(){try{return!!Symbol.iterator}catch(e){return!1}}(),i=function(e){var i={next:function(){var t=e.shift();return{done:void 0===t,value:t}}};return t&&(i[Symbol.iterator]=function(){return i}),i},s=function(e){return encodeURIComponent(e).replace(/%20/g,\"+\")},n=function(e){return decodeURIComponent(String(e).replace(/\\+/g,\" \"))};(function(){try{var t=e.URLSearchParams;return\"a=1\"===new t(\"?a=1\").toString()&&\"function\"==typeof t.prototype.set&&\"function\"==typeof t.prototype.entries}catch(e){return!1}})()||function(){var n=function(e){Object.defineProperty(this,\"_entries\",{writable:!0,value:{}});var t=typeof e;if(\"undefined\"===t);else if(\"string\"===t)\"\"!==e&&this._fromString(e);else if(e instanceof n){var i=this;e.forEach((function(e,t){i.append(t,e)}))}else{if(null===e||\"object\"!==t)throw new TypeError(\"Unsupported input's type for URLSearchParams\");if(\"[object Array]\"===Object.prototype.toString.call(e))for(var s=0;s<e.length;s++){var r=e[s];if(\"[object Array]\"!==Object.prototype.toString.call(r)&&2===r.length)throw new TypeError(\"Expected [string, any] as entry at index \"+s+\" of URLSearchParams's input\");this.append(r[0],r[1])}else for(var a in e)e.hasOwnProperty(a)&&this.append(a,e[a])}},r=n.prototype;r.append=function(e,t){e in this._entries?this._entries[e].push(String(t)):this._entries[e]=[String(t)]},r.delete=function(e){delete this._entries[e]},r.get=function(e){return e in this._entries?this._entries[e][0]:null},r.getAll=function(e){return e in this._entries?this._entries[e].slice(0):[]},r.has=function(e){return e in this._entries},r.set=function(e,t){this._entries[e]=[String(t)]},r.forEach=function(e,t){var i;for(var s in this._entries)if(this._entries.hasOwnProperty(s)){i=this._entries[s];for(var n=0;n<i.length;n++)e.call(t,i[n],s,this)}},r.keys=function(){var e=[];return this.forEach((function(t,i){e.push(i)})),i(e)},r.values=function(){var e=[];return this.forEach((function(t){e.push(t)})),i(e)},r.entries=function(){var e=[];return this.forEach((function(t,i){e.push([i,t])})),i(e)},t&&(r[Symbol.iterator]=r.entries),r.toString=function(){var e=[];return this.forEach((function(t,i){e.push(s(i)+\"=\"+s(t))})),e.join(\"&\")},e.URLSearchParams=n}();var r=e.URLSearchParams.prototype;\"function\"!=typeof r.sort&&(r.sort=function(){var e=this,t=[];this.forEach((function(i,s){t.push([s,i]),e._entries||e.delete(s)})),t.sort((function(e,t){return e[0]<t[0]?-1:e[0]>t[0]?1:0})),e._entries&&(e._entries={});for(var i=0;i<t.length;i++)this.append(t[i][0],t[i][1])}),\"function\"!=typeof r._fromString&&Object.defineProperty(r,\"_fromString\",{enumerable:!1,configurable:!1,writable:!1,value:function(e){if(this._entries)this._entries={};else{var t=[];this.forEach((function(e,i){t.push(i)}));for(var i=0;i<t.length;i++)this.delete(t[i])}var s,r=(e=e.replace(/^\\?/,\"\")).split(\"&\");for(i=0;i<r.length;i++)s=r[i].split(\"=\"),this.append(n(s[0]),s.length>1?n(s[1]):\"\")}})}(void 0!==commonjsGlobal?commonjsGlobal:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:commonjsGlobal),function(e){if(function(){try{var t=new e.URL(\"b\",\"http://a\");return t.pathname=\"c d\",\"http://a/c%20d\"===t.href&&t.searchParams}catch(e){return!1}}()||function(){var t=e.URL,i=function(t,i){\"string\"!=typeof t&&(t=String(t)),i&&\"string\"!=typeof i&&(i=String(i));var s,n=document;if(i&&(void 0===e.location||i!==e.location.href)){i=i.toLowerCase(),(s=(n=document.implementation.createHTMLDocument(\"\")).createElement(\"base\")).href=i,n.head.appendChild(s);try{if(0!==s.href.indexOf(i))throw new Error(s.href)}catch(e){throw new Error(\"URL unable to set base \"+i+\" due to \"+e)}}var r=n.createElement(\"a\");r.href=t,s&&(n.body.appendChild(r),r.href=r.href);var a=n.createElement(\"input\");if(a.type=\"url\",a.value=t,\":\"===r.protocol||!/:/.test(r.href)||!a.checkValidity()&&!i)throw new TypeError(\"Invalid URL\");Object.defineProperty(this,\"_anchorElement\",{value:r});var o=new e.URLSearchParams(this.search),l=!0,c=!0,u=this;[\"append\",\"delete\",\"set\"].forEach((function(e){var t=o[e];o[e]=function(){t.apply(o,arguments),l&&(c=!1,u.search=o.toString(),c=!0)}})),Object.defineProperty(this,\"searchParams\",{value:o,enumerable:!0});var h=void 0;Object.defineProperty(this,\"_updateSearchParams\",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==h&&(h=this.search,c&&(l=!1,this.searchParams._fromString(this.search),l=!0))}})},s=i.prototype;[\"hash\",\"host\",\"hostname\",\"port\",\"protocol\"].forEach((function(e){!function(e){Object.defineProperty(s,e,{get:function(){return this._anchorElement[e]},set:function(t){this._anchorElement[e]=t},enumerable:!0})}(e)})),Object.defineProperty(s,\"search\",{get:function(){return this._anchorElement.search},set:function(e){this._anchorElement.search=e,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(s,{toString:{get:function(){var e=this;return function(){return e.href}}},href:{get:function(){return this._anchorElement.href.replace(/\\?$/,\"\")},set:function(e){this._anchorElement.href=e,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\\/?)/,\"/\")},set:function(e){this._anchorElement.pathname=e},enumerable:!0},origin:{get:function(){var e={\"http:\":80,\"https:\":443,\"ftp:\":21}[this._anchorElement.protocol],t=this._anchorElement.port!=e&&\"\"!==this._anchorElement.port;return this._anchorElement.protocol+\"//\"+this._anchorElement.hostname+(t?\":\"+this._anchorElement.port:\"\")},enumerable:!0},password:{get:function(){return\"\"},set:function(e){},enumerable:!0},username:{get:function(){return\"\"},set:function(e){},enumerable:!0}}),i.createObjectURL=function(e){return t.createObjectURL.apply(t,arguments)},i.revokeObjectURL=function(e){return t.revokeObjectURL.apply(t,arguments)},e.URL=i}(),void 0!==e.location&&!(\"origin\"in e.location)){var t=function(){return e.location.protocol+\"//\"+e.location.hostname+(e.location.port?\":\"+e.location.port:\"\")};try{Object.defineProperty(e.location,\"origin\",{get:t,enumerable:!0})}catch(i){setInterval((function(){e.location.origin=t()}),100)}}}(void 0!==commonjsGlobal?commonjsGlobal:\"undefined\"!=typeof window?window:\"undefined\"!=typeof self?self:commonjsGlobal);var defaults$1={addCSS:!0,thumbWidth:15,watch:!0};function matches$1(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var i=new Event(t,{bubbles:!0});e.dispatchEvent(i)}}var getConstructor$1=function(e){return null!=e?e.constructor:null},instanceOf$1=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined$1=function(e){return null==e},isObject$1=function(e){return getConstructor$1(e)===Object},isNumber$1=function(e){return getConstructor$1(e)===Number&&!Number.isNaN(e)},isString$1=function(e){return getConstructor$1(e)===String},isBoolean$1=function(e){return getConstructor$1(e)===Boolean},isFunction$1=function(e){return getConstructor$1(e)===Function},isArray$1=function(e){return Array.isArray(e)},isNodeList$1=function(e){return instanceOf$1(e,NodeList)},isElement$1=function(e){return instanceOf$1(e,Element)},isEvent$1=function(e){return instanceOf$1(e,Event)},isEmpty$1=function(e){return isNullOrUndefined$1(e)||(isString$1(e)||isArray$1(e)||isNodeList$1(e))&&!e.length||isObject$1(e)&&!Object.keys(e).length},is$1={nullOrUndefined:isNullOrUndefined$1,object:isObject$1,number:isNumber$1,string:isString$1,boolean:isBoolean$1,function:isFunction$1,array:isArray$1,nodeList:isNodeList$1,element:isElement$1,event:isEvent$1,empty:isEmpty$1};function getDecimalPlaces(e){var t=\"\".concat(e).match(/(?:\\.(\\d+))?(?:[eE]([+-]?\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var i=getDecimalPlaces(t);return parseFloat(e.toFixed(i))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,i){_classCallCheck(this,e),is$1.element(t)?this.element=t:is$1.string(t)&&(this.element=document.querySelector(t)),is$1.element(this.element)&&is$1.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults$1,{},i),this.init())}return _createClass(e,[{key:\"init\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"none\",this.element.style.webKitUserSelect=\"none\",this.element.style.touchAction=\"manipulation\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\"destroy\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\"\",this.element.style.webKitUserSelect=\"\",this.element.style.touchAction=\"\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\"listeners\",value:function(e){var t=this,i=e?\"addEventListener\":\"removeEventListener\";[\"touchstart\",\"touchmove\",\"touchend\"].forEach((function(e){t.element[i](e,(function(e){return t.set(e)}),!1)}))}},{key:\"get\",value:function(t){if(!e.enabled||!is$1.event(t))return null;var i,s=t.target,n=t.changedTouches[0],r=parseFloat(s.getAttribute(\"min\"))||0,a=parseFloat(s.getAttribute(\"max\"))||100,o=parseFloat(s.getAttribute(\"step\"))||1,l=s.getBoundingClientRect(),c=100/l.width*(this.config.thumbWidth/2)/100;return 0>(i=100/l.width*(n.clientX-l.left))?i=0:100<i&&(i=100),50>i?i-=(100-2*i)*c:50<i&&(i+=2*(i-50)*c),r+round(i/100*(a-r),o)}},{key:\"set\",value:function(t){e.enabled&&is$1.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\"touchend\"===t.type?\"change\":\"input\"))}}],[{key:\"setup\",value:function(t){var i=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s=null;if(is$1.empty(t)||is$1.string(t)?s=Array.from(document.querySelectorAll(is$1.string(t)?t:'input[type=\"range\"]')):is$1.element(t)?s=[t]:is$1.nodeList(t)?s=Array.from(t):is$1.array(t)&&(s=t.filter(is$1.element)),is$1.empty(s))return null;var n=_objectSpread2({},defaults$1,{},i);if(is$1.string(t)&&n.watch){var r=new MutationObserver((function(i){Array.from(i).forEach((function(i){Array.from(i.addedNodes).forEach((function(i){is$1.element(i)&&matches$1(i,t)&&new e(i,n)}))}))}));r.observe(document.body,{childList:!0,subtree:!0})}return s.map((function(t){return new e(t,i)}))}},{key:\"enabled\",get:function(){return\"ontouchstart\"in document.documentElement}}]),e}();const getConstructor=e=>null!=e?e.constructor:null,instanceOf=(e,t)=>Boolean(e&&t&&e instanceof t),isNullOrUndefined=e=>null==e,isObject=e=>getConstructor(e)===Object,isNumber=e=>getConstructor(e)===Number&&!Number.isNaN(e),isString=e=>getConstructor(e)===String,isBoolean=e=>getConstructor(e)===Boolean,isFunction=e=>\"function\"==typeof e,isArray=e=>Array.isArray(e),isWeakMap=e=>instanceOf(e,WeakMap),isNodeList=e=>instanceOf(e,NodeList),isTextNode=e=>getConstructor(e)===Text,isEvent=e=>instanceOf(e,Event),isKeyboardEvent=e=>instanceOf(e,KeyboardEvent),isCue=e=>instanceOf(e,window.TextTrackCue)||instanceOf(e,window.VTTCue),isTrack=e=>instanceOf(e,TextTrack)||!isNullOrUndefined(e)&&isString(e.kind),isPromise=e=>instanceOf(e,Promise)&&isFunction(e.then),isElement=e=>null!==e&&\"object\"==typeof e&&1===e.nodeType&&\"object\"==typeof e.style&&\"object\"==typeof e.ownerDocument,isEmpty=e=>isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length,isUrl=e=>{if(instanceOf(e,window.URL))return!0;if(!isString(e))return!1;let t=e;e.startsWith(\"http://\")&&e.startsWith(\"https://\")||(t=`http://${e}`);try{return!isEmpty(new URL(t).hostname)}catch(e){return!1}};var is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,weakMap:isWeakMap,nodeList:isNodeList,element:isElement,textNode:isTextNode,event:isEvent,keyboardEvent:isKeyboardEvent,cue:isCue,track:isTrack,promise:isPromise,url:isUrl,empty:isEmpty};const transitionEndEvent=(()=>{const e=document.createElement(\"span\"),t={WebkitTransition:\"webkitTransitionEnd\",MozTransition:\"transitionend\",OTransition:\"oTransitionEnd otransitionend\",transition:\"transitionend\"},i=Object.keys(t).find((t=>void 0!==e.style[t]));return!!is.string(i)&&t[i]})();function repaint(e,t){setTimeout((()=>{try{e.hidden=!0,e.offsetHeight,e.hidden=!1}catch(e){}}),t)}const isIE=Boolean(window.document.documentMode),isEdge=/Edge/g.test(navigator.userAgent),isWebKit=\"WebkitAppearance\"in document.documentElement.style&&!/Edge/g.test(navigator.userAgent),isIPhone=/iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1,isIPadOS=\"MacIntel\"===navigator.platform&&navigator.maxTouchPoints>1,isIos=/iPad|iPhone|iPod/gi.test(navigator.userAgent)&&navigator.maxTouchPoints>1;var browser={isIE:isIE,isEdge:isEdge,isWebKit:isWebKit,isIPhone:isIPhone,isIPadOS:isIPadOS,isIos:isIos};function cloneDeep(e){return JSON.parse(JSON.stringify(e))}function getDeep(e,t){return t.split(\".\").reduce(((e,t)=>e&&e[t]),e)}function extend(e={},...t){if(!t.length)return e;const i=t.shift();return is.object(i)?(Object.keys(i).forEach((t=>{is.object(i[t])?(Object.keys(e).includes(t)||Object.assign(e,{[t]:{}}),extend(e[t],i[t])):Object.assign(e,{[t]:i[t]})})),extend(e,...t)):e}function wrap(e,t){const i=e.length?e:[e];Array.from(i).reverse().forEach(((e,i)=>{const s=i>0?t.cloneNode(!0):t,n=e.parentNode,r=e.nextSibling;s.appendChild(e),r?n.insertBefore(s,r):n.appendChild(s)}))}function setAttributes(e,t){is.element(e)&&!is.empty(t)&&Object.entries(t).filter((([,e])=>!is.nullOrUndefined(e))).forEach((([t,i])=>e.setAttribute(t,i)))}function createElement(e,t,i){const s=document.createElement(e);return is.object(t)&&setAttributes(s,t),is.string(i)&&(s.innerText=i),s}function insertAfter(e,t){is.element(e)&&is.element(t)&&t.parentNode.insertBefore(e,t.nextSibling)}function insertElement(e,t,i,s){is.element(t)&&t.appendChild(createElement(e,i,s))}function removeElement(e){is.nodeList(e)||is.array(e)?Array.from(e).forEach(removeElement):is.element(e)&&is.element(e.parentNode)&&e.parentNode.removeChild(e)}function emptyElement(e){if(!is.element(e))return;let{length:t}=e.childNodes;for(;t>0;)e.removeChild(e.lastChild),t-=1}function replaceElement(e,t){return is.element(t)&&is.element(t.parentNode)&&is.element(e)?(t.parentNode.replaceChild(e,t),e):null}function getAttributesFromSelector(e,t){if(!is.string(e)||is.empty(e))return{};const i={},s=extend({},t);return e.split(\",\").forEach((e=>{const t=e.trim(),n=t.replace(\".\",\"\"),r=t.replace(/[[\\]]/g,\"\").split(\"=\"),[a]=r,o=r.length>1?r[1].replace(/[\"']/g,\"\"):\"\";switch(t.charAt(0)){case\".\":is.string(s.class)?i.class=`${s.class} ${n}`:i.class=n;break;case\"#\":i.id=t.replace(\"#\",\"\");break;case\"[\":i[a]=o}})),extend(s,i)}function toggleHidden(e,t){if(!is.element(e))return;let i=t;is.boolean(i)||(i=!e.hidden),e.hidden=i}function toggleClass(e,t,i){if(is.nodeList(e))return Array.from(e).map((e=>toggleClass(e,t,i)));if(is.element(e)){let s=\"toggle\";return void 0!==i&&(s=i?\"add\":\"remove\"),e.classList[s](t),e.classList.contains(t)}return!1}function hasClass(e,t){return is.element(e)&&e.classList.contains(t)}function matches(e,t){const{prototype:i}=Element;return(i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.msMatchesSelector||function(){return Array.from(document.querySelectorAll(t)).includes(this)}).call(e,t)}function closest$1(e,t){const{prototype:i}=Element;return(i.closest||function(){let e=this;do{if(matches.matches(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}).call(e,t)}function getElements(e){return this.elements.container.querySelectorAll(e)}function getElement(e){return this.elements.container.querySelector(e)}function setFocus(e=null,t=!1){is.element(e)&&e.focus({preventScroll:!0,focusVisible:t})}const defaultCodecs={\"audio/ogg\":\"vorbis\",\"audio/wav\":\"1\",\"video/webm\":\"vp8, vorbis\",\"video/mp4\":\"avc1.42E01E, mp4a.40.2\",\"video/ogg\":\"theora\"},support={audio:\"canPlayType\"in document.createElement(\"audio\"),video:\"canPlayType\"in document.createElement(\"video\"),check(e,t){const i=support[e]||\"html5\"!==t;return{api:i,ui:i&&support.rangeInput}},pip:!(browser.isIPhone||!is.function(createElement(\"video\").webkitSetPresentationMode)&&(!document.pictureInPictureEnabled||createElement(\"video\").disablePictureInPicture)),airplay:is.function(window.WebKitPlaybackTargetAvailabilityEvent),playsinline:\"playsInline\"in document.createElement(\"video\"),mime(e){if(is.empty(e))return!1;const[t]=e.split(\"/\");let i=e;if(!this.isHTML5||t!==this.type)return!1;Object.keys(defaultCodecs).includes(i)&&(i+=`; codecs=\"${defaultCodecs[e]}\"`);try{return Boolean(i&&this.media.canPlayType(i).replace(/no/,\"\"))}catch(e){return!1}},textTracks:\"textTracks\"in document.createElement(\"video\"),rangeInput:(()=>{const e=document.createElement(\"input\");return e.type=\"range\",\"range\"===e.type})(),touch:\"ontouchstart\"in document.documentElement,transitions:!1!==transitionEndEvent,reducedMotion:\"matchMedia\"in window&&window.matchMedia(\"(prefers-reduced-motion)\").matches},supportsPassiveListeners=(()=>{let e=!1;try{const t=Object.defineProperty({},\"passive\",{get:()=>(e=!0,null)});window.addEventListener(\"test\",null,t),window.removeEventListener(\"test\",null,t)}catch(e){}return e})();function toggleListener(e,t,i,s=!1,n=!0,r=!1){if(!e||!(\"addEventListener\"in e)||is.empty(t)||!is.function(i))return;const a=t.split(\" \");let o=r;supportsPassiveListeners&&(o={passive:n,capture:r}),a.forEach((t=>{this&&this.eventListeners&&s&&this.eventListeners.push({element:e,type:t,callback:i,options:o}),e[s?\"addEventListener\":\"removeEventListener\"](t,i,o)}))}function on(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!0,s,n)}function off(e,t=\"\",i,s=!0,n=!1){toggleListener.call(this,e,t,i,!1,s,n)}function once(e,t=\"\",i,s=!0,n=!1){const r=(...a)=>{off(e,t,r,s,n),i.apply(this,a)};toggleListener.call(this,e,t,r,!0,s,n)}function triggerEvent(e,t=\"\",i=!1,s={}){if(!is.element(e)||is.empty(t))return;const n=new CustomEvent(t,{bubbles:i,detail:{...s,plyr:this}});e.dispatchEvent(n)}function unbindListeners(){this&&this.eventListeners&&(this.eventListeners.forEach((e=>{const{element:t,type:i,callback:s,options:n}=e;t.removeEventListener(i,s,n)})),this.eventListeners=[])}function ready(){return new Promise((e=>this.ready?setTimeout(e,0):on.call(this,this.elements.container,\"ready\",e))).then((()=>{}))}function silencePromise(e){is.promise(e)&&e.then(null,(()=>{}))}function dedupe(e){return is.array(e)?e.filter(((t,i)=>e.indexOf(t)===i)):e}function closest(e,t){return is.array(e)&&e.length?e.reduce(((e,i)=>Math.abs(i-t)<Math.abs(e-t)?i:e)):null}function supportsCSS(e){return!(!window||!window.CSS)&&window.CSS.supports(e)}const standardRatios=[[1,1],[4,3],[3,4],[5,4],[4,5],[3,2],[2,3],[16,10],[10,16],[16,9],[9,16],[21,9],[9,21],[32,9],[9,32]].reduce(((e,[t,i])=>({...e,[t/i]:[t,i]})),{});function validateAspectRatio(e){if(!(is.array(e)||is.string(e)&&e.includes(\":\")))return!1;return(is.array(e)?e:e.split(\":\")).map(Number).every(is.number)}function reduceAspectRatio(e){if(!is.array(e)||!e.every(is.number))return null;const[t,i]=e,s=(e,t)=>0===t?e:s(t,e%t),n=s(t,i);return[t/n,i/n]}function getAspectRatio(e){const t=e=>validateAspectRatio(e)?e.split(\":\").map(Number):null;let i=t(e);if(null===i&&(i=t(this.config.ratio)),null===i&&!is.empty(this.embed)&&is.array(this.embed.ratio)&&({ratio:i}=this.embed),null===i&&this.isHTML5){const{videoWidth:e,videoHeight:t}=this.media;i=[e,t]}return reduceAspectRatio(i)}function setAspectRatio(e){if(!this.isVideo)return{};const{wrapper:t}=this.elements,i=getAspectRatio.call(this,e);if(!is.array(i))return{};const[s,n]=reduceAspectRatio(i),r=100/s*n;if(supportsCSS(`aspect-ratio: ${s}/${n}`)?t.style.aspectRatio=`${s}/${n}`:t.style.paddingBottom=`${r}%`,this.isVimeo&&!this.config.vimeo.premium&&this.supported.ui){const e=100/this.media.offsetWidth*parseInt(window.getComputedStyle(this.media).paddingBottom,10),i=(e-r)/(e/50);this.fullscreen.active?t.style.paddingBottom=null:this.media.style.transform=`translateY(-${i}%)`}else this.isHTML5&&t.classList.add(this.config.classNames.videoFixedRatio);return{padding:r,ratio:i}}function roundAspectRatio(e,t,i=.05){const s=e/t,n=closest(Object.keys(standardRatios),s);return Math.abs(n-s)<=i?standardRatios[n]:[e,t]}function getViewportSize(){return[Math.max(document.documentElement.clientWidth||0,window.innerWidth||0),Math.max(document.documentElement.clientHeight||0,window.innerHeight||0)]}const html5={getSources(){if(!this.isHTML5)return[];return Array.from(this.media.querySelectorAll(\"source\")).filter((e=>{const t=e.getAttribute(\"type\");return!!is.empty(t)||support.mime.call(this,t)}))},getQualityOptions(){return this.config.quality.forced?this.config.quality.options:html5.getSources.call(this).map((e=>Number(e.getAttribute(\"size\")))).filter(Boolean)},setup(){if(!this.isHTML5)return;const e=this;e.options.speed=e.config.speed.options,is.empty(this.config.ratio)||setAspectRatio.call(e),Object.defineProperty(e.media,\"quality\",{get(){const t=html5.getSources.call(e).find((t=>t.getAttribute(\"src\")===e.source));return t&&Number(t.getAttribute(\"size\"))},set(t){if(e.quality!==t){if(e.config.quality.forced&&is.function(e.config.quality.onChange))e.config.quality.onChange(t);else{const i=html5.getSources.call(e).find((e=>Number(e.getAttribute(\"size\"))===t));if(!i)return;const{currentTime:s,paused:n,preload:r,readyState:a,playbackRate:o}=e.media;e.media.src=i.getAttribute(\"src\"),(\"none\"!==r||a)&&(e.once(\"loadedmetadata\",(()=>{e.speed=o,e.currentTime=s,n||silencePromise(e.play())})),e.media.load())}triggerEvent.call(e,e.media,\"qualitychange\",!1,{quality:t})}}})},cancelRequests(){this.isHTML5&&(removeElement(html5.getSources.call(this)),this.media.setAttribute(\"src\",this.config.blankVideo),this.media.load(),this.debug.log(\"Cancelled network requests\"))}};function generateId(e){return`${e}-${Math.floor(1e4*Math.random())}`}function format(e,...t){return is.empty(e)?e:e.toString().replace(/{(\\d+)}/g,((e,i)=>t[i].toString()))}function getPercentage(e,t){return 0===e||0===t||Number.isNaN(e)||Number.isNaN(t)?0:(e/t*100).toFixed(2)}const replaceAll=(e=\"\",t=\"\",i=\"\")=>e.replace(new RegExp(t.toString().replace(/([.*+?^=!:${}()|[\\]/\\\\])/g,\"\\\\$1\"),\"g\"),i.toString()),toTitleCase=(e=\"\")=>e.toString().replace(/\\w\\S*/g,(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()));function toPascalCase(e=\"\"){let t=e.toString();return t=replaceAll(t,\"-\",\" \"),t=replaceAll(t,\"_\",\" \"),t=toTitleCase(t),replaceAll(t,\" \",\"\")}function toCamelCase(e=\"\"){let t=e.toString();return t=toPascalCase(t),t.charAt(0).toLowerCase()+t.slice(1)}function stripHTML(e){const t=document.createDocumentFragment(),i=document.createElement(\"div\");return t.appendChild(i),i.innerHTML=e,t.firstChild.innerText}function getHTML(e){const t=document.createElement(\"div\");return t.appendChild(e),t.innerHTML}const resources={pip:\"PIP\",airplay:\"AirPlay\",html5:\"HTML5\",vimeo:\"Vimeo\",youtube:\"YouTube\"},i18n={get(e=\"\",t={}){if(is.empty(e)||is.empty(t))return\"\";let i=getDeep(t.i18n,e);if(is.empty(i))return Object.keys(resources).includes(e)?resources[e]:\"\";const s={\"{seektime}\":t.seekTime,\"{title}\":t.title};return Object.entries(s).forEach((([e,t])=>{i=replaceAll(i,e,t)})),i}};class Storage{constructor(e){_defineProperty$1(this,\"get\",(e=>{if(!Storage.supported||!this.enabled)return null;const t=window.localStorage.getItem(this.key);if(is.empty(t))return null;const i=JSON.parse(t);return is.string(e)&&e.length?i[e]:i})),_defineProperty$1(this,\"set\",(e=>{if(!Storage.supported||!this.enabled)return;if(!is.object(e))return;let t=this.get();is.empty(t)&&(t={}),extend(t,e);try{window.localStorage.setItem(this.key,JSON.stringify(t))}catch(e){}})),this.enabled=e.config.storage.enabled,this.key=e.config.storage.key}static get supported(){try{if(!(\"localStorage\"in window))return!1;const e=\"___test\";return window.localStorage.setItem(e,e),window.localStorage.removeItem(e),!0}catch(e){return!1}}}function fetch(e,t=\"text\"){return new Promise(((i,s)=>{try{const s=new XMLHttpRequest;if(!(\"withCredentials\"in s))return;s.addEventListener(\"load\",(()=>{if(\"text\"===t)try{i(JSON.parse(s.responseText))}catch(e){i(s.responseText)}else i(s.response)})),s.addEventListener(\"error\",(()=>{throw new Error(s.status)})),s.open(\"GET\",e,!0),s.responseType=t,s.send()}catch(e){s(e)}}))}function loadSprite(e,t){if(!is.string(e))return;const i=\"cache\",s=is.string(t);let n=!1;const r=()=>null!==document.getElementById(t),a=(e,t)=>{e.innerHTML=t,s&&r()||document.body.insertAdjacentElement(\"afterbegin\",e)};if(!s||!r()){const r=Storage.supported,o=document.createElement(\"div\");if(o.setAttribute(\"hidden\",\"\"),s&&o.setAttribute(\"id\",t),r){const e=window.localStorage.getItem(`${i}-${t}`);if(n=null!==e,n){const t=JSON.parse(e);a(o,t.content)}}fetch(e).then((e=>{if(!is.empty(e)){if(r)try{window.localStorage.setItem(`${i}-${t}`,JSON.stringify({content:e}))}catch(e){}a(o,e)}})).catch((()=>{}))}}const getHours=e=>Math.trunc(e/60/60%60,10),getMinutes=e=>Math.trunc(e/60%60,10),getSeconds=e=>Math.trunc(e%60,10);function formatTime(e=0,t=!1,i=!1){if(!is.number(e))return formatTime(void 0,t,i);const s=e=>`0${e}`.slice(-2);let n=getHours(e);const r=getMinutes(e),a=getSeconds(e);return n=t||n>0?`${n}:`:\"\",`${i&&e>0?\"-\":\"\"}${n}${s(r)}:${s(a)}`}const controls={getIconUrl(){const e=new URL(this.config.iconUrl,window.location),t=window.location.host?window.location.host:window.top.location.host,i=e.host!==t||browser.isIE&&!window.svg4everybody;return{url:this.config.iconUrl,cors:i}},findElements(){try{return this.elements.controls=getElement.call(this,this.config.selectors.controls.wrapper),this.elements.buttons={play:getElements.call(this,this.config.selectors.buttons.play),pause:getElement.call(this,this.config.selectors.buttons.pause),restart:getElement.call(this,this.config.selectors.buttons.restart),rewind:getElement.call(this,this.config.selectors.buttons.rewind),fastForward:getElement.call(this,this.config.selectors.buttons.fastForward),mute:getElement.call(this,this.config.selectors.buttons.mute),pip:getElement.call(this,this.config.selectors.buttons.pip),airplay:getElement.call(this,this.config.selectors.buttons.airplay),settings:getElement.call(this,this.config.selectors.buttons.settings),captions:getElement.call(this,this.config.selectors.buttons.captions),fullscreen:getElement.call(this,this.config.selectors.buttons.fullscreen)},this.elements.progress=getElement.call(this,this.config.selectors.progress),this.elements.inputs={seek:getElement.call(this,this.config.selectors.inputs.seek),volume:getElement.call(this,this.config.selectors.inputs.volume)},this.elements.display={buffer:getElement.call(this,this.config.selectors.display.buffer),currentTime:getElement.call(this,this.config.selectors.display.currentTime),duration:getElement.call(this,this.config.selectors.display.duration)},is.element(this.elements.progress)&&(this.elements.display.seekTooltip=this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`)),!0}catch(e){return this.debug.warn(\"It looks like there is a problem with your custom controls HTML\",e),this.toggleNativeControls(!0),!1}},createIcon(e,t){const i=\"http://www.w3.org/2000/svg\",s=controls.getIconUrl.call(this),n=`${s.cors?\"\":s.url}#${this.config.iconPrefix}`,r=document.createElementNS(i,\"svg\");setAttributes(r,extend(t,{\"aria-hidden\":\"true\",focusable:\"false\"}));const a=document.createElementNS(i,\"use\"),o=`${n}-${e}`;return\"href\"in a&&a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"href\",o),a.setAttributeNS(\"http://www.w3.org/1999/xlink\",\"xlink:href\",o),r.appendChild(a),r},createLabel(e,t={}){const i=i18n.get(e,this.config);return createElement(\"span\",{...t,class:[t.class,this.config.classNames.hidden].filter(Boolean).join(\" \")},i)},createBadge(e){if(is.empty(e))return null;const t=createElement(\"span\",{class:this.config.classNames.menu.value});return t.appendChild(createElement(\"span\",{class:this.config.classNames.menu.badge},e)),t},createButton(e,t){const i=extend({},t);let s=toCamelCase(e);const n={element:\"button\",toggle:!1,label:null,icon:null,labelPressed:null,iconPressed:null};switch([\"element\",\"icon\",\"label\"].forEach((e=>{Object.keys(i).includes(e)&&(n[e]=i[e],delete i[e])})),\"button\"!==n.element||Object.keys(i).includes(\"type\")||(i.type=\"button\"),Object.keys(i).includes(\"class\")?i.class.split(\" \").some((e=>e===this.config.classNames.control))||extend(i,{class:`${i.class} ${this.config.classNames.control}`}):i.class=this.config.classNames.control,e){case\"play\":n.toggle=!0,n.label=\"play\",n.labelPressed=\"pause\",n.icon=\"play\",n.iconPressed=\"pause\";break;case\"mute\":n.toggle=!0,n.label=\"mute\",n.labelPressed=\"unmute\",n.icon=\"volume\",n.iconPressed=\"muted\";break;case\"captions\":n.toggle=!0,n.label=\"enableCaptions\",n.labelPressed=\"disableCaptions\",n.icon=\"captions-off\",n.iconPressed=\"captions-on\";break;case\"fullscreen\":n.toggle=!0,n.label=\"enterFullscreen\",n.labelPressed=\"exitFullscreen\",n.icon=\"enter-fullscreen\",n.iconPressed=\"exit-fullscreen\";break;case\"play-large\":i.class+=` ${this.config.classNames.control}--overlaid`,s=\"play\",n.label=\"play\",n.icon=\"play\";break;default:is.empty(n.label)&&(n.label=s),is.empty(n.icon)&&(n.icon=e)}const r=createElement(n.element);return n.toggle?(r.appendChild(controls.createIcon.call(this,n.iconPressed,{class:\"icon--pressed\"})),r.appendChild(controls.createIcon.call(this,n.icon,{class:\"icon--not-pressed\"})),r.appendChild(controls.createLabel.call(this,n.labelPressed,{class:\"label--pressed\"})),r.appendChild(controls.createLabel.call(this,n.label,{class:\"label--not-pressed\"}))):(r.appendChild(controls.createIcon.call(this,n.icon)),r.appendChild(controls.createLabel.call(this,n.label))),extend(i,getAttributesFromSelector(this.config.selectors.buttons[s],i)),setAttributes(r,i),\"play\"===s?(is.array(this.elements.buttons[s])||(this.elements.buttons[s]=[]),this.elements.buttons[s].push(r)):this.elements.buttons[s]=r,r},createRange(e,t){const i=createElement(\"input\",extend(getAttributesFromSelector(this.config.selectors.inputs[e]),{type:\"range\",min:0,max:100,step:.01,value:0,autocomplete:\"off\",role:\"slider\",\"aria-label\":i18n.get(e,this.config),\"aria-valuemin\":0,\"aria-valuemax\":100,\"aria-valuenow\":0},t));return this.elements.inputs[e]=i,controls.updateRangeFill.call(this,i),RangeTouch.setup(i),i},createProgress(e,t){const i=createElement(\"progress\",extend(getAttributesFromSelector(this.config.selectors.display[e]),{min:0,max:100,value:0,role:\"progressbar\",\"aria-hidden\":!0},t));if(\"volume\"!==e){i.appendChild(createElement(\"span\",null,\"0\"));const t={played:\"played\",buffer:\"buffered\"}[e],s=t?i18n.get(t,this.config):\"\";i.innerText=`% ${s.toLowerCase()}`}return this.elements.display[e]=i,i},createTime(e,t){const i=getAttributesFromSelector(this.config.selectors.display[e],t),s=createElement(\"div\",extend(i,{class:`${i.class?i.class:\"\"} ${this.config.classNames.display.time} `.trim(),\"aria-label\":i18n.get(e,this.config),role:\"timer\"}),\"00:00\");return this.elements.display[e]=s,s},bindMenuItemShortcuts(e,t){on.call(this,e,\"keydown keyup\",(i=>{if(![\" \",\"ArrowUp\",\"ArrowDown\",\"ArrowRight\"].includes(i.key))return;if(i.preventDefault(),i.stopPropagation(),\"keydown\"===i.type)return;const s=matches(e,'[role=\"menuitemradio\"]');if(!s&&[\" \",\"ArrowRight\"].includes(i.key))controls.showMenuPanel.call(this,t,!0);else{let t;\" \"!==i.key&&(\"ArrowDown\"===i.key||s&&\"ArrowRight\"===i.key?(t=e.nextElementSibling,is.element(t)||(t=e.parentNode.firstElementChild)):(t=e.previousElementSibling,is.element(t)||(t=e.parentNode.lastElementChild)),setFocus.call(this,t,!0))}}),!1),on.call(this,e,\"keyup\",(e=>{\"Return\"===e.key&&controls.focusFirstMenuItem.call(this,null,!0)}))},createMenuItem({value:e,list:t,type:i,title:s,badge:n=null,checked:r=!1}){const a=getAttributesFromSelector(this.config.selectors.inputs[i]),o=createElement(\"button\",extend(a,{type:\"button\",role:\"menuitemradio\",class:`${this.config.classNames.control} ${a.class?a.class:\"\"}`.trim(),\"aria-checked\":r,value:e})),l=createElement(\"span\");l.innerHTML=s,is.element(n)&&l.appendChild(n),o.appendChild(l),Object.defineProperty(o,\"checked\",{enumerable:!0,get:()=>\"true\"===o.getAttribute(\"aria-checked\"),set(e){e&&Array.from(o.parentNode.children).filter((e=>matches(e,'[role=\"menuitemradio\"]'))).forEach((e=>e.setAttribute(\"aria-checked\",\"false\"))),o.setAttribute(\"aria-checked\",e?\"true\":\"false\")}}),this.listeners.bind(o,\"click keyup\",(t=>{if(!is.keyboardEvent(t)||\" \"===t.key){switch(t.preventDefault(),t.stopPropagation(),o.checked=!0,i){case\"language\":this.currentTrack=Number(e);break;case\"quality\":this.quality=e;break;case\"speed\":this.speed=parseFloat(e)}controls.showMenuPanel.call(this,\"home\",is.keyboardEvent(t))}}),i,!1),controls.bindMenuItemShortcuts.call(this,o,i),t.appendChild(o)},formatTime(e=0,t=!1){if(!is.number(e))return e;return formatTime(e,getHours(this.duration)>0,t)},updateTimeDisplay(e=null,t=0,i=!1){is.element(e)&&is.number(t)&&(e.innerText=controls.formatTime(t,i))},updateVolume(){this.supported.ui&&(is.element(this.elements.inputs.volume)&&controls.setRange.call(this,this.elements.inputs.volume,this.muted?0:this.volume),is.element(this.elements.buttons.mute)&&(this.elements.buttons.mute.pressed=this.muted||0===this.volume))},setRange(e,t=0){is.element(e)&&(e.value=t,controls.updateRangeFill.call(this,e))},updateProgress(e){if(!this.supported.ui||!is.event(e))return;let t=0;const i=(e,t)=>{const i=is.number(t)?t:0,s=is.element(e)?e:this.elements.display.buffer;if(is.element(s)){s.value=i;const e=s.getElementsByTagName(\"span\")[0];is.element(e)&&(e.childNodes[0].nodeValue=i)}};if(e)switch(e.type){case\"timeupdate\":case\"seeking\":case\"seeked\":t=getPercentage(this.currentTime,this.duration),\"timeupdate\"===e.type&&controls.setRange.call(this,this.elements.inputs.seek,t);break;case\"playing\":case\"progress\":i(this.elements.display.buffer,100*this.buffered)}},updateRangeFill(e){const t=is.event(e)?e.target:e;if(is.element(t)&&\"range\"===t.getAttribute(\"type\")){if(matches(t,this.config.selectors.inputs.seek)){t.setAttribute(\"aria-valuenow\",this.currentTime);const e=controls.formatTime(this.currentTime),i=controls.formatTime(this.duration),s=i18n.get(\"seekLabel\",this.config);t.setAttribute(\"aria-valuetext\",s.replace(\"{currentTime}\",e).replace(\"{duration}\",i))}else if(matches(t,this.config.selectors.inputs.volume)){const e=100*t.value;t.setAttribute(\"aria-valuenow\",e),t.setAttribute(\"aria-valuetext\",`${e.toFixed(1)}%`)}else t.setAttribute(\"aria-valuenow\",t.value);(browser.isWebKit||browser.isIPadOS)&&t.style.setProperty(\"--value\",t.value/t.max*100+\"%\")}},updateSeekTooltip(e){var t,i;if(!this.config.tooltips.seek||!is.element(this.elements.inputs.seek)||!is.element(this.elements.display.seekTooltip)||0===this.duration)return;const s=this.elements.display.seekTooltip,n=`${this.config.classNames.tooltip}--visible`,r=e=>toggleClass(s,n,e);if(this.touch)return void r(!1);let a=0;const o=this.elements.progress.getBoundingClientRect();if(is.event(e))a=100/o.width*(e.pageX-o.left);else{if(!hasClass(s,n))return;a=parseFloat(s.style.left,10)}a<0?a=0:a>100&&(a=100);const l=this.duration/100*a;s.innerText=controls.formatTime(l);const c=null===(t=this.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(l)));c&&s.insertAdjacentHTML(\"afterbegin\",`${c.label}<br>`),s.style.left=`${a}%`,is.event(e)&&[\"mouseenter\",\"mouseleave\"].includes(e.type)&&r(\"mouseenter\"===e.type)},timeUpdate(e){const t=!is.element(this.elements.display.duration)&&this.config.invertTime;controls.updateTimeDisplay.call(this,this.elements.display.currentTime,t?this.duration-this.currentTime:this.currentTime,t),e&&\"timeupdate\"===e.type&&this.media.seeking||controls.updateProgress.call(this,e)},durationUpdate(){if(!this.supported.ui||!this.config.invertTime&&this.currentTime)return;if(this.duration>=2**32)return toggleHidden(this.elements.display.currentTime,!0),void toggleHidden(this.elements.progress,!0);is.element(this.elements.inputs.seek)&&this.elements.inputs.seek.setAttribute(\"aria-valuemax\",this.duration);const e=is.element(this.elements.display.duration);!e&&this.config.displayDuration&&this.paused&&controls.updateTimeDisplay.call(this,this.elements.display.currentTime,this.duration),e&&controls.updateTimeDisplay.call(this,this.elements.display.duration,this.duration),this.config.markers.enabled&&controls.setMarkers.call(this),controls.updateSeekTooltip.call(this)},toggleMenuButton(e,t){toggleHidden(this.elements.settings.buttons[e],!t)},updateSetting(e,t,i){const s=this.elements.settings.panels[e];let n=null,r=t;if(\"captions\"===e)n=this.currentTrack;else{if(n=is.empty(i)?this[e]:i,is.empty(n)&&(n=this.config[e].default),!is.empty(this.options[e])&&!this.options[e].includes(n))return void this.debug.warn(`Unsupported value of '${n}' for ${e}`);if(!this.config[e].options.includes(n))return void this.debug.warn(`Disabled value of '${n}' for ${e}`)}if(is.element(r)||(r=s&&s.querySelector('[role=\"menu\"]')),!is.element(r))return;this.elements.settings.buttons[e].querySelector(`.${this.config.classNames.menu.value}`).innerHTML=controls.getLabel.call(this,e,n);const a=r&&r.querySelector(`[value=\"${n}\"]`);is.element(a)&&(a.checked=!0)},getLabel(e,t){switch(e){case\"speed\":return 1===t?i18n.get(\"normal\",this.config):`${t}&times;`;case\"quality\":if(is.number(t)){const e=i18n.get(`qualityLabel.${t}`,this.config);return e.length?e:`${t}p`}return toTitleCase(t);case\"captions\":return captions.getLabel.call(this);default:return null}},setQualityMenu(e){if(!is.element(this.elements.settings.panels.quality))return;const t=\"quality\",i=this.elements.settings.panels.quality.querySelector('[role=\"menu\"]');is.array(e)&&(this.options.quality=dedupe(e).filter((e=>this.config.quality.options.includes(e))));const s=!is.empty(this.options.quality)&&this.options.quality.length>1;if(controls.toggleMenuButton.call(this,t,s),emptyElement(i),controls.checkMenu.call(this),!s)return;const n=e=>{const t=i18n.get(`qualityBadge.${e}`,this.config);return t.length?controls.createBadge.call(this,t):null};this.options.quality.sort(((e,t)=>{const i=this.config.quality.options;return i.indexOf(e)>i.indexOf(t)?1:-1})).forEach((e=>{controls.createMenuItem.call(this,{value:e,list:i,type:t,title:controls.getLabel.call(this,\"quality\",e),badge:n(e)})})),controls.updateSetting.call(this,t,i)},setCaptionsMenu(){if(!is.element(this.elements.settings.panels.captions))return;const e=\"captions\",t=this.elements.settings.panels.captions.querySelector('[role=\"menu\"]'),i=captions.getTracks.call(this),s=Boolean(i.length);if(controls.toggleMenuButton.call(this,e,s),emptyElement(t),controls.checkMenu.call(this),!s)return;const n=i.map(((e,i)=>({value:i,checked:this.captions.toggled&&this.currentTrack===i,title:captions.getLabel.call(this,e),badge:e.language&&controls.createBadge.call(this,e.language.toUpperCase()),list:t,type:\"language\"})));n.unshift({value:-1,checked:!this.captions.toggled,title:i18n.get(\"disabled\",this.config),list:t,type:\"language\"}),n.forEach(controls.createMenuItem.bind(this)),controls.updateSetting.call(this,e,t)},setSpeedMenu(){if(!is.element(this.elements.settings.panels.speed))return;const e=\"speed\",t=this.elements.settings.panels.speed.querySelector('[role=\"menu\"]');this.options.speed=this.options.speed.filter((e=>e>=this.minimumSpeed&&e<=this.maximumSpeed));const i=!is.empty(this.options.speed)&&this.options.speed.length>1;controls.toggleMenuButton.call(this,e,i),emptyElement(t),controls.checkMenu.call(this),i&&(this.options.speed.forEach((i=>{controls.createMenuItem.call(this,{value:i,list:t,type:e,title:controls.getLabel.call(this,\"speed\",i)})})),controls.updateSetting.call(this,e,t))},checkMenu(){const{buttons:e}=this.elements.settings,t=!is.empty(e)&&Object.values(e).some((e=>!e.hidden));toggleHidden(this.elements.settings.menu,!t)},focusFirstMenuItem(e,t=!1){if(this.elements.settings.popup.hidden)return;let i=e;is.element(i)||(i=Object.values(this.elements.settings.panels).find((e=>!e.hidden)));const s=i.querySelector('[role^=\"menuitem\"]');setFocus.call(this,s,t)},toggleMenu(e){const{popup:t}=this.elements.settings,i=this.elements.buttons.settings;if(!is.element(t)||!is.element(i))return;const{hidden:s}=t;let n=s;if(is.boolean(e))n=e;else if(is.keyboardEvent(e)&&\"Escape\"===e.key)n=!1;else if(is.event(e)){const s=is.function(e.composedPath)?e.composedPath()[0]:e.target,r=t.contains(s);if(r||!r&&e.target!==i&&n)return}i.setAttribute(\"aria-expanded\",n),toggleHidden(t,!n),toggleClass(this.elements.container,this.config.classNames.menu.open,n),n&&is.keyboardEvent(e)?controls.focusFirstMenuItem.call(this,null,!0):n||s||setFocus.call(this,i,is.keyboardEvent(e))},getMenuSize(e){const t=e.cloneNode(!0);t.style.position=\"absolute\",t.style.opacity=0,t.removeAttribute(\"hidden\"),e.parentNode.appendChild(t);const i=t.scrollWidth,s=t.scrollHeight;return removeElement(t),{width:i,height:s}},showMenuPanel(e=\"\",t=!1){const i=this.elements.container.querySelector(`#plyr-settings-${this.id}-${e}`);if(!is.element(i))return;const s=i.parentNode,n=Array.from(s.children).find((e=>!e.hidden));if(support.transitions&&!support.reducedMotion){s.style.width=`${n.scrollWidth}px`,s.style.height=`${n.scrollHeight}px`;const e=controls.getMenuSize.call(this,i),t=e=>{e.target===s&&[\"width\",\"height\"].includes(e.propertyName)&&(s.style.width=\"\",s.style.height=\"\",off.call(this,s,transitionEndEvent,t))};on.call(this,s,transitionEndEvent,t),s.style.width=`${e.width}px`,s.style.height=`${e.height}px`}toggleHidden(n,!0),toggleHidden(i,!1),controls.focusFirstMenuItem.call(this,i,t)},setDownloadUrl(){const e=this.elements.buttons.download;is.element(e)&&e.setAttribute(\"href\",this.download)},create(e){const{bindMenuItemShortcuts:t,createButton:i,createProgress:s,createRange:n,createTime:r,setQualityMenu:a,setSpeedMenu:o,showMenuPanel:l}=controls;this.elements.controls=null,is.array(this.config.controls)&&this.config.controls.includes(\"play-large\")&&this.elements.container.appendChild(i.call(this,\"play-large\"));const c=createElement(\"div\",getAttributesFromSelector(this.config.selectors.controls.wrapper));this.elements.controls=c;const u={class:\"plyr__controls__item\"};return dedupe(is.array(this.config.controls)?this.config.controls:[]).forEach((a=>{if(\"restart\"===a&&c.appendChild(i.call(this,\"restart\",u)),\"rewind\"===a&&c.appendChild(i.call(this,\"rewind\",u)),\"play\"===a&&c.appendChild(i.call(this,\"play\",u)),\"fast-forward\"===a&&c.appendChild(i.call(this,\"fast-forward\",u)),\"progress\"===a){const t=createElement(\"div\",{class:`${u.class} plyr__progress__container`}),i=createElement(\"div\",getAttributesFromSelector(this.config.selectors.progress));if(i.appendChild(n.call(this,\"seek\",{id:`plyr-seek-${e.id}`})),i.appendChild(s.call(this,\"buffer\")),this.config.tooltips.seek){const e=createElement(\"span\",{class:this.config.classNames.tooltip},\"00:00\");i.appendChild(e),this.elements.display.seekTooltip=e}this.elements.progress=i,t.appendChild(this.elements.progress),c.appendChild(t)}if(\"current-time\"===a&&c.appendChild(r.call(this,\"currentTime\",u)),\"duration\"===a&&c.appendChild(r.call(this,\"duration\",u)),\"mute\"===a||\"volume\"===a){let{volume:t}=this.elements;if(is.element(t)&&c.contains(t)||(t=createElement(\"div\",extend({},u,{class:`${u.class} plyr__volume`.trim()})),this.elements.volume=t,c.appendChild(t)),\"mute\"===a&&t.appendChild(i.call(this,\"mute\")),\"volume\"===a&&!browser.isIos&&!browser.isIPadOS){const i={max:1,step:.05,value:this.config.volume};t.appendChild(n.call(this,\"volume\",extend(i,{id:`plyr-volume-${e.id}`})))}}if(\"captions\"===a&&c.appendChild(i.call(this,\"captions\",u)),\"settings\"===a&&!is.empty(this.config.settings)){const s=createElement(\"div\",extend({},u,{class:`${u.class} plyr__menu`.trim(),hidden:\"\"}));s.appendChild(i.call(this,\"settings\",{\"aria-haspopup\":!0,\"aria-controls\":`plyr-settings-${e.id}`,\"aria-expanded\":!1}));const n=createElement(\"div\",{class:\"plyr__menu__container\",id:`plyr-settings-${e.id}`,hidden:\"\"}),r=createElement(\"div\"),a=createElement(\"div\",{id:`plyr-settings-${e.id}-home`}),o=createElement(\"div\",{role:\"menu\"});a.appendChild(o),r.appendChild(a),this.elements.settings.panels.home=a,this.config.settings.forEach((i=>{const s=createElement(\"button\",extend(getAttributesFromSelector(this.config.selectors.buttons.settings),{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--forward`,role:\"menuitem\",\"aria-haspopup\":!0,hidden:\"\"}));t.call(this,s,i),on.call(this,s,\"click\",(()=>{l.call(this,i,!1)}));const n=createElement(\"span\",null,i18n.get(i,this.config)),a=createElement(\"span\",{class:this.config.classNames.menu.value});a.innerHTML=e[i],n.appendChild(a),s.appendChild(n),o.appendChild(s);const c=createElement(\"div\",{id:`plyr-settings-${e.id}-${i}`,hidden:\"\"}),u=createElement(\"button\",{type:\"button\",class:`${this.config.classNames.control} ${this.config.classNames.control}--back`});u.appendChild(createElement(\"span\",{\"aria-hidden\":!0},i18n.get(i,this.config))),u.appendChild(createElement(\"span\",{class:this.config.classNames.hidden},i18n.get(\"menuBack\",this.config))),on.call(this,c,\"keydown\",(e=>{\"ArrowLeft\"===e.key&&(e.preventDefault(),e.stopPropagation(),l.call(this,\"home\",!0))}),!1),on.call(this,u,\"click\",(()=>{l.call(this,\"home\",!1)})),c.appendChild(u),c.appendChild(createElement(\"div\",{role:\"menu\"})),r.appendChild(c),this.elements.settings.buttons[i]=s,this.elements.settings.panels[i]=c})),n.appendChild(r),s.appendChild(n),c.appendChild(s),this.elements.settings.popup=n,this.elements.settings.menu=s}if(\"pip\"===a&&support.pip&&c.appendChild(i.call(this,\"pip\",u)),\"airplay\"===a&&support.airplay&&c.appendChild(i.call(this,\"airplay\",u)),\"download\"===a){const e=extend({},u,{element:\"a\",href:this.download,target:\"_blank\"});this.isHTML5&&(e.download=\"\");const{download:t}=this.config.urls;!is.url(t)&&this.isEmbed&&extend(e,{icon:`logo-${this.provider}`,label:this.provider}),c.appendChild(i.call(this,\"download\",e))}\"fullscreen\"===a&&c.appendChild(i.call(this,\"fullscreen\",u))})),this.isHTML5&&a.call(this,html5.getQualityOptions.call(this)),o.call(this),c},inject(){if(this.config.loadSprite){const e=controls.getIconUrl.call(this);e.cors&&loadSprite(e.url,\"sprite-plyr\")}this.id=Math.floor(1e4*Math.random());let e=null;this.elements.controls=null;const t={id:this.id,seektime:this.config.seekTime,title:this.config.title};let i=!0;is.function(this.config.controls)&&(this.config.controls=this.config.controls.call(this,t)),this.config.controls||(this.config.controls=[]),is.element(this.config.controls)||is.string(this.config.controls)?e=this.config.controls:(e=controls.create.call(this,{id:this.id,seektime:this.config.seekTime,speed:this.speed,quality:this.quality,captions:captions.getLabel.call(this)}),i=!1);let s;i&&is.string(this.config.controls)&&(e=(e=>{let i=e;return Object.entries(t).forEach((([e,t])=>{i=replaceAll(i,`{${e}}`,t)})),i})(e)),is.string(this.config.selectors.controls.container)&&(s=document.querySelector(this.config.selectors.controls.container)),is.element(s)||(s=this.elements.container);if(s[is.element(e)?\"insertAdjacentElement\":\"insertAdjacentHTML\"](\"afterbegin\",e),is.element(this.elements.controls)||controls.findElements.call(this),!is.empty(this.elements.buttons)){const e=e=>{const t=this.config.classNames.controlPressed;e.setAttribute(\"aria-pressed\",\"false\"),Object.defineProperty(e,\"pressed\",{configurable:!0,enumerable:!0,get:()=>hasClass(e,t),set(i=!1){toggleClass(e,t,i),e.setAttribute(\"aria-pressed\",i?\"true\":\"false\")}})};Object.values(this.elements.buttons).filter(Boolean).forEach((t=>{is.array(t)||is.nodeList(t)?Array.from(t).filter(Boolean).forEach(e):e(t)}))}if(browser.isEdge&&repaint(s),this.config.tooltips.controls){const{classNames:e,selectors:t}=this.config,i=`${t.controls.wrapper} ${t.labels} .${e.hidden}`,s=getElements.call(this,i);Array.from(s).forEach((e=>{toggleClass(e,this.config.classNames.hidden,!1),toggleClass(e,this.config.classNames.tooltip,!0)}))}},setMediaMetadata(){try{\"mediaSession\"in navigator&&(navigator.mediaSession.metadata=new window.MediaMetadata({title:this.config.mediaMetadata.title,artist:this.config.mediaMetadata.artist,album:this.config.mediaMetadata.album,artwork:this.config.mediaMetadata.artwork}))}catch(e){}},setMarkers(){var e,t;if(!this.duration||this.elements.markers)return;const i=null===(e=this.config.markers)||void 0===e||null===(t=e.points)||void 0===t?void 0:t.filter((({time:e})=>e>0&&e<this.duration));if(null==i||!i.length)return;const s=document.createDocumentFragment(),n=document.createDocumentFragment();let r=null;const a=`${this.config.classNames.tooltip}--visible`,o=e=>toggleClass(r,a,e);i.forEach((e=>{const t=createElement(\"span\",{class:this.config.classNames.marker},\"\"),i=e.time/this.duration*100+\"%\";r&&(t.addEventListener(\"mouseenter\",(()=>{e.label||(r.style.left=i,r.innerHTML=e.label,o(!0))})),t.addEventListener(\"mouseleave\",(()=>{o(!1)}))),t.addEventListener(\"click\",(()=>{this.currentTime=e.time})),t.style.left=i,n.appendChild(t)})),s.appendChild(n),this.config.tooltips.seek||(r=createElement(\"span\",{class:this.config.classNames.tooltip},\"\"),s.appendChild(r)),this.elements.markers={points:n,tip:r},this.elements.progress.appendChild(s)}};function parseUrl(e,t=!0){let i=e;if(t){const e=document.createElement(\"a\");e.href=i,i=e.href}try{return new URL(i)}catch(e){return null}}function buildUrlParams(e){const t=new URLSearchParams;return is.object(e)&&Object.entries(e).forEach((([e,i])=>{t.set(e,i)})),t}const captions={setup(){if(!this.supported.ui)return;if(!this.isVideo||this.isYouTube||this.isHTML5&&!support.textTracks)return void(is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this));if(is.element(this.elements.captions)||(this.elements.captions=createElement(\"div\",getAttributesFromSelector(this.config.selectors.captions)),this.elements.captions.setAttribute(\"dir\",\"auto\"),insertAfter(this.elements.captions,this.elements.wrapper)),browser.isIE&&window.URL){const e=this.media.querySelectorAll(\"track\");Array.from(e).forEach((e=>{const t=e.getAttribute(\"src\"),i=parseUrl(t);null!==i&&i.hostname!==window.location.href.hostname&&[\"http:\",\"https:\"].includes(i.protocol)&&fetch(t,\"blob\").then((t=>{e.setAttribute(\"src\",window.URL.createObjectURL(t))})).catch((()=>{removeElement(e)}))}))}const e=dedupe((navigator.languages||[navigator.language||navigator.userLanguage||\"en\"]).map((e=>e.split(\"-\")[0])));let t=(this.storage.get(\"language\")||this.config.captions.language||\"auto\").toLowerCase();\"auto\"===t&&([t]=e);let i=this.storage.get(\"captions\");if(is.boolean(i)||({active:i}=this.config.captions),Object.assign(this.captions,{toggled:!1,active:i,language:t,languages:e}),this.isHTML5){const e=this.config.captions.update?\"addtrack removetrack\":\"removetrack\";on.call(this,this.media.textTracks,e,captions.update.bind(this))}setTimeout(captions.update.bind(this),0)},update(){const e=captions.getTracks.call(this,!0),{active:t,language:i,meta:s,currentTrackNode:n}=this.captions,r=Boolean(e.find((e=>e.language===i)));this.isHTML5&&this.isVideo&&e.filter((e=>!s.get(e))).forEach((e=>{this.debug.log(\"Track added\",e),s.set(e,{default:\"showing\"===e.mode}),\"showing\"===e.mode&&(e.mode=\"hidden\"),on.call(this,e,\"cuechange\",(()=>captions.updateCues.call(this)))})),(r&&this.language!==i||!e.includes(n))&&(captions.setLanguage.call(this,i),captions.toggle.call(this,t&&r)),this.elements&&toggleClass(this.elements.container,this.config.classNames.captions.enabled,!is.empty(e)),is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&this.config.settings.includes(\"captions\")&&controls.setCaptionsMenu.call(this)},toggle(e,t=!0){if(!this.supported.ui)return;const{toggled:i}=this.captions,s=this.config.classNames.captions.active,n=is.nullOrUndefined(e)?!i:e;if(n!==i){if(t||(this.captions.active=n,this.storage.set({captions:n})),!this.language&&n&&!t){const e=captions.getTracks.call(this),t=captions.findTrack.call(this,[this.captions.language,...this.captions.languages],!0);return this.captions.language=t.language,void captions.set.call(this,e.indexOf(t))}this.elements.buttons.captions&&(this.elements.buttons.captions.pressed=n),toggleClass(this.elements.container,s,n),this.captions.toggled=n,controls.updateSetting.call(this,\"captions\"),triggerEvent.call(this,this.media,n?\"captionsenabled\":\"captionsdisabled\")}setTimeout((()=>{n&&this.captions.toggled&&(this.captions.currentTrackNode.mode=\"hidden\")}))},set(e,t=!0){const i=captions.getTracks.call(this);if(-1!==e)if(is.number(e))if(e in i){if(this.captions.currentTrack!==e){this.captions.currentTrack=e;const s=i[e],{language:n}=s||{};this.captions.currentTrackNode=s,controls.updateSetting.call(this,\"captions\"),t||(this.captions.language=n,this.storage.set({language:n})),this.isVimeo&&this.embed.enableTextTrack(n),triggerEvent.call(this,this.media,\"languagechange\")}captions.toggle.call(this,!0,t),this.isHTML5&&this.isVideo&&captions.updateCues.call(this)}else this.debug.warn(\"Track not found\",e);else this.debug.warn(\"Invalid caption argument\",e);else captions.toggle.call(this,!1,t)},setLanguage(e,t=!0){if(!is.string(e))return void this.debug.warn(\"Invalid language argument\",e);const i=e.toLowerCase();this.captions.language=i;const s=captions.getTracks.call(this),n=captions.findTrack.call(this,[i]);captions.set.call(this,s.indexOf(n),t)},getTracks(e=!1){return Array.from((this.media||{}).textTracks||[]).filter((t=>!this.isHTML5||e||this.captions.meta.has(t))).filter((e=>[\"captions\",\"subtitles\"].includes(e.kind)))},findTrack(e,t=!1){const i=captions.getTracks.call(this),s=e=>Number((this.captions.meta.get(e)||{}).default),n=Array.from(i).sort(((e,t)=>s(t)-s(e)));let r;return e.every((e=>(r=n.find((t=>t.language===e)),!r))),r||(t?n[0]:void 0)},getCurrentTrack(){return captions.getTracks.call(this)[this.currentTrack]},getLabel(e){let t=e;return!is.track(t)&&support.textTracks&&this.captions.toggled&&(t=captions.getCurrentTrack.call(this)),is.track(t)?is.empty(t.label)?is.empty(t.language)?i18n.get(\"enabled\",this.config):e.language.toUpperCase():t.label:i18n.get(\"disabled\",this.config)},updateCues(e){if(!this.supported.ui)return;if(!is.element(this.elements.captions))return void this.debug.warn(\"No captions element to render to\");if(!is.nullOrUndefined(e)&&!Array.isArray(e))return void this.debug.warn(\"updateCues: Invalid input\",e);let t=e;if(!t){const e=captions.getCurrentTrack.call(this);t=Array.from((e||{}).activeCues||[]).map((e=>e.getCueAsHTML())).map(getHTML)}const i=t.map((e=>e.trim())).join(\"\\n\");if(i!==this.elements.captions.innerHTML){emptyElement(this.elements.captions);const e=createElement(\"span\",getAttributesFromSelector(this.config.selectors.caption));e.innerHTML=i,this.elements.captions.appendChild(e),triggerEvent.call(this,this.media,\"cuechange\")}}},defaults={enabled:!0,title:\"\",debug:!1,autoplay:!1,autopause:!0,playsinline:!0,seekTime:10,volume:1,muted:!1,duration:null,displayDuration:!0,invertTime:!0,toggleInvert:!0,ratio:null,clickToPlay:!0,hideControls:!0,resetOnEnd:!1,disableContextMenu:!0,loadSprite:!0,iconPrefix:\"plyr\",iconUrl:\"https://cdn.plyr.io/3.7.8/plyr.svg\",blankVideo:\"https://cdn.plyr.io/static/blank.mp4\",quality:{default:576,options:[4320,2880,2160,1440,1080,720,576,480,360,240],forced:!1,onChange:null},loop:{active:!1},speed:{selected:1,options:[.5,.75,1,1.25,1.5,1.75,2,4]},keyboard:{focused:!0,global:!1},tooltips:{controls:!1,seek:!0},captions:{active:!1,language:\"auto\",update:!1},fullscreen:{enabled:!0,fallback:!0,iosNative:!1},storage:{enabled:!0,key:\"plyr\"},controls:[\"play-large\",\"play\",\"progress\",\"current-time\",\"mute\",\"volume\",\"captions\",\"settings\",\"pip\",\"airplay\",\"fullscreen\"],settings:[\"captions\",\"quality\",\"speed\"],i18n:{restart:\"Restart\",rewind:\"Rewind {seektime}s\",play:\"Play\",pause:\"Pause\",fastForward:\"Forward {seektime}s\",seek:\"Seek\",seekLabel:\"{currentTime} of {duration}\",played:\"Played\",buffered:\"Buffered\",currentTime:\"Current time\",duration:\"Duration\",volume:\"Volume\",mute:\"Mute\",unmute:\"Unmute\",enableCaptions:\"Enable captions\",disableCaptions:\"Disable captions\",download:\"Download\",enterFullscreen:\"Enter fullscreen\",exitFullscreen:\"Exit fullscreen\",frameTitle:\"Player for {title}\",captions:\"Captions\",settings:\"Settings\",pip:\"PIP\",menuBack:\"Go back to previous menu\",speed:\"Speed\",normal:\"Normal\",quality:\"Quality\",loop:\"Loop\",start:\"Start\",end:\"End\",all:\"All\",reset:\"Reset\",disabled:\"Disabled\",enabled:\"Enabled\",advertisement:\"Ad\",qualityBadge:{2160:\"4K\",1440:\"HD\",1080:\"HD\",720:\"HD\",576:\"SD\",480:\"SD\"}},urls:null,listeners:{seek:null,play:null,pause:null,restart:null,rewind:null,fastForward:null,mute:null,volume:null,captions:null,download:null,fullscreen:null,pip:null,airplay:null,speed:null,quality:null,loop:null,language:null},events:[\"ended\",\"progress\",\"stalled\",\"playing\",\"waiting\",\"canplay\",\"canplaythrough\",\"loadstart\",\"loadeddata\",\"loadedmetadata\",\"timeupdate\",\"volumechange\",\"play\",\"pause\",\"error\",\"seeking\",\"seeked\",\"emptied\",\"ratechange\",\"cuechange\",\"download\",\"enterfullscreen\",\"exitfullscreen\",\"captionsenabled\",\"captionsdisabled\",\"languagechange\",\"controlshidden\",\"controlsshown\",\"ready\",\"statechange\",\"qualitychange\",\"adsloaded\",\"adscontentpause\",\"adscontentresume\",\"adstarted\",\"adsmidpoint\",\"adscomplete\",\"adsallcomplete\",\"adsimpression\",\"adsclick\"],selectors:{editable:\"input, textarea, select, [contenteditable]\",container:\".plyr\",controls:{container:null,wrapper:\".plyr__controls\"},labels:\"[data-plyr]\",buttons:{play:'[data-plyr=\"play\"]',pause:'[data-plyr=\"pause\"]',restart:'[data-plyr=\"restart\"]',rewind:'[data-plyr=\"rewind\"]',fastForward:'[data-plyr=\"fast-forward\"]',mute:'[data-plyr=\"mute\"]',captions:'[data-plyr=\"captions\"]',download:'[data-plyr=\"download\"]',fullscreen:'[data-plyr=\"fullscreen\"]',pip:'[data-plyr=\"pip\"]',airplay:'[data-plyr=\"airplay\"]',settings:'[data-plyr=\"settings\"]',loop:'[data-plyr=\"loop\"]'},inputs:{seek:'[data-plyr=\"seek\"]',volume:'[data-plyr=\"volume\"]',speed:'[data-plyr=\"speed\"]',language:'[data-plyr=\"language\"]',quality:'[data-plyr=\"quality\"]'},display:{currentTime:\".plyr__time--current\",duration:\".plyr__time--duration\",buffer:\".plyr__progress__buffer\",loop:\".plyr__progress__loop\",volume:\".plyr__volume--display\"},progress:\".plyr__progress\",captions:\".plyr__captions\",caption:\".plyr__caption\"},classNames:{type:\"plyr--{0}\",provider:\"plyr--{0}\",video:\"plyr__video-wrapper\",embed:\"plyr__video-embed\",videoFixedRatio:\"plyr__video-wrapper--fixed-ratio\",embedContainer:\"plyr__video-embed__container\",poster:\"plyr__poster\",posterEnabled:\"plyr__poster-enabled\",ads:\"plyr__ads\",control:\"plyr__control\",controlPressed:\"plyr__control--pressed\",playing:\"plyr--playing\",paused:\"plyr--paused\",stopped:\"plyr--stopped\",loading:\"plyr--loading\",hover:\"plyr--hover\",tooltip:\"plyr__tooltip\",cues:\"plyr__cues\",marker:\"plyr__progress__marker\",hidden:\"plyr__sr-only\",hideControls:\"plyr--hide-controls\",isTouch:\"plyr--is-touch\",uiSupported:\"plyr--full-ui\",noTransition:\"plyr--no-transition\",display:{time:\"plyr__time\"},menu:{value:\"plyr__menu__value\",badge:\"plyr__badge\",open:\"plyr--menu-open\"},captions:{enabled:\"plyr--captions-enabled\",active:\"plyr--captions-active\"},fullscreen:{enabled:\"plyr--fullscreen-enabled\",fallback:\"plyr--fullscreen-fallback\"},pip:{supported:\"plyr--pip-supported\",active:\"plyr--pip-active\"},airplay:{supported:\"plyr--airplay-supported\",active:\"plyr--airplay-active\"},previewThumbnails:{thumbContainer:\"plyr__preview-thumb\",thumbContainerShown:\"plyr__preview-thumb--is-shown\",imageContainer:\"plyr__preview-thumb__image-container\",timeContainer:\"plyr__preview-thumb__time-container\",scrubbingContainer:\"plyr__preview-scrubbing\",scrubbingContainerShown:\"plyr__preview-scrubbing--is-shown\"}},attributes:{embed:{provider:\"data-plyr-provider\",id:\"data-plyr-embed-id\",hash:\"data-plyr-embed-hash\"}},ads:{enabled:!1,publisherId:\"\",tagUrl:\"\"},previewThumbnails:{enabled:!1,src:\"\"},vimeo:{byline:!1,portrait:!1,title:!1,speed:!0,transparent:!1,customControls:!0,referrerPolicy:null,premium:!1},youtube:{rel:0,showinfo:0,iv_load_policy:3,modestbranding:1,customControls:!0,noCookie:!1},mediaMetadata:{title:\"\",artist:\"\",album:\"\",artwork:[]},markers:{enabled:!1,points:[]}},pip={active:\"picture-in-picture\",inactive:\"inline\"},providers={html5:\"html5\",youtube:\"youtube\",vimeo:\"vimeo\"},types={audio:\"audio\",video:\"video\"};function getProviderByUrl(e){return/^(https?:\\/\\/)?(www\\.)?(youtube\\.com|youtube-nocookie\\.com|youtu\\.?be)\\/.+$/.test(e)?providers.youtube:/^https?:\\/\\/player.vimeo.com\\/video\\/\\d{0,9}(?=\\b|\\/)/.test(e)?providers.vimeo:null}const noop=()=>{};class Console{constructor(e=!1){this.enabled=window.console&&e,this.enabled&&this.log(\"Debugging enabled\")}get log(){return this.enabled?Function.prototype.bind.call(console.log,console):noop}get warn(){return this.enabled?Function.prototype.bind.call(console.warn,console):noop}get error(){return this.enabled?Function.prototype.bind.call(console.error,console):noop}}class Fullscreen{constructor(e){_defineProperty$1(this,\"onChange\",(()=>{if(!this.supported)return;const e=this.player.elements.buttons.fullscreen;is.element(e)&&(e.pressed=this.active);const t=this.target===this.player.media?this.target:this.player.elements.container;triggerEvent.call(this.player,t,this.active?\"enterfullscreen\":\"exitfullscreen\",!0)})),_defineProperty$1(this,\"toggleFallback\",((e=!1)=>{if(e?this.scrollPosition={x:window.scrollX??0,y:window.scrollY??0}:window.scrollTo(this.scrollPosition.x,this.scrollPosition.y),document.body.style.overflow=e?\"hidden\":\"\",toggleClass(this.target,this.player.config.classNames.fullscreen.fallback,e),browser.isIos){let t=document.head.querySelector('meta[name=\"viewport\"]');const i=\"viewport-fit=cover\";t||(t=document.createElement(\"meta\"),t.setAttribute(\"name\",\"viewport\"));const s=is.string(t.content)&&t.content.includes(i);e?(this.cleanupViewport=!s,s||(t.content+=`,${i}`)):this.cleanupViewport&&(t.content=t.content.split(\",\").filter((e=>e.trim()!==i)).join(\",\"))}this.onChange()})),_defineProperty$1(this,\"trapFocus\",(e=>{if(browser.isIos||browser.isIPadOS||!this.active||\"Tab\"!==e.key)return;const t=document.activeElement,i=getElements.call(this.player,\"a[href], button:not(:disabled), input:not(:disabled), [tabindex]\"),[s]=i,n=i[i.length-1];t!==n||e.shiftKey?t===s&&e.shiftKey&&(n.focus(),e.preventDefault()):(s.focus(),e.preventDefault())})),_defineProperty$1(this,\"update\",(()=>{if(this.supported){let e;e=this.forceFallback?\"Fallback (forced)\":Fullscreen.nativeSupported?\"Native\":\"Fallback\",this.player.debug.log(`${e} fullscreen enabled`)}else this.player.debug.log(\"Fullscreen not supported and fallback disabled\");toggleClass(this.player.elements.container,this.player.config.classNames.fullscreen.enabled,this.supported)})),_defineProperty$1(this,\"enter\",(()=>{this.supported&&(browser.isIos&&this.player.config.fullscreen.iosNative?this.player.isVimeo?this.player.embed.requestFullscreen():this.target.webkitEnterFullscreen():!Fullscreen.nativeSupported||this.forceFallback?this.toggleFallback(!0):this.prefix?is.empty(this.prefix)||this.target[`${this.prefix}Request${this.property}`]():this.target.requestFullscreen({navigationUI:\"hide\"}))})),_defineProperty$1(this,\"exit\",(()=>{if(this.supported)if(browser.isIos&&this.player.config.fullscreen.iosNative)this.player.isVimeo?this.player.embed.exitFullscreen():this.target.webkitEnterFullscreen(),silencePromise(this.player.play());else if(!Fullscreen.nativeSupported||this.forceFallback)this.toggleFallback(!1);else if(this.prefix){if(!is.empty(this.prefix)){const e=\"moz\"===this.prefix?\"Cancel\":\"Exit\";document[`${this.prefix}${e}${this.property}`]()}}else(document.cancelFullScreen||document.exitFullscreen).call(document)})),_defineProperty$1(this,\"toggle\",(()=>{this.active?this.exit():this.enter()})),this.player=e,this.prefix=Fullscreen.prefix,this.property=Fullscreen.property,this.scrollPosition={x:0,y:0},this.forceFallback=\"force\"===e.config.fullscreen.fallback,this.player.elements.fullscreen=e.config.fullscreen.container&&closest$1(this.player.elements.container,e.config.fullscreen.container),on.call(this.player,document,\"ms\"===this.prefix?\"MSFullscreenChange\":`${this.prefix}fullscreenchange`,(()=>{this.onChange()})),on.call(this.player,this.player.elements.container,\"dblclick\",(e=>{is.element(this.player.elements.controls)&&this.player.elements.controls.contains(e.target)||this.player.listeners.proxy(e,this.toggle,\"fullscreen\")})),on.call(this,this.player.elements.container,\"keydown\",(e=>this.trapFocus(e))),this.update()}static get nativeSupported(){return!!(document.fullscreenEnabled||document.webkitFullscreenEnabled||document.mozFullScreenEnabled||document.msFullscreenEnabled)}get useNative(){return Fullscreen.nativeSupported&&!this.forceFallback}static get prefix(){if(is.function(document.exitFullscreen))return\"\";let e=\"\";return[\"webkit\",\"moz\",\"ms\"].some((t=>!(!is.function(document[`${t}ExitFullscreen`])&&!is.function(document[`${t}CancelFullScreen`]))&&(e=t,!0))),e}static get property(){return\"moz\"===this.prefix?\"FullScreen\":\"Fullscreen\"}get supported(){return[this.player.config.fullscreen.enabled,this.player.isVideo,Fullscreen.nativeSupported||this.player.config.fullscreen.fallback,!this.player.isYouTube||Fullscreen.nativeSupported||!browser.isIos||this.player.config.playsinline&&!this.player.config.fullscreen.iosNative].every(Boolean)}get active(){if(!this.supported)return!1;if(!Fullscreen.nativeSupported||this.forceFallback)return hasClass(this.target,this.player.config.classNames.fullscreen.fallback);const e=this.prefix?this.target.getRootNode()[`${this.prefix}${this.property}Element`]:this.target.getRootNode().fullscreenElement;return e&&e.shadowRoot?e===this.target.getRootNode().host:e===this.target}get target(){return browser.isIos&&this.player.config.fullscreen.iosNative?this.player.media:this.player.elements.fullscreen??this.player.elements.container}}function loadImage(e,t=1){return new Promise(((i,s)=>{const n=new Image,r=()=>{delete n.onload,delete n.onerror,(n.naturalWidth>=t?i:s)(n)};Object.assign(n,{onload:r,onerror:r,src:e})}))}const ui={addStyleHook(){toggleClass(this.elements.container,this.config.selectors.container.replace(\".\",\"\"),!0),toggleClass(this.elements.container,this.config.classNames.uiSupported,this.supported.ui)},toggleNativeControls(e=!1){e&&this.isHTML5?this.media.setAttribute(\"controls\",\"\"):this.media.removeAttribute(\"controls\")},build(){if(this.listeners.media(),!this.supported.ui)return this.debug.warn(`Basic support only for ${this.provider} ${this.type}`),void ui.toggleNativeControls.call(this,!0);is.element(this.elements.controls)||(controls.inject.call(this),this.listeners.controls()),ui.toggleNativeControls.call(this),this.isHTML5&&captions.setup.call(this),this.volume=null,this.muted=null,this.loop=null,this.quality=null,this.speed=null,controls.updateVolume.call(this),controls.timeUpdate.call(this),controls.durationUpdate.call(this),ui.checkPlaying.call(this),toggleClass(this.elements.container,this.config.classNames.pip.supported,support.pip&&this.isHTML5&&this.isVideo),toggleClass(this.elements.container,this.config.classNames.airplay.supported,support.airplay&&this.isHTML5),toggleClass(this.elements.container,this.config.classNames.isTouch,this.touch),this.ready=!0,setTimeout((()=>{triggerEvent.call(this,this.media,\"ready\")}),0),ui.setTitle.call(this),this.poster&&ui.setPoster.call(this,this.poster,!1).catch((()=>{})),this.config.duration&&controls.durationUpdate.call(this),this.config.mediaMetadata&&controls.setMediaMetadata.call(this)},setTitle(){let e=i18n.get(\"play\",this.config);if(is.string(this.config.title)&&!is.empty(this.config.title)&&(e+=`, ${this.config.title}`),Array.from(this.elements.buttons.play||[]).forEach((t=>{t.setAttribute(\"aria-label\",e)})),this.isEmbed){const e=getElement.call(this,\"iframe\");if(!is.element(e))return;const t=is.empty(this.config.title)?\"video\":this.config.title,i=i18n.get(\"frameTitle\",this.config);e.setAttribute(\"title\",i.replace(\"{title}\",t))}},togglePoster(e){toggleClass(this.elements.container,this.config.classNames.posterEnabled,e)},setPoster(e,t=!0){return t&&this.poster?Promise.reject(new Error(\"Poster already set\")):(this.media.setAttribute(\"data-poster\",e),this.elements.poster.removeAttribute(\"hidden\"),ready.call(this).then((()=>loadImage(e))).catch((t=>{throw e===this.poster&&ui.togglePoster.call(this,!1),t})).then((()=>{if(e!==this.poster)throw new Error(\"setPoster cancelled by later call to setPoster\")})).then((()=>(Object.assign(this.elements.poster.style,{backgroundImage:`url('${e}')`,backgroundSize:\"\"}),ui.togglePoster.call(this,!0),e))))},checkPlaying(e){toggleClass(this.elements.container,this.config.classNames.playing,this.playing),toggleClass(this.elements.container,this.config.classNames.paused,this.paused),toggleClass(this.elements.container,this.config.classNames.stopped,this.stopped),Array.from(this.elements.buttons.play||[]).forEach((e=>{Object.assign(e,{pressed:this.playing}),e.setAttribute(\"aria-label\",i18n.get(this.playing?\"pause\":\"play\",this.config))})),is.event(e)&&\"timeupdate\"===e.type||ui.toggleControls.call(this)},checkLoading(e){this.loading=[\"stalled\",\"waiting\"].includes(e.type),clearTimeout(this.timers.loading),this.timers.loading=setTimeout((()=>{toggleClass(this.elements.container,this.config.classNames.loading,this.loading),ui.toggleControls.call(this)}),this.loading?250:0)},toggleControls(e){const{controls:t}=this.elements;if(t&&this.config.hideControls){const i=this.touch&&this.lastSeekTime+2e3>Date.now();this.toggleControls(Boolean(e||this.loading||this.paused||t.pressed||t.hover||i))}},migrateStyles(){Object.values({...this.media.style}).filter((e=>!is.empty(e)&&is.string(e)&&e.startsWith(\"--plyr\"))).forEach((e=>{this.elements.container.style.setProperty(e,this.media.style.getPropertyValue(e)),this.media.style.removeProperty(e)})),is.empty(this.media.style)&&this.media.removeAttribute(\"style\")}};class Listeners{constructor(e){_defineProperty$1(this,\"firstTouch\",(()=>{const{player:e}=this,{elements:t}=e;e.touch=!0,toggleClass(t.container,e.config.classNames.isTouch,!0)})),_defineProperty$1(this,\"global\",((e=!0)=>{const{player:t}=this;t.config.keyboard.global&&toggleListener.call(t,window,\"keydown keyup\",this.handleKey,e,!1),toggleListener.call(t,document.body,\"click\",this.toggleMenu,e),once.call(t,document.body,\"touchstart\",this.firstTouch)})),_defineProperty$1(this,\"container\",(()=>{const{player:e}=this,{config:t,elements:i,timers:s}=e;!t.keyboard.global&&t.keyboard.focused&&on.call(e,i.container,\"keydown keyup\",this.handleKey,!1),on.call(e,i.container,\"mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen\",(t=>{const{controls:n}=i;n&&\"enterfullscreen\"===t.type&&(n.pressed=!1,n.hover=!1);let r=0;[\"touchstart\",\"touchmove\",\"mousemove\"].includes(t.type)&&(ui.toggleControls.call(e,!0),r=e.touch?3e3:2e3),clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),r)}));const n=()=>{if(!e.isVimeo||e.config.vimeo.premium)return;const t=i.wrapper,{active:s}=e.fullscreen,[n,r]=getAspectRatio.call(e),a=supportsCSS(`aspect-ratio: ${n} / ${r}`);if(!s)return void(a?(t.style.width=null,t.style.height=null):(t.style.maxWidth=null,t.style.margin=null));const[o,l]=getViewportSize(),c=o/l>n/r;a?(t.style.width=c?\"auto\":\"100%\",t.style.height=c?\"100%\":\"auto\"):(t.style.maxWidth=c?l/r*n+\"px\":null,t.style.margin=c?\"0 auto\":null)},r=()=>{clearTimeout(s.resized),s.resized=setTimeout(n,50)};on.call(e,i.container,\"enterfullscreen exitfullscreen\",(t=>{const{target:s}=e.fullscreen;if(s!==i.container)return;if(!e.isEmbed&&is.empty(e.config.ratio))return;n();(\"enterfullscreen\"===t.type?on:off).call(e,window,\"resize\",r)}))})),_defineProperty$1(this,\"media\",(()=>{const{player:e}=this,{elements:t}=e;if(on.call(e,e.media,\"timeupdate seeking seeked\",(t=>controls.timeUpdate.call(e,t))),on.call(e,e.media,\"durationchange loadeddata loadedmetadata\",(t=>controls.durationUpdate.call(e,t))),on.call(e,e.media,\"ended\",(()=>{e.isHTML5&&e.isVideo&&e.config.resetOnEnd&&(e.restart(),e.pause())})),on.call(e,e.media,\"progress playing seeking seeked\",(t=>controls.updateProgress.call(e,t))),on.call(e,e.media,\"volumechange\",(t=>controls.updateVolume.call(e,t))),on.call(e,e.media,\"playing play pause ended emptied timeupdate\",(t=>ui.checkPlaying.call(e,t))),on.call(e,e.media,\"waiting canplay seeked playing\",(t=>ui.checkLoading.call(e,t))),e.supported.ui&&e.config.clickToPlay&&!e.isAudio){const i=getElement.call(e,`.${e.config.classNames.video}`);if(!is.element(i))return;on.call(e,t.container,\"click\",(s=>{([t.container,i].includes(s.target)||i.contains(s.target))&&(e.touch&&e.config.hideControls||(e.ended?(this.proxy(s,e.restart,\"restart\"),this.proxy(s,(()=>{silencePromise(e.play())}),\"play\")):this.proxy(s,(()=>{silencePromise(e.togglePlay())}),\"play\")))}))}e.supported.ui&&e.config.disableContextMenu&&on.call(e,t.wrapper,\"contextmenu\",(e=>{e.preventDefault()}),!1),on.call(e,e.media,\"volumechange\",(()=>{e.storage.set({volume:e.volume,muted:e.muted})})),on.call(e,e.media,\"ratechange\",(()=>{controls.updateSetting.call(e,\"speed\"),e.storage.set({speed:e.speed})})),on.call(e,e.media,\"qualitychange\",(t=>{controls.updateSetting.call(e,\"quality\",null,t.detail.quality)})),on.call(e,e.media,\"ready qualitychange\",(()=>{controls.setDownloadUrl.call(e)}));const i=e.config.events.concat([\"keyup\",\"keydown\"]).join(\" \");on.call(e,e.media,i,(i=>{let{detail:s={}}=i;\"error\"===i.type&&(s=e.media.error),triggerEvent.call(e,t.container,i.type,!0,s)}))})),_defineProperty$1(this,\"proxy\",((e,t,i)=>{const{player:s}=this,n=s.config.listeners[i];let r=!0;is.function(n)&&(r=n.call(s,e)),!1!==r&&is.function(t)&&t.call(s,e)})),_defineProperty$1(this,\"bind\",((e,t,i,s,n=!0)=>{const{player:r}=this,a=r.config.listeners[s],o=is.function(a);on.call(r,e,t,(e=>this.proxy(e,i,s)),n&&!o)})),_defineProperty$1(this,\"controls\",(()=>{const{player:e}=this,{elements:t}=e,i=browser.isIE?\"change\":\"input\";if(t.buttons.play&&Array.from(t.buttons.play).forEach((t=>{this.bind(t,\"click\",(()=>{silencePromise(e.togglePlay())}),\"play\")})),this.bind(t.buttons.restart,\"click\",e.restart,\"restart\"),this.bind(t.buttons.rewind,\"click\",(()=>{e.lastSeekTime=Date.now(),e.rewind()}),\"rewind\"),this.bind(t.buttons.fastForward,\"click\",(()=>{e.lastSeekTime=Date.now(),e.forward()}),\"fastForward\"),this.bind(t.buttons.mute,\"click\",(()=>{e.muted=!e.muted}),\"mute\"),this.bind(t.buttons.captions,\"click\",(()=>e.toggleCaptions())),this.bind(t.buttons.download,\"click\",(()=>{triggerEvent.call(e,e.media,\"download\")}),\"download\"),this.bind(t.buttons.fullscreen,\"click\",(()=>{e.fullscreen.toggle()}),\"fullscreen\"),this.bind(t.buttons.pip,\"click\",(()=>{e.pip=\"toggle\"}),\"pip\"),this.bind(t.buttons.airplay,\"click\",e.airplay,\"airplay\"),this.bind(t.buttons.settings,\"click\",(t=>{t.stopPropagation(),t.preventDefault(),controls.toggleMenu.call(e,t)}),null,!1),this.bind(t.buttons.settings,\"keyup\",(t=>{[\" \",\"Enter\"].includes(t.key)&&(\"Enter\"!==t.key?(t.preventDefault(),t.stopPropagation(),controls.toggleMenu.call(e,t)):controls.focusFirstMenuItem.call(e,null,!0))}),null,!1),this.bind(t.settings.menu,\"keydown\",(t=>{\"Escape\"===t.key&&controls.toggleMenu.call(e,t)})),this.bind(t.inputs.seek,\"mousedown mousemove\",(e=>{const i=t.progress.getBoundingClientRect(),s=100/i.width*(e.pageX-i.left);e.currentTarget.setAttribute(\"seek-value\",s)})),this.bind(t.inputs.seek,\"mousedown mouseup keydown keyup touchstart touchend\",(t=>{const i=t.currentTarget,s=\"play-on-seeked\";if(is.keyboardEvent(t)&&![\"ArrowLeft\",\"ArrowRight\"].includes(t.key))return;e.lastSeekTime=Date.now();const n=i.hasAttribute(s),r=[\"mouseup\",\"touchend\",\"keyup\"].includes(t.type);n&&r?(i.removeAttribute(s),silencePromise(e.play())):!r&&e.playing&&(i.setAttribute(s,\"\"),e.pause())})),browser.isIos){const t=getElements.call(e,'input[type=\"range\"]');Array.from(t).forEach((e=>this.bind(e,i,(e=>repaint(e.target)))))}this.bind(t.inputs.seek,i,(t=>{const i=t.currentTarget;let s=i.getAttribute(\"seek-value\");is.empty(s)&&(s=i.value),i.removeAttribute(\"seek-value\"),e.currentTime=s/i.max*e.duration}),\"seek\"),this.bind(t.progress,\"mouseenter mouseleave mousemove\",(t=>controls.updateSeekTooltip.call(e,t))),this.bind(t.progress,\"mousemove touchmove\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startMove(t)})),this.bind(t.progress,\"mouseleave touchend click\",(()=>{const{previewThumbnails:t}=e;t&&t.loaded&&t.endMove(!1,!0)})),this.bind(t.progress,\"mousedown touchstart\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.startScrubbing(t)})),this.bind(t.progress,\"mouseup touchend\",(t=>{const{previewThumbnails:i}=e;i&&i.loaded&&i.endScrubbing(t)})),browser.isWebKit&&Array.from(getElements.call(e,'input[type=\"range\"]')).forEach((t=>{this.bind(t,\"input\",(t=>controls.updateRangeFill.call(e,t.target)))})),e.config.toggleInvert&&!is.element(t.display.duration)&&this.bind(t.display.currentTime,\"click\",(()=>{0!==e.currentTime&&(e.config.invertTime=!e.config.invertTime,controls.timeUpdate.call(e))})),this.bind(t.inputs.volume,i,(t=>{e.volume=t.target.value}),\"volume\"),this.bind(t.controls,\"mouseenter mouseleave\",(i=>{t.controls.hover=!e.touch&&\"mouseenter\"===i.type})),t.fullscreen&&Array.from(t.fullscreen.children).filter((e=>!e.contains(t.container))).forEach((i=>{this.bind(i,\"mouseenter mouseleave\",(i=>{t.controls&&(t.controls.hover=!e.touch&&\"mouseenter\"===i.type)}))})),this.bind(t.controls,\"mousedown mouseup touchstart touchend touchcancel\",(e=>{t.controls.pressed=[\"mousedown\",\"touchstart\"].includes(e.type)})),this.bind(t.controls,\"focusin\",(()=>{const{config:i,timers:s}=e;toggleClass(t.controls,i.classNames.noTransition,!0),ui.toggleControls.call(e,!0),setTimeout((()=>{toggleClass(t.controls,i.classNames.noTransition,!1)}),0);const n=this.touch?3e3:4e3;clearTimeout(s.controls),s.controls=setTimeout((()=>ui.toggleControls.call(e,!1)),n)})),this.bind(t.inputs.volume,\"wheel\",(t=>{const i=t.webkitDirectionInvertedFromDevice,[s,n]=[t.deltaX,-t.deltaY].map((e=>i?-e:e)),r=Math.sign(Math.abs(s)>Math.abs(n)?s:n);e.increaseVolume(r/50);const{volume:a}=e.media;(1===r&&a<1||-1===r&&a>0)&&t.preventDefault()}),\"volume\",!1)})),this.player=e,this.lastKey=null,this.focusTimer=null,this.lastKeyDown=null,this.handleKey=this.handleKey.bind(this),this.toggleMenu=this.toggleMenu.bind(this),this.firstTouch=this.firstTouch.bind(this)}handleKey(e){const{player:t}=this,{elements:i}=t,{key:s,type:n,altKey:r,ctrlKey:a,metaKey:o,shiftKey:l}=e,c=\"keydown\"===n,u=c&&s===this.lastKey;if(r||a||o||l)return;if(!s)return;if(c){const n=document.activeElement;if(is.element(n)){const{editable:s}=t.config.selectors,{seek:r}=i.inputs;if(n!==r&&matches(n,s))return;if(\" \"===e.key&&matches(n,'button, [role^=\"menuitem\"]'))return}switch([\" \",\"ArrowLeft\",\"ArrowUp\",\"ArrowRight\",\"ArrowDown\",\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"c\",\"f\",\"k\",\"l\",\"m\"].includes(s)&&(e.preventDefault(),e.stopPropagation()),s){case\"0\":case\"1\":case\"2\":case\"3\":case\"4\":case\"5\":case\"6\":case\"7\":case\"8\":case\"9\":u||(h=parseInt(s,10),t.currentTime=t.duration/10*h);break;case\" \":case\"k\":u||silencePromise(t.togglePlay());break;case\"ArrowUp\":t.increaseVolume(.1);break;case\"ArrowDown\":t.decreaseVolume(.1);break;case\"m\":u||(t.muted=!t.muted);break;case\"ArrowRight\":t.forward();break;case\"ArrowLeft\":t.rewind();break;case\"f\":t.fullscreen.toggle();break;case\"c\":u||t.toggleCaptions();break;case\"l\":t.loop=!t.loop}\"Escape\"===s&&!t.fullscreen.usingNative&&t.fullscreen.active&&t.fullscreen.toggle(),this.lastKey=s}else this.lastKey=null;var h}toggleMenu(e){controls.toggleMenu.call(this.player,e)}}var loadjs_umd=createCommonjsModule((function(e,t){e.exports=function(){var e=function(){},t={},i={},s={};function n(e,t){e=e.push?e:[e];var n,r,a,o=[],l=e.length,c=l;for(n=function(e,i){i.length&&o.push(e),--c||t(o)};l--;)r=e[l],(a=i[r])?n(r,a):(s[r]=s[r]||[]).push(n)}function r(e,t){if(e){var n=s[e];if(i[e]=t,n)for(;n.length;)n[0](e,t),n.splice(0,1)}}function a(t,i){t.call&&(t={success:t}),i.length?(t.error||e)(i):(t.success||e)(t)}function o(t,i,s,n){var r,a,l=document,c=s.async,u=(s.numRetries||0)+1,h=s.before||e,d=t.replace(/[\\?|#].*$/,\"\"),p=t.replace(/^(css|img)!/,\"\");n=n||0,/(^css!|\\.css$)/.test(d)?((a=l.createElement(\"link\")).rel=\"stylesheet\",a.href=p,(r=\"hideFocus\"in a)&&a.relList&&(r=0,a.rel=\"preload\",a.as=\"style\")):/(^img!|\\.(png|gif|jpg|svg|webp)$)/.test(d)?(a=l.createElement(\"img\")).src=p:((a=l.createElement(\"script\")).src=t,a.async=void 0===c||c),a.onload=a.onerror=a.onbeforeload=function(e){var l=e.type[0];if(r)try{a.sheet.cssText.length||(l=\"e\")}catch(e){18!=e.code&&(l=\"e\")}if(\"e\"==l){if((n+=1)<u)return o(t,i,s,n)}else if(\"preload\"==a.rel&&\"style\"==a.as)return a.rel=\"stylesheet\";i(t,l,e.defaultPrevented)},!1!==h(t,a)&&l.head.appendChild(a)}function l(e,t,i){var s,n,r=(e=e.push?e:[e]).length,a=r,l=[];for(s=function(e,i,s){if(\"e\"==i&&l.push(e),\"b\"==i){if(!s)return;l.push(e)}--r||t(l)},n=0;n<a;n++)o(e[n],s,i)}function c(e,i,s){var n,o;if(i&&i.trim&&(n=i),o=(n?s:i)||{},n){if(n in t)throw\"LoadJS\";t[n]=!0}function c(t,i){l(e,(function(e){a(o,e),t&&a({success:t,error:i},e),r(n,e)}),o)}if(o.returnPromise)return new Promise(c);c()}return c.ready=function(e,t){return n(e,(function(e){a(t,e)})),c},c.done=function(e){r(e,[])},c.reset=function(){t={},i={},s={}},c.isDefined=function(e){return e in t},c}()}));function loadScript(e){return new Promise(((t,i)=>{loadjs_umd(e,{success:t,error:i})}))}function parseId$1(e){if(is.empty(e))return null;if(is.number(Number(e)))return e;return e.match(/^.*(vimeo.com\\/|video\\/)(\\d+).*/)?RegExp.$2:e}function parseHash(e){const t=e.match(/^.*(vimeo.com\\/|video\\/)(\\d+)(\\?.*&*h=|\\/)+([\\d,a-f]+)/);return t&&5===t.length?t[4]:null}function assurePlaybackState$1(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}const vimeo={setup(){const e=this;toggleClass(e.elements.wrapper,e.config.classNames.embed,!0),e.options.speed=e.config.speed.options,setAspectRatio.call(e),is.object(window.Vimeo)?vimeo.ready.call(e):loadScript(e.config.urls.vimeo.sdk).then((()=>{vimeo.ready.call(e)})).catch((t=>{e.debug.warn(\"Vimeo SDK (player.js) failed to load\",t)}))},ready(){const e=this,t=e.config.vimeo,{premium:i,referrerPolicy:s,...n}=t;let r=e.media.getAttribute(\"src\"),a=\"\";is.empty(r)?(r=e.media.getAttribute(e.config.attributes.embed.id),a=e.media.getAttribute(e.config.attributes.embed.hash)):a=parseHash(r);const o=a?{h:a}:{};i&&Object.assign(n,{controls:!1,sidedock:!1});const l=buildUrlParams({loop:e.config.loop.active,autoplay:e.autoplay,muted:e.muted,gesture:\"media\",playsinline:e.config.playsinline,...o,...n}),c=parseId$1(r),u=createElement(\"iframe\"),h=format(e.config.urls.vimeo.iframe,c,l);if(u.setAttribute(\"src\",h),u.setAttribute(\"allowfullscreen\",\"\"),u.setAttribute(\"allow\",[\"autoplay\",\"fullscreen\",\"picture-in-picture\",\"encrypted-media\",\"accelerometer\",\"gyroscope\"].join(\"; \")),is.empty(s)||u.setAttribute(\"referrerPolicy\",s),i||!t.customControls)u.setAttribute(\"data-poster\",e.poster),e.media=replaceElement(u,e.media);else{const t=createElement(\"div\",{class:e.config.classNames.embedContainer,\"data-poster\":e.poster});t.appendChild(u),e.media=replaceElement(t,e.media)}t.customControls||fetch(format(e.config.urls.vimeo.api,h)).then((t=>{!is.empty(t)&&t.thumbnail_url&&ui.setPoster.call(e,t.thumbnail_url).catch((()=>{}))})),e.embed=new window.Vimeo.Player(u,{autopause:e.config.autopause,muted:e.muted}),e.media.paused=!0,e.media.currentTime=0,e.supported.ui&&e.embed.disableTextTrack(),e.media.play=()=>(assurePlaybackState$1.call(e,!0),e.embed.play()),e.media.pause=()=>(assurePlaybackState$1.call(e,!1),e.embed.pause()),e.media.stop=()=>{e.pause(),e.currentTime=0};let{currentTime:d}=e.media;Object.defineProperty(e.media,\"currentTime\",{get:()=>d,set(t){const{embed:i,media:s,paused:n,volume:r}=e,a=n&&!i.hasPlayed;s.seeking=!0,triggerEvent.call(e,s,\"seeking\"),Promise.resolve(a&&i.setVolume(0)).then((()=>i.setCurrentTime(t))).then((()=>a&&i.pause())).then((()=>a&&i.setVolume(r))).catch((()=>{}))}});let p=e.config.speed.selected;Object.defineProperty(e.media,\"playbackRate\",{get:()=>p,set(t){e.embed.setPlaybackRate(t).then((()=>{p=t,triggerEvent.call(e,e.media,\"ratechange\")})).catch((()=>{e.options.speed=[1]}))}});let{volume:m}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>m,set(t){e.embed.setVolume(t).then((()=>{m=t,triggerEvent.call(e,e.media,\"volumechange\")}))}});let{muted:g}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>g,set(t){const i=!!is.boolean(t)&&t;e.embed.setMuted(!!i||e.config.muted).then((()=>{g=i,triggerEvent.call(e,e.media,\"volumechange\")}))}});let f,{loop:y}=e.config;Object.defineProperty(e.media,\"loop\",{get:()=>y,set(t){const i=is.boolean(t)?t:e.config.loop.active;e.embed.setLoop(i).then((()=>{y=i}))}}),e.embed.getVideoUrl().then((t=>{f=t,controls.setDownloadUrl.call(e)})).catch((e=>{this.debug.warn(e)})),Object.defineProperty(e.media,\"currentSrc\",{get:()=>f}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration}),Promise.all([e.embed.getVideoWidth(),e.embed.getVideoHeight()]).then((t=>{const[i,s]=t;e.embed.ratio=roundAspectRatio(i,s),setAspectRatio.call(this)})),e.embed.setAutopause(e.config.autopause).then((t=>{e.config.autopause=t})),e.embed.getVideoTitle().then((t=>{e.config.title=t,ui.setTitle.call(this)})),e.embed.getCurrentTime().then((t=>{d=t,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.getDuration().then((t=>{e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\")})),e.embed.getTextTracks().then((t=>{e.media.textTracks=t,captions.setup.call(e)})),e.embed.on(\"cuechange\",(({cues:t=[]})=>{const i=t.map((e=>stripHTML(e.text)));captions.updateCues.call(e,i)})),e.embed.on(\"loaded\",(()=>{if(e.embed.getPaused().then((t=>{assurePlaybackState$1.call(e,!t),t||triggerEvent.call(e,e.media,\"playing\")})),is.element(e.embed.element)&&e.supported.ui){e.embed.element.setAttribute(\"tabindex\",-1)}})),e.embed.on(\"bufferstart\",(()=>{triggerEvent.call(e,e.media,\"waiting\")})),e.embed.on(\"bufferend\",(()=>{triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"play\",(()=>{assurePlaybackState$1.call(e,!0),triggerEvent.call(e,e.media,\"playing\")})),e.embed.on(\"pause\",(()=>{assurePlaybackState$1.call(e,!1)})),e.embed.on(\"timeupdate\",(t=>{e.media.seeking=!1,d=t.seconds,triggerEvent.call(e,e.media,\"timeupdate\")})),e.embed.on(\"progress\",(t=>{e.media.buffered=t.percent,triggerEvent.call(e,e.media,\"progress\"),1===parseInt(t.percent,10)&&triggerEvent.call(e,e.media,\"canplaythrough\"),e.embed.getDuration().then((t=>{t!==e.media.duration&&(e.media.duration=t,triggerEvent.call(e,e.media,\"durationchange\"))}))})),e.embed.on(\"seeked\",(()=>{e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")})),e.embed.on(\"ended\",(()=>{e.media.paused=!0,triggerEvent.call(e,e.media,\"ended\")})),e.embed.on(\"error\",(t=>{e.media.error=t,triggerEvent.call(e,e.media,\"error\")})),t.customControls&&setTimeout((()=>ui.build.call(e)),0)}};function parseId(e){if(is.empty(e))return null;return e.match(/^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|&v=)([^#&?]*).*/)?RegExp.$2:e}function assurePlaybackState(e){e&&!this.embed.hasPlayed&&(this.embed.hasPlayed=!0),this.media.paused===e&&(this.media.paused=!e,triggerEvent.call(this,this.media,e?\"play\":\"pause\"))}function getHost(e){return e.noCookie?\"https://www.youtube-nocookie.com\":\"http:\"===window.location.protocol?\"http://www.youtube.com\":void 0}const youtube={setup(){if(toggleClass(this.elements.wrapper,this.config.classNames.embed,!0),is.object(window.YT)&&is.function(window.YT.Player))youtube.ready.call(this);else{const e=window.onYouTubeIframeAPIReady;window.onYouTubeIframeAPIReady=()=>{is.function(e)&&e(),youtube.ready.call(this)},loadScript(this.config.urls.youtube.sdk).catch((e=>{this.debug.warn(\"YouTube API failed to load\",e)}))}},getTitle(e){fetch(format(this.config.urls.youtube.api,e)).then((e=>{if(is.object(e)){const{title:t,height:i,width:s}=e;this.config.title=t,ui.setTitle.call(this),this.embed.ratio=roundAspectRatio(s,i)}setAspectRatio.call(this)})).catch((()=>{setAspectRatio.call(this)}))},ready(){const e=this,t=e.config.youtube,i=e.media&&e.media.getAttribute(\"id\");if(!is.empty(i)&&i.startsWith(\"youtube-\"))return;let s=e.media.getAttribute(\"src\");is.empty(s)&&(s=e.media.getAttribute(this.config.attributes.embed.id));const n=parseId(s),r=createElement(\"div\",{id:generateId(e.provider),\"data-poster\":t.customControls?e.poster:void 0});if(e.media=replaceElement(r,e.media),t.customControls){const t=e=>`https://i.ytimg.com/vi/${n}/${e}default.jpg`;loadImage(t(\"maxres\"),121).catch((()=>loadImage(t(\"sd\"),121))).catch((()=>loadImage(t(\"hq\")))).then((t=>ui.setPoster.call(e,t.src))).then((t=>{t.includes(\"maxres\")||(e.elements.poster.style.backgroundSize=\"cover\")})).catch((()=>{}))}e.embed=new window.YT.Player(e.media,{videoId:n,host:getHost(t),playerVars:extend({},{autoplay:e.config.autoplay?1:0,hl:e.config.hl,controls:e.supported.ui&&t.customControls?0:1,disablekb:1,playsinline:e.config.playsinline&&!e.config.fullscreen.iosNative?1:0,cc_load_policy:e.captions.active?1:0,cc_lang_pref:e.config.captions.language,widget_referrer:window?window.location.href:null},t),events:{onError(t){if(!e.media.error){const i=t.data,s={2:\"The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.\",5:\"The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.\",100:\"The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.\",101:\"The owner of the requested video does not allow it to be played in embedded players.\",150:\"The owner of the requested video does not allow it to be played in embedded players.\"}[i]||\"An unknown error occurred\";e.media.error={code:i,message:s},triggerEvent.call(e,e.media,\"error\")}},onPlaybackRateChange(t){const i=t.target;e.media.playbackRate=i.getPlaybackRate(),triggerEvent.call(e,e.media,\"ratechange\")},onReady(i){if(is.function(e.media.play))return;const s=i.target;youtube.getTitle.call(e,n),e.media.play=()=>{assurePlaybackState.call(e,!0),s.playVideo()},e.media.pause=()=>{assurePlaybackState.call(e,!1),s.pauseVideo()},e.media.stop=()=>{s.stopVideo()},e.media.duration=s.getDuration(),e.media.paused=!0,e.media.currentTime=0,Object.defineProperty(e.media,\"currentTime\",{get:()=>Number(s.getCurrentTime()),set(t){e.paused&&!e.embed.hasPlayed&&e.embed.mute(),e.media.seeking=!0,triggerEvent.call(e,e.media,\"seeking\"),s.seekTo(t)}}),Object.defineProperty(e.media,\"playbackRate\",{get:()=>s.getPlaybackRate(),set(e){s.setPlaybackRate(e)}});let{volume:r}=e.config;Object.defineProperty(e.media,\"volume\",{get:()=>r,set(t){r=t,s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}});let{muted:a}=e.config;Object.defineProperty(e.media,\"muted\",{get:()=>a,set(t){const i=is.boolean(t)?t:a;a=i,s[i?\"mute\":\"unMute\"](),s.setVolume(100*r),triggerEvent.call(e,e.media,\"volumechange\")}}),Object.defineProperty(e.media,\"currentSrc\",{get:()=>s.getVideoUrl()}),Object.defineProperty(e.media,\"ended\",{get:()=>e.currentTime===e.duration});const o=s.getAvailablePlaybackRates();e.options.speed=o.filter((t=>e.config.speed.options.includes(t))),e.supported.ui&&t.customControls&&e.media.setAttribute(\"tabindex\",-1),triggerEvent.call(e,e.media,\"timeupdate\"),triggerEvent.call(e,e.media,\"durationchange\"),clearInterval(e.timers.buffering),e.timers.buffering=setInterval((()=>{e.media.buffered=s.getVideoLoadedFraction(),(null===e.media.lastBuffered||e.media.lastBuffered<e.media.buffered)&&triggerEvent.call(e,e.media,\"progress\"),e.media.lastBuffered=e.media.buffered,1===e.media.buffered&&(clearInterval(e.timers.buffering),triggerEvent.call(e,e.media,\"canplaythrough\"))}),200),t.customControls&&setTimeout((()=>ui.build.call(e)),50)},onStateChange(i){const s=i.target;clearInterval(e.timers.playing);switch(e.media.seeking&&[1,2].includes(i.data)&&(e.media.seeking=!1,triggerEvent.call(e,e.media,\"seeked\")),i.data){case-1:triggerEvent.call(e,e.media,\"timeupdate\"),e.media.buffered=s.getVideoLoadedFraction(),triggerEvent.call(e,e.media,\"progress\");break;case 0:assurePlaybackState.call(e,!1),e.media.loop?(s.stopVideo(),s.playVideo()):triggerEvent.call(e,e.media,\"ended\");break;case 1:t.customControls&&!e.config.autoplay&&e.media.paused&&!e.embed.hasPlayed?e.media.pause():(assurePlaybackState.call(e,!0),triggerEvent.call(e,e.media,\"playing\"),e.timers.playing=setInterval((()=>{triggerEvent.call(e,e.media,\"timeupdate\")}),50),e.media.duration!==s.getDuration()&&(e.media.duration=s.getDuration(),triggerEvent.call(e,e.media,\"durationchange\")));break;case 2:e.muted||e.embed.unMute(),assurePlaybackState.call(e,!1);break;case 3:triggerEvent.call(e,e.media,\"waiting\")}triggerEvent.call(e,e.elements.container,\"statechange\",!1,{code:i.data})}}})}},media={setup(){this.media?(toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",this.type),!0),toggleClass(this.elements.container,this.config.classNames.provider.replace(\"{0}\",this.provider),!0),this.isEmbed&&toggleClass(this.elements.container,this.config.classNames.type.replace(\"{0}\",\"video\"),!0),this.isVideo&&(this.elements.wrapper=createElement(\"div\",{class:this.config.classNames.video}),wrap(this.media,this.elements.wrapper),this.elements.poster=createElement(\"div\",{class:this.config.classNames.poster}),this.elements.wrapper.appendChild(this.elements.poster)),this.isHTML5?html5.setup.call(this):this.isYouTube?youtube.setup.call(this):this.isVimeo&&vimeo.setup.call(this)):this.debug.warn(\"No media element found!\")}},destroy=e=>{e.manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()};class Ads{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.enabled&&(is.object(window.google)&&is.object(window.google.ima)?this.ready():loadScript(this.player.config.urls.googleIMA.sdk).then((()=>{this.ready()})).catch((()=>{this.trigger(\"error\",new Error(\"Google IMA SDK failed to load\"))})))})),_defineProperty$1(this,\"ready\",(()=>{var e;this.enabled||((e=this).manager&&e.manager.destroy(),e.elements.displayContainer&&e.elements.displayContainer.destroy(),e.elements.container.remove()),this.startSafetyTimer(12e3,\"ready()\"),this.managerPromise.then((()=>{this.clearSafetyTimer(\"onAdsManagerLoaded()\")})),this.listeners(),this.setupIMA()})),_defineProperty$1(this,\"setupIMA\",(()=>{this.elements.container=createElement(\"div\",{class:this.player.config.classNames.ads}),this.player.elements.container.appendChild(this.elements.container),google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED),google.ima.settings.setLocale(this.player.config.ads.language),google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline),this.elements.displayContainer=new google.ima.AdDisplayContainer(this.elements.container,this.player.media),this.loader=new google.ima.AdsLoader(this.elements.displayContainer),this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,(e=>this.onAdsManagerLoaded(e)),!1),this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e)),!1),this.requestAds()})),_defineProperty$1(this,\"requestAds\",(()=>{const{container:e}=this.player.elements;try{const t=new google.ima.AdsRequest;t.adTagUrl=this.tagUrl,t.linearAdSlotWidth=e.offsetWidth,t.linearAdSlotHeight=e.offsetHeight,t.nonLinearAdSlotWidth=e.offsetWidth,t.nonLinearAdSlotHeight=e.offsetHeight,t.forceNonLinearFullSlot=!1,t.setAdWillPlayMuted(!this.player.muted),this.loader.requestAds(t)}catch(e){this.onAdError(e)}})),_defineProperty$1(this,\"pollCountdown\",((e=!1)=>{if(!e)return clearInterval(this.countdownTimer),void this.elements.container.removeAttribute(\"data-badge-text\");this.countdownTimer=setInterval((()=>{const e=formatTime(Math.max(this.manager.getRemainingTime(),0)),t=`${i18n.get(\"advertisement\",this.player.config)} - ${e}`;this.elements.container.setAttribute(\"data-badge-text\",t)}),100)})),_defineProperty$1(this,\"onAdsManagerLoaded\",(e=>{if(!this.enabled)return;const t=new google.ima.AdsRenderingSettings;t.restoreCustomPlaybackStateOnAdBreakComplete=!0,t.enablePreloading=!0,this.manager=e.getAdsManager(this.player,t),this.cuePoints=this.manager.getCuePoints(),this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR,(e=>this.onAdError(e))),Object.keys(google.ima.AdEvent.Type).forEach((e=>{this.manager.addEventListener(google.ima.AdEvent.Type[e],(e=>this.onAdEvent(e)))})),this.trigger(\"loaded\")})),_defineProperty$1(this,\"addCuePoints\",(()=>{is.empty(this.cuePoints)||this.cuePoints.forEach((e=>{if(0!==e&&-1!==e&&e<this.player.duration){const t=this.player.elements.progress;if(is.element(t)){const i=100/this.player.duration*e,s=createElement(\"span\",{class:this.player.config.classNames.cues});s.style.left=`${i.toString()}%`,t.appendChild(s)}}}))})),_defineProperty$1(this,\"onAdEvent\",(e=>{const{container:t}=this.player.elements,i=e.getAd(),s=e.getAdData();switch((e=>{triggerEvent.call(this.player,this.player.media,`ads${e.replace(/_/g,\"\").toLowerCase()}`)})(e.type),e.type){case google.ima.AdEvent.Type.LOADED:this.trigger(\"loaded\"),this.pollCountdown(!0),i.isLinear()||(i.width=t.offsetWidth,i.height=t.offsetHeight);break;case google.ima.AdEvent.Type.STARTED:this.manager.setVolume(this.player.volume);break;case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:this.player.ended?this.loadAds():this.loader.contentComplete();break;case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:this.pauseContent();break;case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:this.pollCountdown(),this.resumeContent();break;case google.ima.AdEvent.Type.LOG:s.adError&&this.player.debug.warn(`Non-fatal ad error: ${s.adError.getMessage()}`)}})),_defineProperty$1(this,\"onAdError\",(e=>{this.cancel(),this.player.debug.warn(\"Ads error\",e)})),_defineProperty$1(this,\"listeners\",(()=>{const{container:e}=this.player.elements;let t;this.player.on(\"canplay\",(()=>{this.addCuePoints()})),this.player.on(\"ended\",(()=>{this.loader.contentComplete()})),this.player.on(\"timeupdate\",(()=>{t=this.player.currentTime})),this.player.on(\"seeked\",(()=>{const e=this.player.currentTime;is.empty(this.cuePoints)||this.cuePoints.forEach(((i,s)=>{t<i&&i<e&&(this.manager.discardAdBreak(),this.cuePoints.splice(s,1))}))})),window.addEventListener(\"resize\",(()=>{this.manager&&this.manager.resize(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL)}))})),_defineProperty$1(this,\"play\",(()=>{const{container:e}=this.player.elements;this.managerPromise||this.resumeContent(),this.managerPromise.then((()=>{this.manager.setVolume(this.player.volume),this.elements.displayContainer.initialize();try{this.initialized||(this.manager.init(e.offsetWidth,e.offsetHeight,google.ima.ViewMode.NORMAL),this.manager.start()),this.initialized=!0}catch(e){this.onAdError(e)}})).catch((()=>{}))})),_defineProperty$1(this,\"resumeContent\",(()=>{this.elements.container.style.zIndex=\"\",this.playing=!1,silencePromise(this.player.media.play())})),_defineProperty$1(this,\"pauseContent\",(()=>{this.elements.container.style.zIndex=3,this.playing=!0,this.player.media.pause()})),_defineProperty$1(this,\"cancel\",(()=>{this.initialized&&this.resumeContent(),this.trigger(\"error\"),this.loadAds()})),_defineProperty$1(this,\"loadAds\",(()=>{this.managerPromise.then((()=>{this.manager&&this.manager.destroy(),this.managerPromise=new Promise((e=>{this.on(\"loaded\",e),this.player.debug.log(this.manager)})),this.initialized=!1,this.requestAds()})).catch((()=>{}))})),_defineProperty$1(this,\"trigger\",((e,...t)=>{const i=this.events[e];is.array(i)&&i.forEach((e=>{is.function(e)&&e.apply(this,t)}))})),_defineProperty$1(this,\"on\",((e,t)=>(is.array(this.events[e])||(this.events[e]=[]),this.events[e].push(t),this))),_defineProperty$1(this,\"startSafetyTimer\",((e,t)=>{this.player.debug.log(`Safety timer invoked from: ${t}`),this.safetyTimer=setTimeout((()=>{this.cancel(),this.clearSafetyTimer(\"startSafetyTimer()\")}),e)})),_defineProperty$1(this,\"clearSafetyTimer\",(e=>{is.nullOrUndefined(this.safetyTimer)||(this.player.debug.log(`Safety timer cleared from: ${e}`),clearTimeout(this.safetyTimer),this.safetyTimer=null)})),this.player=e,this.config=e.config.ads,this.playing=!1,this.initialized=!1,this.elements={container:null,displayContainer:null},this.manager=null,this.loader=null,this.cuePoints=null,this.events={},this.safetyTimer=null,this.countdownTimer=null,this.managerPromise=new Promise(((e,t)=>{this.on(\"loaded\",e),this.on(\"error\",t)})),this.load()}get enabled(){const{config:e}=this;return this.player.isHTML5&&this.player.isVideo&&e.enabled&&(!is.empty(e.publisherId)||is.url(e.tagUrl))}get tagUrl(){const{config:e}=this;if(is.url(e.tagUrl))return e.tagUrl;return`https://go.aniview.com/api/adserver6/vast/?${buildUrlParams({AV_PUBLISHERID:\"58c25bb0073ef448b1087ad6\",AV_CHANNELID:\"5a0458dc28a06145e4519d21\",AV_URL:window.location.hostname,cb:Date.now(),AV_WIDTH:640,AV_HEIGHT:480,AV_CDIM2:e.publisherId})}`}}function clamp(e=0,t=0,i=255){return Math.min(Math.max(e,t),i)}const parseVtt=e=>{const t=[];return e.split(/\\r\\n\\r\\n|\\n\\n|\\r\\r/).forEach((e=>{const i={};e.split(/\\r\\n|\\n|\\r/).forEach((e=>{if(is.number(i.startTime)){if(!is.empty(e.trim())&&is.empty(i.text)){const t=e.trim().split(\"#xywh=\");[i.text]=t,t[1]&&([i.x,i.y,i.w,i.h]=t[1].split(\",\"))}}else{const t=e.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/);t&&(i.startTime=60*Number(t[1]||0)*60+60*Number(t[2])+Number(t[3])+Number(`0.${t[4]}`),i.endTime=60*Number(t[6]||0)*60+60*Number(t[7])+Number(t[8])+Number(`0.${t[9]}`))}})),i.text&&t.push(i)})),t},fitRatio=(e,t)=>{const i={};return e>t.width/t.height?(i.width=t.width,i.height=1/e*t.width):(i.height=t.height,i.width=e*t.height),i};class PreviewThumbnails{constructor(e){_defineProperty$1(this,\"load\",(()=>{this.player.elements.display.seekTooltip&&(this.player.elements.display.seekTooltip.hidden=this.enabled),this.enabled&&this.getThumbnails().then((()=>{this.enabled&&(this.render(),this.determineContainerAutoSizing(),this.listeners(),this.loaded=!0)}))})),_defineProperty$1(this,\"getThumbnails\",(()=>new Promise((e=>{const{src:t}=this.player.config.previewThumbnails;if(is.empty(t))throw new Error(\"Missing previewThumbnails.src config attribute\");const i=()=>{this.thumbnails.sort(((e,t)=>e.height-t.height)),this.player.debug.log(\"Preview thumbnails\",this.thumbnails),e()};if(is.function(t))t((e=>{this.thumbnails=e,i()}));else{const e=(is.string(t)?[t]:t).map((e=>this.getThumbnail(e)));Promise.all(e).then(i)}})))),_defineProperty$1(this,\"getThumbnail\",(e=>new Promise((t=>{fetch(e).then((i=>{const s={frames:parseVtt(i),height:null,urlPrefix:\"\"};s.frames[0].text.startsWith(\"/\")||s.frames[0].text.startsWith(\"http://\")||s.frames[0].text.startsWith(\"https://\")||(s.urlPrefix=e.substring(0,e.lastIndexOf(\"/\")+1));const n=new Image;n.onload=()=>{s.height=n.naturalHeight,s.width=n.naturalWidth,this.thumbnails.push(s),t()},n.src=s.urlPrefix+s.frames[0].text}))})))),_defineProperty$1(this,\"startMove\",(e=>{if(this.loaded&&is.event(e)&&[\"touchmove\",\"mousemove\"].includes(e.type)&&this.player.media.duration){if(\"touchmove\"===e.type)this.seekTime=this.player.media.duration*(this.player.elements.inputs.seek.value/100);else{var t,i;const s=this.player.elements.progress.getBoundingClientRect(),n=100/s.width*(e.pageX-s.left);this.seekTime=this.player.media.duration*(n/100),this.seekTime<0&&(this.seekTime=0),this.seekTime>this.player.media.duration-1&&(this.seekTime=this.player.media.duration-1),this.mousePosX=e.pageX,this.elements.thumb.time.innerText=formatTime(this.seekTime);const r=null===(t=this.player.config.markers)||void 0===t||null===(i=t.points)||void 0===i?void 0:i.find((({time:e})=>e===Math.round(this.seekTime)));r&&this.elements.thumb.time.insertAdjacentHTML(\"afterbegin\",`${r.label}<br>`)}this.showImageAtCurrentTime()}})),_defineProperty$1(this,\"endMove\",(()=>{this.toggleThumbContainer(!1,!0)})),_defineProperty$1(this,\"startScrubbing\",(e=>{(is.nullOrUndefined(e.button)||!1===e.button||0===e.button)&&(this.mouseDown=!0,this.player.media.duration&&(this.toggleScrubbingContainer(!0),this.toggleThumbContainer(!1,!0),this.showImageAtCurrentTime()))})),_defineProperty$1(this,\"endScrubbing\",(()=>{this.mouseDown=!1,Math.ceil(this.lastTime)===Math.ceil(this.player.media.currentTime)?this.toggleScrubbingContainer(!1):once.call(this.player,this.player.media,\"timeupdate\",(()=>{this.mouseDown||this.toggleScrubbingContainer(!1)}))})),_defineProperty$1(this,\"listeners\",(()=>{this.player.on(\"play\",(()=>{this.toggleThumbContainer(!1,!0)})),this.player.on(\"seeked\",(()=>{this.toggleThumbContainer(!1)})),this.player.on(\"timeupdate\",(()=>{this.lastTime=this.player.media.currentTime}))})),_defineProperty$1(this,\"render\",(()=>{this.elements.thumb.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.thumbContainer}),this.elements.thumb.imageContainer=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.imageContainer}),this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);const e=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.timeContainer});this.elements.thumb.time=createElement(\"span\",{},\"00:00\"),e.appendChild(this.elements.thumb.time),this.elements.thumb.imageContainer.appendChild(e),is.element(this.player.elements.progress)&&this.player.elements.progress.appendChild(this.elements.thumb.container),this.elements.scrubbing.container=createElement(\"div\",{class:this.player.config.classNames.previewThumbnails.scrubbingContainer}),this.player.elements.wrapper.appendChild(this.elements.scrubbing.container)})),_defineProperty$1(this,\"destroy\",(()=>{this.elements.thumb.container&&this.elements.thumb.container.remove(),this.elements.scrubbing.container&&this.elements.scrubbing.container.remove()})),_defineProperty$1(this,\"showImageAtCurrentTime\",(()=>{this.mouseDown?this.setScrubbingContainerSize():this.setThumbContainerSizeAndPos();const e=this.thumbnails[0].frames.findIndex((e=>this.seekTime>=e.startTime&&this.seekTime<=e.endTime)),t=e>=0;let i=0;this.mouseDown||this.toggleThumbContainer(t),t&&(this.thumbnails.forEach(((t,s)=>{this.loadedImages.includes(t.frames[e].text)&&(i=s)})),e!==this.showingThumb&&(this.showingThumb=e,this.loadImage(i)))})),_defineProperty$1(this,\"loadImage\",((e=0)=>{const t=this.showingThumb,i=this.thumbnails[e],{urlPrefix:s}=i,n=i.frames[t],r=i.frames[t].text,a=s+r;if(this.currentImageElement&&this.currentImageElement.dataset.filename===r)this.showImage(this.currentImageElement,n,e,t,r,!1),this.currentImageElement.dataset.index=t,this.removeOldImages(this.currentImageElement);else{this.loadingImage&&this.usingSprites&&(this.loadingImage.onload=null);const i=new Image;i.src=a,i.dataset.index=t,i.dataset.filename=r,this.showingThumbFilename=r,this.player.debug.log(`Loading image: ${a}`),i.onload=()=>this.showImage(i,n,e,t,r,!0),this.loadingImage=i,this.removeOldImages(i)}})),_defineProperty$1(this,\"showImage\",((e,t,i,s,n,r=!0)=>{this.player.debug.log(`Showing thumb: ${n}. num: ${s}. qual: ${i}. newimg: ${r}`),this.setImageSizeAndOffset(e,t),r&&(this.currentImageContainer.appendChild(e),this.currentImageElement=e,this.loadedImages.includes(n)||this.loadedImages.push(n)),this.preloadNearby(s,!0).then(this.preloadNearby(s,!1)).then(this.getHigherQuality(i,e,t,n))})),_defineProperty$1(this,\"removeOldImages\",(e=>{Array.from(this.currentImageContainer.children).forEach((t=>{if(\"img\"!==t.tagName.toLowerCase())return;const i=this.usingSprites?500:1e3;if(t.dataset.index!==e.dataset.index&&!t.dataset.deleting){t.dataset.deleting=!0;const{currentImageContainer:e}=this;setTimeout((()=>{e.removeChild(t),this.player.debug.log(`Removing thumb: ${t.dataset.filename}`)}),i)}}))})),_defineProperty$1(this,\"preloadNearby\",((e,t=!0)=>new Promise((i=>{setTimeout((()=>{const s=this.thumbnails[0].frames[e].text;if(this.showingThumbFilename===s){let n;n=t?this.thumbnails[0].frames.slice(e):this.thumbnails[0].frames.slice(0,e).reverse();let r=!1;n.forEach((e=>{const t=e.text;if(t!==s&&!this.loadedImages.includes(t)){r=!0,this.player.debug.log(`Preloading thumb filename: ${t}`);const{urlPrefix:e}=this.thumbnails[0],s=e+t,n=new Image;n.src=s,n.onload=()=>{this.player.debug.log(`Preloaded thumb filename: ${t}`),this.loadedImages.includes(t)||this.loadedImages.push(t),i()}}})),r||i()}}),300)})))),_defineProperty$1(this,\"getHigherQuality\",((e,t,i,s)=>{if(e<this.thumbnails.length-1){let n=t.naturalHeight;this.usingSprites&&(n=i.h),n<this.thumbContainerHeight&&setTimeout((()=>{this.showingThumbFilename===s&&(this.player.debug.log(`Showing higher quality thumb for: ${s}`),this.loadImage(e+1))}),300)}})),_defineProperty$1(this,\"toggleThumbContainer\",((e=!1,t=!1)=>{const i=this.player.config.classNames.previewThumbnails.thumbContainerShown;this.elements.thumb.container.classList.toggle(i,e),!e&&t&&(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"toggleScrubbingContainer\",((e=!1)=>{const t=this.player.config.classNames.previewThumbnails.scrubbingContainerShown;this.elements.scrubbing.container.classList.toggle(t,e),e||(this.showingThumb=null,this.showingThumbFilename=null)})),_defineProperty$1(this,\"determineContainerAutoSizing\",(()=>{(this.elements.thumb.imageContainer.clientHeight>20||this.elements.thumb.imageContainer.clientWidth>20)&&(this.sizeSpecifiedInCSS=!0)})),_defineProperty$1(this,\"setThumbContainerSizeAndPos\",(()=>{const{imageContainer:e}=this.elements.thumb;if(this.sizeSpecifiedInCSS){if(e.clientHeight>20&&e.clientWidth<20){const t=Math.floor(e.clientHeight*this.thumbAspectRatio);e.style.width=`${t}px`}else if(e.clientHeight<20&&e.clientWidth>20){const t=Math.floor(e.clientWidth/this.thumbAspectRatio);e.style.height=`${t}px`}}else{const t=Math.floor(this.thumbContainerHeight*this.thumbAspectRatio);e.style.height=`${this.thumbContainerHeight}px`,e.style.width=`${t}px`}this.setThumbContainerPos()})),_defineProperty$1(this,\"setThumbContainerPos\",(()=>{const e=this.player.elements.progress.getBoundingClientRect(),t=this.player.elements.container.getBoundingClientRect(),{container:i}=this.elements.thumb,s=t.left-e.left+10,n=t.right-e.left-i.clientWidth-10,r=this.mousePosX-e.left-i.clientWidth/2,a=clamp(r,s,n);i.style.left=`${a}px`,i.style.setProperty(\"--preview-arrow-offset\",r-a+\"px\")})),_defineProperty$1(this,\"setScrubbingContainerSize\",(()=>{const{width:e,height:t}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});this.elements.scrubbing.container.style.width=`${e}px`,this.elements.scrubbing.container.style.height=`${t}px`})),_defineProperty$1(this,\"setImageSizeAndOffset\",((e,t)=>{if(!this.usingSprites)return;const i=this.thumbContainerHeight/t.h;e.style.height=e.naturalHeight*i+\"px\",e.style.width=e.naturalWidth*i+\"px\",e.style.left=`-${t.x*i}px`,e.style.top=`-${t.y*i}px`})),this.player=e,this.thumbnails=[],this.loaded=!1,this.lastMouseMoveTime=Date.now(),this.mouseDown=!1,this.loadedImages=[],this.elements={thumb:{},scrubbing:{}},this.load()}get enabled(){return this.player.isHTML5&&this.player.isVideo&&this.player.config.previewThumbnails.enabled}get currentImageContainer(){return this.mouseDown?this.elements.scrubbing.container:this.elements.thumb.imageContainer}get usingSprites(){return Object.keys(this.thumbnails[0].frames[0]).includes(\"w\")}get thumbAspectRatio(){return this.usingSprites?this.thumbnails[0].frames[0].w/this.thumbnails[0].frames[0].h:this.thumbnails[0].width/this.thumbnails[0].height}get thumbContainerHeight(){if(this.mouseDown){const{height:e}=fitRatio(this.thumbAspectRatio,{width:this.player.media.clientWidth,height:this.player.media.clientHeight});return e}return this.sizeSpecifiedInCSS?this.elements.thumb.imageContainer.clientHeight:Math.floor(this.player.media.clientWidth/this.thumbAspectRatio/4)}get currentImageElement(){return this.mouseDown?this.currentScrubbingImageElement:this.currentThumbnailImageElement}set currentImageElement(e){this.mouseDown?this.currentScrubbingImageElement=e:this.currentThumbnailImageElement=e}}const source={insertElements(e,t){is.string(t)?insertElement(e,this.media,{src:t}):is.array(t)&&t.forEach((t=>{insertElement(e,this.media,t)}))},change(e){getDeep(e,\"sources.length\")?(html5.cancelRequests.call(this),this.destroy.call(this,(()=>{this.options.quality=[],removeElement(this.media),this.media=null,is.element(this.elements.container)&&this.elements.container.removeAttribute(\"class\");const{sources:t,type:i}=e,[{provider:s=providers.html5,src:n}]=t,r=\"html5\"===s?i:\"div\",a=\"html5\"===s?{}:{src:n};Object.assign(this,{provider:s,type:i,supported:support.check(i,s,this.config.playsinline),media:createElement(r,a)}),this.elements.container.appendChild(this.media),is.boolean(e.autoplay)&&(this.config.autoplay=e.autoplay),this.isHTML5&&(this.config.crossorigin&&this.media.setAttribute(\"crossorigin\",\"\"),this.config.autoplay&&this.media.setAttribute(\"autoplay\",\"\"),is.empty(e.poster)||(this.poster=e.poster),this.config.loop.active&&this.media.setAttribute(\"loop\",\"\"),this.config.muted&&this.media.setAttribute(\"muted\",\"\"),this.config.playsinline&&this.media.setAttribute(\"playsinline\",\"\")),ui.addStyleHook.call(this),this.isHTML5&&source.insertElements.call(this,\"source\",t),this.config.title=e.title,media.setup.call(this),this.isHTML5&&Object.keys(e).includes(\"tracks\")&&source.insertElements.call(this,\"track\",e.tracks),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.isHTML5&&this.media.load(),is.empty(e.previewThumbnails)||(Object.assign(this.config.previewThumbnails,e.previewThumbnails),this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))),this.fullscreen.update()}),!0)):this.debug.warn(\"Invalid source format\")}};class Plyr{constructor(e,t){if(_defineProperty$1(this,\"play\",(()=>is.function(this.media.play)?(this.ads&&this.ads.enabled&&this.ads.managerPromise.then((()=>this.ads.play())).catch((()=>silencePromise(this.media.play()))),this.media.play()):null)),_defineProperty$1(this,\"pause\",(()=>this.playing&&is.function(this.media.pause)?this.media.pause():null)),_defineProperty$1(this,\"togglePlay\",(e=>(is.boolean(e)?e:!this.playing)?this.play():this.pause())),_defineProperty$1(this,\"stop\",(()=>{this.isHTML5?(this.pause(),this.restart()):is.function(this.media.stop)&&this.media.stop()})),_defineProperty$1(this,\"restart\",(()=>{this.currentTime=0})),_defineProperty$1(this,\"rewind\",(e=>{this.currentTime-=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"forward\",(e=>{this.currentTime+=is.number(e)?e:this.config.seekTime})),_defineProperty$1(this,\"increaseVolume\",(e=>{const t=this.media.muted?0:this.volume;this.volume=t+(is.number(e)?e:0)})),_defineProperty$1(this,\"decreaseVolume\",(e=>{this.increaseVolume(-e)})),_defineProperty$1(this,\"airplay\",(()=>{support.airplay&&this.media.webkitShowPlaybackTargetPicker()})),_defineProperty$1(this,\"toggleControls\",(e=>{if(this.supported.ui&&!this.isAudio){const t=hasClass(this.elements.container,this.config.classNames.hideControls),i=void 0===e?void 0:!e,s=toggleClass(this.elements.container,this.config.classNames.hideControls,i);if(s&&is.array(this.config.controls)&&this.config.controls.includes(\"settings\")&&!is.empty(this.config.settings)&&controls.toggleMenu.call(this,!1),s!==t){const e=s?\"controlshidden\":\"controlsshown\";triggerEvent.call(this,this.media,e)}return!s}return!1})),_defineProperty$1(this,\"on\",((e,t)=>{on.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"once\",((e,t)=>{once.call(this,this.elements.container,e,t)})),_defineProperty$1(this,\"off\",((e,t)=>{off(this.elements.container,e,t)})),_defineProperty$1(this,\"destroy\",((e,t=!1)=>{if(!this.ready)return;const i=()=>{document.body.style.overflow=\"\",this.embed=null,t?(Object.keys(this.elements).length&&(removeElement(this.elements.buttons.play),removeElement(this.elements.captions),removeElement(this.elements.controls),removeElement(this.elements.wrapper),this.elements.buttons.play=null,this.elements.captions=null,this.elements.controls=null,this.elements.wrapper=null),is.function(e)&&e()):(unbindListeners.call(this),html5.cancelRequests.call(this),replaceElement(this.elements.original,this.elements.container),triggerEvent.call(this,this.elements.original,\"destroyed\",!0),is.function(e)&&e.call(this.elements.original),this.ready=!1,setTimeout((()=>{this.elements=null,this.media=null}),200))};this.stop(),clearTimeout(this.timers.loading),clearTimeout(this.timers.controls),clearTimeout(this.timers.resized),this.isHTML5?(ui.toggleNativeControls.call(this,!0),i()):this.isYouTube?(clearInterval(this.timers.buffering),clearInterval(this.timers.playing),null!==this.embed&&is.function(this.embed.destroy)&&this.embed.destroy(),i()):this.isVimeo&&(null!==this.embed&&this.embed.unload().then(i),setTimeout(i,200))})),_defineProperty$1(this,\"supports\",(e=>support.mime.call(this,e))),this.timers={},this.ready=!1,this.loading=!1,this.failed=!1,this.touch=support.touch,this.media=e,is.string(this.media)&&(this.media=document.querySelectorAll(this.media)),(window.jQuery&&this.media instanceof jQuery||is.nodeList(this.media)||is.array(this.media))&&(this.media=this.media[0]),this.config=extend({},defaults,Plyr.defaults,t||{},(()=>{try{return JSON.parse(this.media.getAttribute(\"data-plyr-config\"))}catch(e){return{}}})()),this.elements={container:null,fullscreen:null,captions:null,buttons:{},display:{},progress:{},inputs:{},settings:{popup:null,menu:null,panels:{},buttons:{}}},this.captions={active:null,currentTrack:-1,meta:new WeakMap},this.fullscreen={active:!1},this.options={speed:[],quality:[]},this.debug=new Console(this.config.debug),this.debug.log(\"Config\",this.config),this.debug.log(\"Support\",support),is.nullOrUndefined(this.media)||!is.element(this.media))return void this.debug.error(\"Setup failed: no suitable element passed\");if(this.media.plyr)return void this.debug.warn(\"Target already setup\");if(!this.config.enabled)return void this.debug.error(\"Setup failed: disabled by config\");if(!support.check().api)return void this.debug.error(\"Setup failed: no support\");const i=this.media.cloneNode(!0);i.autoplay=!1,this.elements.original=i;const s=this.media.tagName.toLowerCase();let n=null,r=null;switch(s){case\"div\":if(n=this.media.querySelector(\"iframe\"),is.element(n)){if(r=parseUrl(n.getAttribute(\"src\")),this.provider=getProviderByUrl(r.toString()),this.elements.container=this.media,this.media=n,this.elements.container.className=\"\",r.search.length){const e=[\"1\",\"true\"];e.includes(r.searchParams.get(\"autoplay\"))&&(this.config.autoplay=!0),e.includes(r.searchParams.get(\"loop\"))&&(this.config.loop.active=!0),this.isYouTube?(this.config.playsinline=e.includes(r.searchParams.get(\"playsinline\")),this.config.youtube.hl=r.searchParams.get(\"hl\")):this.config.playsinline=!0}}else this.provider=this.media.getAttribute(this.config.attributes.embed.provider),this.media.removeAttribute(this.config.attributes.embed.provider);if(is.empty(this.provider)||!Object.values(providers).includes(this.provider))return void this.debug.error(\"Setup failed: Invalid provider\");this.type=types.video;break;case\"video\":case\"audio\":this.type=s,this.provider=providers.html5,this.media.hasAttribute(\"crossorigin\")&&(this.config.crossorigin=!0),this.media.hasAttribute(\"autoplay\")&&(this.config.autoplay=!0),(this.media.hasAttribute(\"playsinline\")||this.media.hasAttribute(\"webkit-playsinline\"))&&(this.config.playsinline=!0),this.media.hasAttribute(\"muted\")&&(this.config.muted=!0),this.media.hasAttribute(\"loop\")&&(this.config.loop.active=!0);break;default:return void this.debug.error(\"Setup failed: unsupported type\")}this.supported=support.check(this.type,this.provider),this.supported.api?(this.eventListeners=[],this.listeners=new Listeners(this),this.storage=new Storage(this),this.media.plyr=this,is.element(this.elements.container)||(this.elements.container=createElement(\"div\"),wrap(this.media,this.elements.container)),ui.migrateStyles.call(this),ui.addStyleHook.call(this),media.setup.call(this),this.config.debug&&on.call(this,this.elements.container,this.config.events.join(\" \"),(e=>{this.debug.log(`event: ${e.type}`)})),this.fullscreen=new Fullscreen(this),(this.isHTML5||this.isEmbed&&!this.supported.ui)&&ui.build.call(this),this.listeners.container(),this.listeners.global(),this.config.ads.enabled&&(this.ads=new Ads(this)),this.isHTML5&&this.config.autoplay&&this.once(\"canplay\",(()=>silencePromise(this.play()))),this.lastSeekTime=0,this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))):this.debug.error(\"Setup failed: no support\")}get isHTML5(){return this.provider===providers.html5}get isEmbed(){return this.isYouTube||this.isVimeo}get isYouTube(){return this.provider===providers.youtube}get isVimeo(){return this.provider===providers.vimeo}get isVideo(){return this.type===types.video}get isAudio(){return this.type===types.audio}get playing(){return Boolean(this.ready&&!this.paused&&!this.ended)}get paused(){return Boolean(this.media.paused)}get stopped(){return Boolean(this.paused&&0===this.currentTime)}get ended(){return Boolean(this.media.ended)}set currentTime(e){if(!this.duration)return;const t=is.number(e)&&e>0;this.media.currentTime=t?Math.min(e,this.duration):0,this.debug.log(`Seeking to ${this.currentTime} seconds`)}get currentTime(){return Number(this.media.currentTime)}get buffered(){const{buffered:e}=this.media;return is.number(e)?e:e&&e.length&&this.duration>0?e.end(0)/this.duration:0}get seeking(){return Boolean(this.media.seeking)}get duration(){const e=parseFloat(this.config.duration),t=(this.media||{}).duration,i=is.number(t)&&t!==1/0?t:0;return e||i}set volume(e){let t=e;is.string(t)&&(t=Number(t)),is.number(t)||(t=this.storage.get(\"volume\")),is.number(t)||({volume:t}=this.config),t>1&&(t=1),t<0&&(t=0),this.config.volume=t,this.media.volume=t,!is.empty(e)&&this.muted&&t>0&&(this.muted=!1)}get volume(){return Number(this.media.volume)}set muted(e){let t=e;is.boolean(t)||(t=this.storage.get(\"muted\")),is.boolean(t)||(t=this.config.muted),this.config.muted=t,this.media.muted=t}get muted(){return Boolean(this.media.muted)}get hasAudio(){return!this.isHTML5||(!!this.isAudio||(Boolean(this.media.mozHasAudio)||Boolean(this.media.webkitAudioDecodedByteCount)||Boolean(this.media.audioTracks&&this.media.audioTracks.length)))}set speed(e){let t=null;is.number(e)&&(t=e),is.number(t)||(t=this.storage.get(\"speed\")),is.number(t)||(t=this.config.speed.selected);const{minimumSpeed:i,maximumSpeed:s}=this;t=clamp(t,i,s),this.config.speed.selected=t,setTimeout((()=>{this.media&&(this.media.playbackRate=t)}),0)}get speed(){return Number(this.media.playbackRate)}get minimumSpeed(){return this.isYouTube?Math.min(...this.options.speed):this.isVimeo?.5:.0625}get maximumSpeed(){return this.isYouTube?Math.max(...this.options.speed):this.isVimeo?2:16}set quality(e){const t=this.config.quality,i=this.options.quality;if(!i.length)return;let s=[!is.empty(e)&&Number(e),this.storage.get(\"quality\"),t.selected,t.default].find(is.number),n=!0;if(!i.includes(s)){const e=closest(i,s);this.debug.warn(`Unsupported quality option: ${s}, using ${e} instead`),s=e,n=!1}t.selected=s,this.media.quality=s,n&&this.storage.set({quality:s})}get quality(){return this.media.quality}set loop(e){const t=is.boolean(e)?e:this.config.loop.active;this.config.loop.active=t,this.media.loop=t}get loop(){return Boolean(this.media.loop)}set source(e){source.change.call(this,e)}get source(){return this.media.currentSrc}get download(){const{download:e}=this.config.urls;return is.url(e)?e:this.source}set download(e){is.url(e)&&(this.config.urls.download=e,controls.setDownloadUrl.call(this))}set poster(e){this.isVideo?ui.setPoster.call(this,e,!1).catch((()=>{})):this.debug.warn(\"Poster can only be set for video\")}get poster(){return this.isVideo?this.media.getAttribute(\"poster\")||this.media.getAttribute(\"data-poster\"):null}get ratio(){if(!this.isVideo)return null;const e=reduceAspectRatio(getAspectRatio.call(this));return is.array(e)?e.join(\":\"):e}set ratio(e){this.isVideo?is.string(e)&&validateAspectRatio(e)?(this.config.ratio=reduceAspectRatio(e),setAspectRatio.call(this)):this.debug.error(`Invalid aspect ratio specified (${e})`):this.debug.warn(\"Aspect ratio can only be set for video\")}set autoplay(e){this.config.autoplay=is.boolean(e)?e:this.config.autoplay}get autoplay(){return Boolean(this.config.autoplay)}toggleCaptions(e){captions.toggle.call(this,e,!1)}set currentTrack(e){captions.set.call(this,e,!1),captions.setup.call(this)}get currentTrack(){const{toggled:e,currentTrack:t}=this.captions;return e?t:-1}set language(e){captions.setLanguage.call(this,e,!1)}get language(){return(captions.getCurrentTrack.call(this)||{}).language}set pip(e){if(!support.pip)return;const t=is.boolean(e)?e:!this.pip;is.function(this.media.webkitSetPresentationMode)&&this.media.webkitSetPresentationMode(t?pip.active:pip.inactive),is.function(this.media.requestPictureInPicture)&&(!this.pip&&t?this.media.requestPictureInPicture():this.pip&&!t&&document.exitPictureInPicture())}get pip(){return support.pip?is.empty(this.media.webkitPresentationMode)?this.media===document.pictureInPictureElement:this.media.webkitPresentationMode===pip.active:null}setPreviewThumbnails(e){this.previewThumbnails&&this.previewThumbnails.loaded&&(this.previewThumbnails.destroy(),this.previewThumbnails=null),Object.assign(this.config.previewThumbnails,e),this.config.previewThumbnails.enabled&&(this.previewThumbnails=new PreviewThumbnails(this))}static supported(e,t){return support.check(e,t)}static loadSprite(e,t){return loadSprite(e,t)}static setup(e,t={}){let i=null;return is.string(e)?i=Array.from(document.querySelectorAll(e)):is.nodeList(e)?i=Array.from(e):is.array(e)&&(i=e.filter(is.element)),is.empty(i)?null:i.map((e=>new Plyr(e,t)))}}Plyr.defaults=cloneDeep(defaults);export{Plyr as default};\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.min.mjs.map b/node_modules/plyr/dist/plyr.polyfilled.min.mjs.map\nindex f8770de..2fb299f 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.min.mjs.map\n+++ b/node_modules/plyr/dist/plyr.polyfilled.min.mjs.map\n@@ -1 +1 @@\n-{\"version\":3,\"sources\":[\"node_modules/.pnpm/custom-event-polyfill@1.0.7/node_modules/custom-event-polyfill/polyfill.js\",\"plyr.polyfilled.mjs\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"node_modules/.pnpm/url-polyfill@1.1.12/node_modules/url-polyfill/url-polyfill.js\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"window\",\"ce\",\"CustomEvent\",\"cancelable\",\"preventDefault\",\"defaultPrevented\",\"Error\",\"e\",\"event\",\"params\",\"evt\",\"origPrevent\",\"bubbles\",\"document\",\"createEvent\",\"initCustomEvent\",\"detail\",\"call\",\"this\",\"Object\",\"defineProperty\",\"get\",\"prototype\",\"Event\",\"commonjsGlobal\",\"globalThis\",\"global\",\"self\",\"createCommonjsModule\",\"fn\",\"module\",\"exports\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"_toPropertyKey\",\"enumerable\",\"configurable\",\"writable\",\"_toPrimitive\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"TypeError\",\"String\",\"Number\",\"arg\",\"_classCallCheck\",\"t\",\"_defineProperties\",\"n\",\"length\",\"r\",\"_createClass\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"iteratorSupported\",\"iterator\",\"error\",\"checkIfIteratorIsSupported\",\"createIterator\",\"items\",\"next\",\"shift\",\"done\",\"serializeParam\",\"encodeURIComponent\",\"replace\",\"deserializeParam\",\"decodeURIComponent\",\"URLSearchParams\",\"toString\",\"set\",\"entries\",\"checkIfURLSearchParamsSupported\",\"searchString\",\"typeofSearchString\",\"_fromString\",\"_this\",\"name\",\"append\",\"i\",\"entry\",\"hasOwnProperty\",\"proto\",\"_entries\",\"delete\",\"getAll\",\"slice\",\"has\",\"callback\",\"thisArg\",\"values\",\"searchArray\",\"join\",\"polyfillURLSearchParams\",\"sort\",\"a\",\"b\",\"attribute\",\"attributes\",\"split\",\"u\",\"URL\",\"pathname\",\"href\",\"searchParams\",\"checkIfURLIsSupported\",\"_URL\",\"url\",\"base\",\"baseElement\",\"doc\",\"location\",\"toLowerCase\",\"implementation\",\"createHTMLDocument\",\"createElement\",\"head\",\"appendChild\",\"indexOf\",\"err\",\"anchorElement\",\"body\",\"inputElement\",\"type\",\"protocol\",\"test\",\"checkValidity\",\"search\",\"enableSearchUpdate\",\"enableSearchParamsUpdate\",\"methodName\",\"method\",\"attributeName\",\"_anchorElement\",\"linkURLWithAnchorAttribute\",\"_updateSearchParams\",\"origin\",\"expectedPort\",\"addPortToOrigin\",\"port\",\"hostname\",\"password\",\"username\",\"createObjectURL\",\"blob\",\"revokeObjectURL\",\"polyfillURL\",\"getOrigin\",\"setInterval\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"matches\",\"Array\",\"from\",\"querySelectorAll\",\"includes\",\"trigger\",\"dispatchEvent\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isNumber\",\"isNaN\",\"isString\",\"isBoolean\",\"Boolean\",\"isFunction\",\"Function\",\"isArray\",\"isNodeList\",\"NodeList\",\"isElement\",\"Element\",\"isEvent\",\"isEmpty\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"string\",\"boolean\",\"function\",\"array\",\"nodeList\",\"element\",\"empty\",\"getDecimalPlaces\",\"concat\",\"match\",\"Math\",\"max\",\"round\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"target\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"c\",\"getBoundingClientRect\",\"width\",\"clientX\",\"left\",\"disabled\",\"MutationObserver\",\"addedNodes\",\"observe\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isWeakMap\",\"WeakMap\",\"isTextNode\",\"Text\",\"isKeyboardEvent\",\"KeyboardEvent\",\"isCue\",\"TextTrackCue\",\"VTTCue\",\"isTrack\",\"TextTrack\",\"kind\",\"isPromise\",\"Promise\",\"then\",\"nodeType\",\"ownerDocument\",\"isUrl\",\"startsWith\",\"_\",\"weakMap\",\"textNode\",\"keyboardEvent\",\"cue\",\"track\",\"promise\",\"transitionEndEvent\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"isIE\",\"documentMode\",\"isEdge\",\"navigator\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"browser\",\"cloneDeep\",\"JSON\",\"parse\",\"stringify\",\"getDeep\",\"path\",\"reduce\",\"extend\",\"sources\",\"source\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"insertBefore\",\"setAttributes\",\"setAttribute\",\"text\",\"innerText\",\"insertAfter\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"closest\",\"el\",\"parentElement\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"getViewportSize\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"generateId\",\"prefix\",\"floor\",\"random\",\"format\",\"getPercentage\",\"current\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"toPascalCase\",\"toCamelCase\",\"stripHTML\",\"fragment\",\"createDocumentFragment\",\"innerHTML\",\"firstChild\",\"getHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"storage\",\"setItem\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"status\",\"open\",\"send\",\"loadSprite\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"urls\",\"isEmbed\",\"inject\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"isYouTube\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"inactive\",\"providers\",\"types\",\"getProviderByUrl\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"hasAttribute\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"factory\",\"loadScript\",\"parseId\",\"$2\",\"parseHash\",\"found\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"hashParam\",\"sidedock\",\"gesture\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"destroy\",\"manager\",\"displayContainer\",\"remove\",\"Ads\",\"google\",\"ima\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"Plyr\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"truthy\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.polyfilled.min.mjs\",\"sourcesContent\":[\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function() {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n\\n  try {\\n    var ce = new window.CustomEvent('test', { cancelable: true });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function(event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(\\n        event,\\n        params.bubbles,\\n        params.cancelable,\\n        params.detail\\n      );\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function() {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function() {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\",\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function () {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n  try {\\n    var ce = new window.CustomEvent('test', {\\n      cancelable: true\\n    });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function (event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function () {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function () {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\\nvar commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\nfunction createCommonjsModule(fn, module) {\\n\\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n}\\n\\n(function (global) {\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\n\\n  var checkIfIteratorIsSupported = function () {\\n    try {\\n      return !!Symbol.iterator;\\n    } catch (error) {\\n      return false;\\n    }\\n  };\\n  var iteratorSupported = checkIfIteratorIsSupported();\\n  var createIterator = function (items) {\\n    var iterator = {\\n      next: function () {\\n        var value = items.shift();\\n        return {\\n          done: value === void 0,\\n          value: value\\n        };\\n      }\\n    };\\n    if (iteratorSupported) {\\n      iterator[Symbol.iterator] = function () {\\n        return iterator;\\n      };\\n    }\\n    return iterator;\\n  };\\n\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\n  var serializeParam = function (value) {\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\n  };\\n  var deserializeParam = function (value) {\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\n  };\\n  var polyfillURLSearchParams = function () {\\n    var URLSearchParams = function (searchString) {\\n      Object.defineProperty(this, '_entries', {\\n        writable: true,\\n        value: {}\\n      });\\n      var typeofSearchString = typeof searchString;\\n      if (typeofSearchString === 'undefined') ; else if (typeofSearchString === 'string') {\\n        if (searchString !== '') {\\n          this._fromString(searchString);\\n        }\\n      } else if (searchString instanceof URLSearchParams) {\\n        var _this = this;\\n        searchString.forEach(function (value, name) {\\n          _this.append(name, value);\\n        });\\n      } else if (searchString !== null && typeofSearchString === 'object') {\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\n          for (var i = 0; i < searchString.length; i++) {\\n            var entry = searchString[i];\\n            if (Object.prototype.toString.call(entry) === '[object Array]' || entry.length !== 2) {\\n              this.append(entry[0], entry[1]);\\n            } else {\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\n            }\\n          }\\n        } else {\\n          for (var key in searchString) {\\n            if (searchString.hasOwnProperty(key)) {\\n              this.append(key, searchString[key]);\\n            }\\n          }\\n        }\\n      } else {\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\n      }\\n    };\\n    var proto = URLSearchParams.prototype;\\n    proto.append = function (name, value) {\\n      if (name in this._entries) {\\n        this._entries[name].push(String(value));\\n      } else {\\n        this._entries[name] = [String(value)];\\n      }\\n    };\\n    proto.delete = function (name) {\\n      delete this._entries[name];\\n    };\\n    proto.get = function (name) {\\n      return name in this._entries ? this._entries[name][0] : null;\\n    };\\n    proto.getAll = function (name) {\\n      return name in this._entries ? this._entries[name].slice(0) : [];\\n    };\\n    proto.has = function (name) {\\n      return name in this._entries;\\n    };\\n    proto.set = function (name, value) {\\n      this._entries[name] = [String(value)];\\n    };\\n    proto.forEach = function (callback, thisArg) {\\n      var entries;\\n      for (var name in this._entries) {\\n        if (this._entries.hasOwnProperty(name)) {\\n          entries = this._entries[name];\\n          for (var i = 0; i < entries.length; i++) {\\n            callback.call(thisArg, entries[i], name, this);\\n          }\\n        }\\n      }\\n    };\\n    proto.keys = function () {\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push(name);\\n      });\\n      return createIterator(items);\\n    };\\n    proto.values = function () {\\n      var items = [];\\n      this.forEach(function (value) {\\n        items.push(value);\\n      });\\n      return createIterator(items);\\n    };\\n    proto.entries = function () {\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push([name, value]);\\n      });\\n      return createIterator(items);\\n    };\\n    if (iteratorSupported) {\\n      proto[Symbol.iterator] = proto.entries;\\n    }\\n    proto.toString = function () {\\n      var searchArray = [];\\n      this.forEach(function (value, name) {\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\n      });\\n      return searchArray.join('&');\\n    };\\n    global.URLSearchParams = URLSearchParams;\\n  };\\n  var checkIfURLSearchParamsSupported = function () {\\n    try {\\n      var URLSearchParams = global.URLSearchParams;\\n      return new URLSearchParams('?a=1').toString() === 'a=1' && typeof URLSearchParams.prototype.set === 'function' && typeof URLSearchParams.prototype.entries === 'function';\\n    } catch (e) {\\n      return false;\\n    }\\n  };\\n  if (!checkIfURLSearchParamsSupported()) {\\n    polyfillURLSearchParams();\\n  }\\n  var proto = global.URLSearchParams.prototype;\\n  if (typeof proto.sort !== 'function') {\\n    proto.sort = function () {\\n      var _this = this;\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push([name, value]);\\n        if (!_this._entries) {\\n          _this.delete(name);\\n        }\\n      });\\n      items.sort(function (a, b) {\\n        if (a[0] < b[0]) {\\n          return -1;\\n        } else if (a[0] > b[0]) {\\n          return +1;\\n        } else {\\n          return 0;\\n        }\\n      });\\n      if (_this._entries) {\\n        // force reset because IE keeps keys index\\n        _this._entries = {};\\n      }\\n      for (var i = 0; i < items.length; i++) {\\n        this.append(items[i][0], items[i][1]);\\n      }\\n    };\\n  }\\n  if (typeof proto._fromString !== 'function') {\\n    Object.defineProperty(proto, '_fromString', {\\n      enumerable: false,\\n      configurable: false,\\n      writable: false,\\n      value: function (searchString) {\\n        if (this._entries) {\\n          this._entries = {};\\n        } else {\\n          var keys = [];\\n          this.forEach(function (value, name) {\\n            keys.push(name);\\n          });\\n          for (var i = 0; i < keys.length; i++) {\\n            this.delete(keys[i]);\\n          }\\n        }\\n        searchString = searchString.replace(/^\\\\?/, '');\\n        var attributes = searchString.split('&');\\n        var attribute;\\n        for (var i = 0; i < attributes.length; i++) {\\n          attribute = attributes[i].split('=');\\n          this.append(deserializeParam(attribute[0]), attribute.length > 1 ? deserializeParam(attribute[1]) : '');\\n        }\\n      }\\n    });\\n  }\\n\\n  // HTMLAnchorElement\\n})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n(function (global) {\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\n\\n  var checkIfURLIsSupported = function () {\\n    try {\\n      var u = new global.URL('b', 'http://a');\\n      u.pathname = 'c d';\\n      return u.href === 'http://a/c%20d' && u.searchParams;\\n    } catch (e) {\\n      return false;\\n    }\\n  };\\n  var polyfillURL = function () {\\n    var _URL = global.URL;\\n    var URL = function (url, base) {\\n      if (typeof url !== 'string') url = String(url);\\n      if (base && typeof base !== 'string') base = String(base);\\n\\n      // Only create another document if the base is different from current location.\\n      var doc = document,\\n        baseElement;\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\n        base = base.toLowerCase();\\n        doc = document.implementation.createHTMLDocument('');\\n        baseElement = doc.createElement('base');\\n        baseElement.href = base;\\n        doc.head.appendChild(baseElement);\\n        try {\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\n        } catch (err) {\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\n        }\\n      }\\n      var anchorElement = doc.createElement('a');\\n      anchorElement.href = url;\\n      if (baseElement) {\\n        doc.body.appendChild(anchorElement);\\n        anchorElement.href = anchorElement.href; // force href to refresh\\n      }\\n\\n      var inputElement = doc.createElement('input');\\n      inputElement.type = 'url';\\n      inputElement.value = url;\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || !inputElement.checkValidity() && !base) {\\n        throw new TypeError('Invalid URL');\\n      }\\n      Object.defineProperty(this, '_anchorElement', {\\n        value: anchorElement\\n      });\\n\\n      // create a linked searchParams which reflect its changes on URL\\n      var searchParams = new global.URLSearchParams(this.search);\\n      var enableSearchUpdate = true;\\n      var enableSearchParamsUpdate = true;\\n      var _this = this;\\n      ['append', 'delete', 'set'].forEach(function (methodName) {\\n        var method = searchParams[methodName];\\n        searchParams[methodName] = function () {\\n          method.apply(searchParams, arguments);\\n          if (enableSearchUpdate) {\\n            enableSearchParamsUpdate = false;\\n            _this.search = searchParams.toString();\\n            enableSearchParamsUpdate = true;\\n          }\\n        };\\n      });\\n      Object.defineProperty(this, 'searchParams', {\\n        value: searchParams,\\n        enumerable: true\\n      });\\n      var search = void 0;\\n      Object.defineProperty(this, '_updateSearchParams', {\\n        enumerable: false,\\n        configurable: false,\\n        writable: false,\\n        value: function () {\\n          if (this.search !== search) {\\n            search = this.search;\\n            if (enableSearchParamsUpdate) {\\n              enableSearchUpdate = false;\\n              this.searchParams._fromString(this.search);\\n              enableSearchUpdate = true;\\n            }\\n          }\\n        }\\n      });\\n    };\\n    var proto = URL.prototype;\\n    var linkURLWithAnchorAttribute = function (attributeName) {\\n      Object.defineProperty(proto, attributeName, {\\n        get: function () {\\n          return this._anchorElement[attributeName];\\n        },\\n        set: function (value) {\\n          this._anchorElement[attributeName] = value;\\n        },\\n        enumerable: true\\n      });\\n    };\\n    ['hash', 'host', 'hostname', 'port', 'protocol'].forEach(function (attributeName) {\\n      linkURLWithAnchorAttribute(attributeName);\\n    });\\n    Object.defineProperty(proto, 'search', {\\n      get: function () {\\n        return this._anchorElement['search'];\\n      },\\n      set: function (value) {\\n        this._anchorElement['search'] = value;\\n        this._updateSearchParams();\\n      },\\n      enumerable: true\\n    });\\n    Object.defineProperties(proto, {\\n      'toString': {\\n        get: function () {\\n          var _this = this;\\n          return function () {\\n            return _this.href;\\n          };\\n        }\\n      },\\n      'href': {\\n        get: function () {\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\n        },\\n        set: function (value) {\\n          this._anchorElement.href = value;\\n          this._updateSearchParams();\\n        },\\n        enumerable: true\\n      },\\n      'pathname': {\\n        get: function () {\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\n        },\\n        set: function (value) {\\n          this._anchorElement.pathname = value;\\n        },\\n        enumerable: true\\n      },\\n      'origin': {\\n        get: function () {\\n          // get expected port from protocol\\n          var expectedPort = {\\n            'http:': 80,\\n            'https:': 443,\\n            'ftp:': 21\\n          }[this._anchorElement.protocol];\\n          // add port to origin if, expected port is different than actual port\\n          // and it is not empty f.e http://foo:8080\\n          // 8080 != 80 && 8080 != ''\\n          var addPortToOrigin = this._anchorElement.port != expectedPort && this._anchorElement.port !== '';\\n          return this._anchorElement.protocol + '//' + this._anchorElement.hostname + (addPortToOrigin ? ':' + this._anchorElement.port : '');\\n        },\\n        enumerable: true\\n      },\\n      'password': {\\n        // TODO\\n        get: function () {\\n          return '';\\n        },\\n        set: function (value) {},\\n        enumerable: true\\n      },\\n      'username': {\\n        // TODO\\n        get: function () {\\n          return '';\\n        },\\n        set: function (value) {},\\n        enumerable: true\\n      }\\n    });\\n    URL.createObjectURL = function (blob) {\\n      return _URL.createObjectURL.apply(_URL, arguments);\\n    };\\n    URL.revokeObjectURL = function (url) {\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\n    };\\n    global.URL = URL;\\n  };\\n  if (!checkIfURLIsSupported()) {\\n    polyfillURL();\\n  }\\n  if (global.location !== void 0 && !('origin' in global.location)) {\\n    var getOrigin = function () {\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? ':' + global.location.port : '');\\n    };\\n    try {\\n      Object.defineProperty(global.location, 'origin', {\\n        get: getOrigin,\\n        enumerable: true\\n      });\\n    } catch (e) {\\n      setInterval(function () {\\n        global.location.origin = getOrigin();\\n      }, 100);\\n    }\\n  }\\n})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n\\nfunction _defineProperty$1(obj, key, value) {\\n  key = _toPropertyKey(key);\\n  if (key in obj) {\\n    Object.defineProperty(obj, key, {\\n      value: value,\\n      enumerable: true,\\n      configurable: true,\\n      writable: true\\n    });\\n  } else {\\n    obj[key] = value;\\n  }\\n  return obj;\\n}\\nfunction _toPrimitive(input, hint) {\\n  if (typeof input !== \\\"object\\\" || input === null) return input;\\n  var prim = input[Symbol.toPrimitive];\\n  if (prim !== undefined) {\\n    var res = prim.call(input, hint || \\\"default\\\");\\n    if (typeof res !== \\\"object\\\") return res;\\n    throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n  }\\n  return (hint === \\\"string\\\" ? String : Number)(input);\\n}\\nfunction _toPropertyKey(arg) {\\n  var key = _toPrimitive(arg, \\\"string\\\");\\n  return typeof key === \\\"symbol\\\" ? key : String(key);\\n}\\n\\nfunction _classCallCheck(e, t) {\\n  if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n}\\nfunction _defineProperties(e, t) {\\n  for (var n = 0; n < t.length; n++) {\\n    var r = t[n];\\n    r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n  }\\n}\\nfunction _createClass(e, t, n) {\\n  return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n}\\nfunction _defineProperty(e, t, n) {\\n  return t in e ? Object.defineProperty(e, t, {\\n    value: n,\\n    enumerable: !0,\\n    configurable: !0,\\n    writable: !0\\n  }) : e[t] = n, e;\\n}\\nfunction ownKeys(e, t) {\\n  var n = Object.keys(e);\\n  if (Object.getOwnPropertySymbols) {\\n    var r = Object.getOwnPropertySymbols(e);\\n    t && (r = r.filter(function (t) {\\n      return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n    })), n.push.apply(n, r);\\n  }\\n  return n;\\n}\\nfunction _objectSpread2(e) {\\n  for (var t = 1; t < arguments.length; t++) {\\n    var n = null != arguments[t] ? arguments[t] : {};\\n    t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n      _defineProperty(e, t, n[t]);\\n    }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n      Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n    });\\n  }\\n  return e;\\n}\\nvar defaults$1 = {\\n  addCSS: !0,\\n  thumbWidth: 15,\\n  watch: !0\\n};\\nfunction matches$1(e, t) {\\n  return function () {\\n    return Array.from(document.querySelectorAll(t)).includes(this);\\n  }.call(e, t);\\n}\\nfunction trigger(e, t) {\\n  if (e && t) {\\n    var n = new Event(t, {\\n      bubbles: !0\\n    });\\n    e.dispatchEvent(n);\\n  }\\n}\\nvar getConstructor$1 = function (e) {\\n    return null != e ? e.constructor : null;\\n  },\\n  instanceOf$1 = function (e, t) {\\n    return !!(e && t && e instanceof t);\\n  },\\n  isNullOrUndefined$1 = function (e) {\\n    return null == e;\\n  },\\n  isObject$1 = function (e) {\\n    return getConstructor$1(e) === Object;\\n  },\\n  isNumber$1 = function (e) {\\n    return getConstructor$1(e) === Number && !Number.isNaN(e);\\n  },\\n  isString$1 = function (e) {\\n    return getConstructor$1(e) === String;\\n  },\\n  isBoolean$1 = function (e) {\\n    return getConstructor$1(e) === Boolean;\\n  },\\n  isFunction$1 = function (e) {\\n    return getConstructor$1(e) === Function;\\n  },\\n  isArray$1 = function (e) {\\n    return Array.isArray(e);\\n  },\\n  isNodeList$1 = function (e) {\\n    return instanceOf$1(e, NodeList);\\n  },\\n  isElement$1 = function (e) {\\n    return instanceOf$1(e, Element);\\n  },\\n  isEvent$1 = function (e) {\\n    return instanceOf$1(e, Event);\\n  },\\n  isEmpty$1 = function (e) {\\n    return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n  },\\n  is$1 = {\\n    nullOrUndefined: isNullOrUndefined$1,\\n    object: isObject$1,\\n    number: isNumber$1,\\n    string: isString$1,\\n    boolean: isBoolean$1,\\n    function: isFunction$1,\\n    array: isArray$1,\\n    nodeList: isNodeList$1,\\n    element: isElement$1,\\n    event: isEvent$1,\\n    empty: isEmpty$1\\n  };\\nfunction getDecimalPlaces(e) {\\n  var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n  return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n}\\nfunction round(e, t) {\\n  if (1 > t) {\\n    var n = getDecimalPlaces(t);\\n    return parseFloat(e.toFixed(n));\\n  }\\n  return Math.round(e / t) * t;\\n}\\nvar RangeTouch = function () {\\n  function e(t, n) {\\n    _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n  }\\n  return _createClass(e, [{\\n    key: \\\"init\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n    }\\n  }, {\\n    key: \\\"destroy\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n    }\\n  }, {\\n    key: \\\"listeners\\\",\\n    value: function (e) {\\n      var t = this,\\n        n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n      [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n        t.element[n](e, function (e) {\\n          return t.set(e);\\n        }, !1);\\n      });\\n    }\\n  }, {\\n    key: \\\"get\\\",\\n    value: function (t) {\\n      if (!e.enabled || !is$1.event(t)) return null;\\n      var n,\\n        r = t.target,\\n        i = t.changedTouches[0],\\n        o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n        s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n        u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n        c = r.getBoundingClientRect(),\\n        a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n      return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n    }\\n  }, {\\n    key: \\\"set\\\",\\n    value: function (t) {\\n      e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n    }\\n  }], [{\\n    key: \\\"setup\\\",\\n    value: function (t) {\\n      var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n        r = null;\\n      if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n      var i = _objectSpread2({}, defaults$1, {}, n);\\n      if (is$1.string(t) && i.watch) {\\n        var o = new MutationObserver(function (n) {\\n          Array.from(n).forEach(function (n) {\\n            Array.from(n.addedNodes).forEach(function (n) {\\n              is$1.element(n) && matches$1(n, t) && new e(n, i);\\n            });\\n          });\\n        });\\n        o.observe(document.body, {\\n          childList: !0,\\n          subtree: !0\\n        });\\n      }\\n      return r.map(function (t) {\\n        return new e(t, n);\\n      });\\n    }\\n  }, {\\n    key: \\\"enabled\\\",\\n    get: function () {\\n      return \\\"ontouchstart\\\" in document.documentElement;\\n    }\\n  }]), e;\\n}();\\n\\n// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = input => input === null || typeof input === 'undefined';\\nconst isObject = input => getConstructor(input) === Object;\\nconst isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = input => getConstructor(input) === String;\\nconst isBoolean = input => getConstructor(input) === Boolean;\\nconst isFunction = input => typeof input === 'function';\\nconst isArray = input => Array.isArray(input);\\nconst isWeakMap = input => instanceOf(input, WeakMap);\\nconst isNodeList = input => instanceOf(input, NodeList);\\nconst isTextNode = input => getConstructor(input) === Text;\\nconst isEvent = input => instanceOf(input, Event);\\nconst isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\nconst isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\nconst isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\nconst isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\nconst isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\nconst isUrl = input => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\nvar is = {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty\\n};\\n\\n// ==========================================================================\\nconst transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend'\\n  };\\n  const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nfunction repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\\n// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\nvar browser = {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos\\n};\\n\\n// ==========================================================================\\n\\n// Clone nested objects\\nfunction cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nfunction getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nfunction extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n  const source = sources.shift();\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n  Object.keys(source).forEach(key => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, {\\n          [key]: {}\\n        });\\n      }\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, {\\n        [key]: source[key]\\n      });\\n    }\\n  });\\n  return extend(target, ...sources);\\n}\\n\\n// ==========================================================================\\n\\n// Wrap an element\\nfunction wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets).reverse().forEach((element, index) => {\\n    const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n    // Cache the current parent and sibling.\\n    const parent = element.parentNode;\\n    const sibling = element.nextSibling;\\n\\n    // Wrap the element (is automatically removed from its current\\n    // parent).\\n    child.appendChild(element);\\n\\n    // If the element had a sibling, insert the wrapper before\\n    // the sibling to maintain the HTML structure; otherwise, just\\n    // append it to the parent.\\n    if (sibling) {\\n      parent.insertBefore(child, sibling);\\n    } else {\\n      parent.appendChild(child);\\n    }\\n  });\\n}\\n\\n// Set attributes\\nfunction setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nfunction createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nfunction insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nfunction insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nfunction removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nfunction emptyElement(element) {\\n  if (!is.element(element)) return;\\n  let {\\n    length\\n  } = element.childNodes;\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nfunction replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nfunction getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n  sel.split(',').forEach(s => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n        break;\\n    }\\n  });\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nfunction toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n  let hide = hidden;\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nfunction toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map(e => toggleClass(e, className, force));\\n  }\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n  return false;\\n}\\n\\n// Has class name\\nfunction hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nfunction matches(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n  const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nfunction closest$1(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n  const method = prototype.closest || closestElement;\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nfunction getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nfunction getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nfunction setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({\\n    preventScroll: true,\\n    focusVisible\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora'\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n    return {\\n      api,\\n      ui\\n    };\\n  },\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n    return false;\\n  })(),\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n};\\n\\n// ==========================================================================\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      }\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nfunction toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach(type => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({\\n        element,\\n        type,\\n        callback,\\n        options\\n      });\\n    }\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nfunction on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nfunction off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nfunction once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nfunction triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: {\\n      ...detail,\\n      plyr: this\\n    }\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nfunction unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach(item => {\\n      const {\\n        element,\\n        type,\\n        callback,\\n        options\\n      } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nfunction ready() {\\n  return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n}\\n\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nfunction silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Remove duplicates in an array\\nfunction dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nfunction closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n  return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n}\\n\\n// ==========================================================================\\n\\n// Check support for a CSS declaration\\nfunction supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n  ...out,\\n  [x / y]: [x, y]\\n}), {});\\n\\n// Validate an aspect ratio\\nfunction validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n  const ratio = is.array(input) ? input : input.split(':');\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nfunction reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n  const divider = getDivider(width, height);\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nfunction getAspectRatio(input) {\\n  const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({\\n      ratio\\n    } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const {\\n      videoWidth,\\n      videoHeight\\n    } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nfunction setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n  const {\\n    wrapper\\n  } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = 100 / x * y;\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n  return {\\n    padding,\\n    ratio\\n  };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nfunction roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nfunction getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\\n// ==========================================================================\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter(source => {\\n      const type = source.getAttribute('type');\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n      return support.mime.call(this, type);\\n    });\\n  },\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n  },\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const {\\n            currentTime,\\n            paused,\\n            preload,\\n            readyState,\\n            playbackRate\\n          } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input\\n        });\\n      }\\n    });\\n  },\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Generate a random ID\\nfunction generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nfunction format(input, ...args) {\\n  if (is.empty(input)) return input;\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nfunction getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n  return (current / max * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nconst replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nconst toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nfunction toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nfunction toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nfunction stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nfunction getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\\n// ==========================================================================\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube'\\n};\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n    let string = getDeep(config.i18n, key);\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n      return '';\\n    }\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title\\n    };\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n    return string;\\n  }\\n};\\n\\nclass Storage {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"get\\\", key => {\\n      if (!Storage.supported || !this.enabled) {\\n        return null;\\n      }\\n      const store = window.localStorage.getItem(this.key);\\n      if (is.empty(store)) {\\n        return null;\\n      }\\n      const json = JSON.parse(store);\\n      return is.string(key) && key.length ? json[key] : json;\\n    });\\n    _defineProperty$1(this, \\\"set\\\", object => {\\n      // Bail if we don't have localStorage support or it's disabled\\n      if (!Storage.supported || !this.enabled) {\\n        return;\\n      }\\n\\n      // Can only store objectst\\n      if (!is.object(object)) {\\n        return;\\n      }\\n\\n      // Get current storage\\n      let storage = this.get();\\n\\n      // Default to empty object\\n      if (is.empty(storage)) {\\n        storage = {};\\n      }\\n\\n      // Update the working copy of the values\\n      extend(storage, object);\\n\\n      // Update storage\\n      try {\\n        window.localStorage.setItem(this.key, JSON.stringify(storage));\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    });\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nfunction fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Load an external SVG sprite\\nfunction loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url).then(result => {\\n      if (is.empty(result)) {\\n        return;\\n      }\\n      if (useStorage) {\\n        try {\\n          window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n            content: result\\n          }));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      }\\n      update(container, result);\\n    }).catch(() => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Time helpers\\nconst getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\nconst getMinutes = value => Math.trunc(value / 60 % 60, 10);\\nconst getSeconds = value => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nfunction formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = value => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\\n// ==========================================================================\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n    return {\\n      url: this.config.iconUrl,\\n      cors\\n    };\\n  },\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume)\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration)\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n      return false;\\n    }\\n  },\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(icon, extend(attributes, {\\n      'aria-hidden': 'true',\\n      focusable: 'false'\\n    }));\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n    return icon;\\n  },\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = {\\n      ...attr,\\n      class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n    };\\n    return createElement('span', attributes, text);\\n  },\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value\\n    });\\n    badge.appendChild(createElement('span', {\\n      class: this.config.classNames.menu.badge\\n    }, text));\\n    return badge;\\n  },\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null\\n    };\\n    ['element', 'icon', 'label'].forEach(key => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n        class: 'icon--pressed'\\n      }));\\n      button.appendChild(controls.createIcon.call(this, props.icon, {\\n        class: 'icon--not-pressed'\\n      }));\\n\\n      // Label/Tooltip\\n      button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n        class: 'label--pressed'\\n      }));\\n      button.appendChild(controls.createLabel.call(this, props.label, {\\n        class: 'label--not-pressed'\\n      }));\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n    return button;\\n  },\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n      type: 'range',\\n      min: 0,\\n      max: 100,\\n      step: 0.01,\\n      value: 0,\\n      autocomplete: 'off',\\n      // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n      role: 'slider',\\n      'aria-label': i18n.get(type, this.config),\\n      'aria-valuemin': 0,\\n      'aria-valuemax': 100,\\n      'aria-valuenow': 0\\n    }, attributes));\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n    return input;\\n  },\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n      min: 0,\\n      max: 100,\\n      value: 0,\\n      role: 'progressbar',\\n      'aria-hidden': true\\n    }, attributes));\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered'\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n    this.elements.display[type] = progress;\\n    return progress;\\n  },\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n    const container = createElement('div', extend(attributes, {\\n      class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n      'aria-label': i18n.get(type, this.config),\\n      role: 'timer'\\n    }), '00:00');\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n    return container;\\n  },\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(this, menuItem, 'keydown keyup', event => {\\n      // We only care about space and ⬆️ ⬇️️ ➡️\\n      if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Prevent play / seek\\n      event.preventDefault();\\n      event.stopPropagation();\\n\\n      // We're just here to prevent the keydown bubbling\\n      if (event.type === 'keydown') {\\n        return;\\n      }\\n      const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n      // Show the respective menu\\n      if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n        controls.showMenuPanel.call(this, type, true);\\n      } else {\\n        let target;\\n        if (event.key !== ' ') {\\n          if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n            target = menuItem.nextElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.firstElementChild;\\n            }\\n          } else {\\n            target = menuItem.previousElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.lastElementChild;\\n            }\\n          }\\n          setFocus.call(this, target, true);\\n        }\\n      }\\n    }, false);\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', event => {\\n      if (event.key !== 'Return') return;\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n  // Create a settings menu item\\n  createMenuItem({\\n    value,\\n    list,\\n    type,\\n    title,\\n    badge = null,\\n    checked = false\\n  }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n    const menuItem = createElement('button', extend(attributes, {\\n      type: 'button',\\n      role: 'menuitemradio',\\n      class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n      'aria-checked': checked,\\n      value\\n    }));\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n        }\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      }\\n    });\\n    this.listeners.bind(menuItem, 'click keyup', event => {\\n      if (is.keyboardEvent(event) && event.key !== ' ') {\\n        return;\\n      }\\n      event.preventDefault();\\n      event.stopPropagation();\\n      menuItem.checked = true;\\n      switch (type) {\\n        case 'language':\\n          this.currentTrack = Number(value);\\n          break;\\n        case 'quality':\\n          this.quality = value;\\n          break;\\n        case 'speed':\\n          this.speed = parseFloat(value);\\n          break;\\n      }\\n      controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n    }, type, false);\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n    list.appendChild(menuItem);\\n  },\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n    return formatTime(time, forceHours, inverted);\\n  },\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n    let value = 0;\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n          break;\\n      }\\n    }\\n  },\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n  },\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    var _this$config$markers, _this$config$markers$;\\n    // Bail if setting not true\\n    if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n      return;\\n    }\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = show => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n    if (is.event(event)) {\\n      percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n    const time = this.duration / 100 * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n      time: t\\n    }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n          return label;\\n        }\\n        return toTitleCase(value);\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n      default:\\n        return null;\\n    }\\n  },\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = quality => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n      if (!label.length) {\\n        return null;\\n      }\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality.sort((a, b) => {\\n      const sorting = this.config.quality.options;\\n      return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n    }).forEach(quality => {\\n      controls.createMenuItem.call(this, {\\n        value: quality,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'quality', quality),\\n        badge: getBadge(quality)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n         const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n         // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n         // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n         // Empty the menu\\n        emptyElement(list);\\n         options.forEach(option => {\\n            const item = createElement('li');\\n             const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n             if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n             item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language'\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language'\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach(speed => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const {\\n      buttons\\n    } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n    let target = pane;\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n    }\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const {\\n      popup\\n    } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const {\\n      hidden\\n    } = popup;\\n    let show = hidden;\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n    return {\\n      width,\\n      height\\n    };\\n  },\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find(node => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = event => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = {\\n      class: 'plyr__controls__item'\\n    };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`\\n        });\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(createRange.call(this, 'seek', {\\n          id: `plyr-seek-${data.id}`\\n        }));\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement('span', {\\n            class: this.config.classNames.tooltip\\n          }, '00:00');\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let {\\n          volume\\n        } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__volume`.trim()\\n          }));\\n          this.elements.volume = volume;\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n            id: `plyr-volume-${data.id}`\\n          })));\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement('div', extend({}, defaultAttributes, {\\n          class: `${defaultAttributes.class} plyr__menu`.trim(),\\n          hidden: ''\\n        }));\\n        wrapper.appendChild(createButton.call(this, 'settings', {\\n          'aria-haspopup': true,\\n          'aria-controls': `plyr-settings-${data.id}`,\\n          'aria-expanded': false\\n        }));\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: ''\\n        });\\n        const inner = createElement('div');\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu'\\n        });\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach(type => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n            role: 'menuitem',\\n            'aria-haspopup': true,\\n            hidden: ''\\n          }));\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: ''\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(createElement('span', {\\n            'aria-hidden': true\\n          }, i18n.get(type, this.config)));\\n\\n          // Screen reader label\\n          backButton.appendChild(createElement('span', {\\n            class: this.config.classNames.hidden\\n          }, i18n.get('menuBack', this.config)));\\n\\n          // Go back via keyboard\\n          on.call(this, pane, 'keydown', event => {\\n            if (event.key !== 'ArrowLeft') return;\\n\\n            // Prevent seek\\n            event.preventDefault();\\n            event.stopPropagation();\\n\\n            // Show the respective menu\\n            showMenuPanel.call(this, 'home', true);\\n          }, false);\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(createElement('div', {\\n            role: 'menu'\\n          }));\\n          inner.appendChild(pane);\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank'\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n        const {\\n          download\\n        } = this.config.urls;\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider\\n          });\\n        }\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n    setSpeedMenu.call(this);\\n    return container;\\n  },\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this)\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = input => {\\n      let result = input;\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = button => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          }\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n        if (is.array(button) || is.nodeList(button)) {\\n          Array.from(button).filter(Boolean).forEach(addProperty);\\n        } else {\\n          addProperty(button);\\n        }\\n      });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const {\\n        classNames,\\n        selectors\\n      } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n      Array.from(labels).forEach(label => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n  // Add markers\\n  setMarkers() {\\n    var _this$config$markers2, _this$config$markers3;\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n      time\\n    }) => time > 0 && time < this.duration);\\n    if (!(points !== null && points !== void 0 && points.length)) return;\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach(point => {\\n      const markerElement = createElement('span', {\\n        class: this.config.classNames.marker\\n      }, '');\\n      const left = `${point.time / this.duration * 100}%`;\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement('span', {\\n        class: this.config.classNames.tooltip\\n      }, '');\\n      containerFragment.appendChild(tipElement);\\n    }\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement\\n    };\\n    this.elements.progress.appendChild(containerFragment);\\n  }\\n};\\n\\n// ==========================================================================\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nfunction parseUrl(input, safe = true) {\\n  let url = input;\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nfunction buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n  return params;\\n}\\n\\n// ==========================================================================\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n      // Clear menu and hide\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n      Array.from(elements).forEach(track => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n        if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n          fetch(src, 'blob').then(blob => {\\n            track.setAttribute('src', window.URL.createObjectURL(blob));\\n          }).catch(() => {\\n            removeElement(track);\\n          });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({\\n        active\\n      } = this.config.captions);\\n    }\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const {\\n      active,\\n      language,\\n      meta,\\n      currentTrackNode\\n    } = this.captions;\\n    const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks.filter(track => !meta.get(track)).forEach(track => {\\n        this.debug.log('Track added', track);\\n\\n        // Attempt to store if the original dom element was \\\"default\\\"\\n        meta.set(track, {\\n          default: track.mode === 'showing'\\n        });\\n\\n        // Turn off native caption rendering to avoid double captions\\n        // Note: mode='hidden' forces a track to download. To ensure every track\\n        // isn't downloaded at once, only 'showing' tracks should be reassigned\\n        // eslint-disable-next-line no-param-reassign\\n        if (track.mode === 'showing') {\\n          // eslint-disable-next-line no-param-reassign\\n          track.mode = 'hidden';\\n        }\\n\\n        // Add event listener for cue changes\\n        on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n      });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    const {\\n      toggled\\n    } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({\\n          captions: active\\n        });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const {\\n        language\\n      } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({\\n          language\\n        });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n    languages.every(language => {\\n      track = sorted.find(t => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n      return i18n.get('enabled', this.config);\\n    }\\n    return i18n.get('disabled', this.config);\\n  },\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n      cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n  // Custom media title\\n  title: '',\\n  // Logging to console\\n  debug: false,\\n  // Auto play (if supported)\\n  autoplay: false,\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n  // Pass a custom duration\\n  duration: null,\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n  // Auto hide the controls\\n  hideControls: true,\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null\\n  },\\n  // Set loops\\n  loop: {\\n    active: false\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n  },\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false\\n  },\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true\\n  },\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false\\n  },\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true,\\n    // Allow fullscreen?\\n    fallback: true,\\n    // Fallback using full viewport/window\\n    iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr'\\n  },\\n  // Default controls\\n  controls: ['play-large',\\n  // 'restart',\\n  // 'rewind',\\n  'play',\\n  // 'fast-forward',\\n  'progress', 'current-time',\\n  // 'duration',\\n  'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n  // 'download',\\n  'fullscreen'],\\n  settings: ['captions', 'quality', 'speed'],\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD'\\n    }\\n  },\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}'\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\\n    }\\n  },\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null\\n  },\\n  // Events to watch and bubble\\n  events: [\\n  // Events to watch on HTML5 media elements and bubble\\n  // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n  'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n  // Custom events\\n  'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n  // YouTube\\n  'statechange',\\n  // Quality\\n  'qualitychange',\\n  // Ads\\n  'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls'\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]'\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]'\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop',\\n      // Used later\\n      volume: '.plyr__volume--display'\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption'\\n  },\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time'\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open'\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active'\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback'\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active'\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active'\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n    }\\n  },\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash'\\n    }\\n  },\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: ''\\n  },\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: ''\\n  },\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null,\\n    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false\\n  },\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0,\\n    // No related vids\\n    showinfo: 0,\\n    // Hide info\\n    iv_load_policy: 3,\\n    // Hide annotations\\n    modestbranding: 1,\\n    // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: []\\n  },\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: []\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nconst pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline'\\n};\\n\\n// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nconst providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo'\\n};\\nconst types = {\\n  audio: 'audio',\\n  video: 'video'\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nfunction getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n  return null;\\n}\\n\\n// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\nclass Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"onChange\\\", () => {\\n      if (!this.supported) return;\\n\\n      // Update toggle button\\n      const button = this.player.elements.buttons.fullscreen;\\n      if (is.element(button)) {\\n        button.pressed = this.active;\\n      }\\n\\n      // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n      const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n      // Trigger an event\\n      triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n    });\\n    _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n      // Store or restore scroll position\\n      if (toggle) {\\n        this.scrollPosition = {\\n          x: window.scrollX ?? 0,\\n          y: window.scrollY ?? 0\\n        };\\n      } else {\\n        window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n      }\\n\\n      // Toggle scroll\\n      document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n      // Toggle class hook\\n      toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n      // Force full viewport on iPhone X+\\n      if (browser.isIos) {\\n        let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n        const property = 'viewport-fit=cover';\\n\\n        // Inject the viewport meta if required\\n        if (!viewport) {\\n          viewport = document.createElement('meta');\\n          viewport.setAttribute('name', 'viewport');\\n        }\\n\\n        // Check if the property already exists\\n        const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n        if (toggle) {\\n          this.cleanupViewport = !hasProperty;\\n          if (!hasProperty) viewport.content += `,${property}`;\\n        } else if (this.cleanupViewport) {\\n          viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n        }\\n      }\\n\\n      // Toggle button and fire events\\n      this.onChange();\\n    });\\n    // Trap focus inside container\\n    _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n      // Bail if iOS/iPadOS, not active, not the tab key\\n      if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n      // Get the current focused element\\n      const focused = document.activeElement;\\n      const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n      const [first] = focusable;\\n      const last = focusable[focusable.length - 1];\\n      if (focused === last && !event.shiftKey) {\\n        // Move focus to first element that can be tabbed if Shift isn't used\\n        first.focus();\\n        event.preventDefault();\\n      } else if (focused === first && event.shiftKey) {\\n        // Move focus to last element that can be tabbed if Shift is used\\n        last.focus();\\n        event.preventDefault();\\n      }\\n    });\\n    // Update UI\\n    _defineProperty$1(this, \\\"update\\\", () => {\\n      if (this.supported) {\\n        let mode;\\n        if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n        this.player.debug.log(`${mode} fullscreen enabled`);\\n      } else {\\n        this.player.debug.log('Fullscreen not supported and fallback disabled');\\n      }\\n\\n      // Add styling hook to show button\\n      toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n    });\\n    // Make an element fullscreen\\n    _defineProperty$1(this, \\\"enter\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen doesn't need the request step\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.requestFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(true);\\n      } else if (!this.prefix) {\\n        this.target.requestFullscreen({\\n          navigationUI: 'hide'\\n        });\\n      } else if (!is.empty(this.prefix)) {\\n        this.target[`${this.prefix}Request${this.property}`]();\\n      }\\n    });\\n    // Bail from fullscreen\\n    _defineProperty$1(this, \\\"exit\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.exitFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n        silencePromise(this.player.play());\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(false);\\n      } else if (!this.prefix) {\\n        (document.cancelFullScreen || document.exitFullscreen).call(document);\\n      } else if (!is.empty(this.prefix)) {\\n        const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n        document[`${this.prefix}${action}${this.property}`]();\\n      }\\n    });\\n    // Toggle state\\n    _defineProperty$1(this, \\\"toggle\\\", () => {\\n      if (!this.active) this.enter();else this.exit();\\n    });\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = {\\n      x: 0,\\n      y: 0\\n    };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n      // TODO: Filter for target??\\n      this.onChange();\\n    });\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n    prefixes.some(pre => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n      return false;\\n    });\\n    return value;\\n  }\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n    // Fullscreen is enabled in config\\n    this.player.config.fullscreen.enabled,\\n    // Must be a video\\n    this.player.isVideo,\\n    // Either native is supported or fallback enabled\\n    Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n    // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n    // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n    !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n    const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n}\\n\\n// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nfunction loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n    Object.assign(image, {\\n      onload: handler,\\n      onerror: handler,\\n      src\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach(button => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return ready.call(this)\\n    // Load image\\n    .then(() => loadImage(poster)).catch(error => {\\n      // Hide poster on error unless it's been set by another call\\n      if (poster === this.poster) {\\n        ui.togglePoster.call(this, false);\\n      }\\n      // Rethrow\\n      throw error;\\n    }).then(() => {\\n      // Prevent race conditions\\n      if (poster !== this.poster) {\\n        throw new Error('setPoster cancelled by later call to setPoster');\\n      }\\n    }).then(() => {\\n      Object.assign(this.elements.poster.style, {\\n        backgroundImage: `url('${poster}')`,\\n        // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n        backgroundSize: ''\\n      });\\n      ui.togglePoster.call(this, true);\\n      return poster;\\n    });\\n  },\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach(target => {\\n      Object.assign(target, {\\n        pressed: this.playing\\n      });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(() => {\\n      // Update progress bar loading class state\\n      toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n      // Update controls visibility\\n      ui.toggleControls.call(this);\\n    }, this.loading ? 250 : 0);\\n  },\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const {\\n      controls: controlsElement\\n    } = this.elements;\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n    }\\n  },\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({\\n      ...this.media.style\\n    })\\n    // We're only fussed about Plyr specific properties\\n    .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n      // Set on the container\\n      this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n      // Clean up from media element\\n      this.media.style.removeProperty(key);\\n    });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  }\\n};\\n\\nclass Listeners {\\n  constructor(_player) {\\n    // Device is touch enabled\\n    _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      player.touch = true;\\n\\n      // Add touch class\\n      toggleClass(elements.container, player.config.classNames.isTouch, true);\\n    });\\n    // Global window & document listeners\\n    _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n      const {\\n        player\\n      } = this;\\n\\n      // Keyboard shortcuts\\n      if (player.config.keyboard.global) {\\n        toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n      }\\n\\n      // Click anywhere closes menu\\n      toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n      // Detect touch by events\\n      once.call(player, document.body, 'touchstart', this.firstTouch);\\n    });\\n    // Container listeners\\n    _defineProperty$1(this, \\\"container\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        config,\\n        elements,\\n        timers\\n      } = player;\\n\\n      // Keyboard shortcuts\\n      if (!config.keyboard.global && config.keyboard.focused) {\\n        on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n      }\\n\\n      // Toggle controls on mouse events and entering fullscreen\\n      on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n        const {\\n          controls: controlsElement\\n        } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Set a gutter for Vimeo\\n      const setGutter = () => {\\n        if (!player.isVimeo || player.config.vimeo.premium) {\\n          return;\\n        }\\n        const target = elements.wrapper;\\n        const {\\n          active\\n        } = player.fullscreen;\\n        const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n        const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n        // If not active, remove styles\\n        if (!active) {\\n          if (useNativeAspectRatio) {\\n            target.style.width = null;\\n            target.style.height = null;\\n          } else {\\n            target.style.maxWidth = null;\\n            target.style.margin = null;\\n          }\\n          return;\\n        }\\n\\n        // Determine which dimension will overflow and constrain view\\n        const [viewportWidth, viewportHeight] = getViewportSize();\\n        const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n        if (useNativeAspectRatio) {\\n          target.style.width = overflow ? 'auto' : '100%';\\n          target.style.height = overflow ? '100%' : 'auto';\\n        } else {\\n          target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n          target.style.margin = overflow ? '0 auto' : null;\\n        }\\n      };\\n\\n      // Handle resizing\\n      const resized = () => {\\n        clearTimeout(timers.resized);\\n        timers.resized = setTimeout(setGutter, 50);\\n      };\\n      on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n        const {\\n          target\\n        } = player.fullscreen;\\n\\n        // Ignore events not from target\\n        if (target !== elements.container) {\\n          return;\\n        }\\n\\n        // If it's not an embed and no ratio specified\\n        if (!player.isEmbed && is.empty(player.config.ratio)) {\\n          return;\\n        }\\n\\n        // Set Vimeo gutter\\n        setGutter();\\n\\n        // Watch for resizes\\n        const method = event.type === 'enterfullscreen' ? on : off;\\n        method.call(player, window, 'resize', resized);\\n      });\\n    });\\n    // Listen for media events\\n    _defineProperty$1(this, \\\"media\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n\\n      // Time change on media\\n      on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n      // Display duration\\n      on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n      // Handle the media finishing\\n      on.call(player, player.media, 'ended', () => {\\n        // Show poster on end\\n        if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n          // Restart\\n          player.restart();\\n\\n          // Call pause otherwise IE11 will start playing the video again\\n          player.pause();\\n        }\\n      });\\n\\n      // Check for buffer progress\\n      on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n      // Handle volume changes\\n      on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n      // Handle play/pause\\n      on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n      // Loading state\\n      on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n      // Click video\\n      if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n        // Re-fetch the wrapper\\n        const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n        // Bail if there's no wrapper (this should never happen)\\n        if (!is.element(wrapper)) {\\n          return;\\n        }\\n\\n        // On click play, pause or restart\\n        on.call(player, elements.container, 'click', event => {\\n          const targets = [elements.container, wrapper];\\n\\n          // Ignore if click if not container or in video wrapper\\n          if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n            return;\\n          }\\n\\n          // Touch devices will just show controls (if hidden)\\n          if (player.touch && player.config.hideControls) {\\n            return;\\n          }\\n          if (player.ended) {\\n            this.proxy(event, player.restart, 'restart');\\n            this.proxy(event, () => {\\n              silencePromise(player.play());\\n            }, 'play');\\n          } else {\\n            this.proxy(event, () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          }\\n        });\\n      }\\n\\n      // Disable right click\\n      if (player.supported.ui && player.config.disableContextMenu) {\\n        on.call(player, elements.wrapper, 'contextmenu', event => {\\n          event.preventDefault();\\n        }, false);\\n      }\\n\\n      // Volume change\\n      on.call(player, player.media, 'volumechange', () => {\\n        // Save to storage\\n        player.storage.set({\\n          volume: player.volume,\\n          muted: player.muted\\n        });\\n      });\\n\\n      // Speed change\\n      on.call(player, player.media, 'ratechange', () => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'speed');\\n\\n        // Save to storage\\n        player.storage.set({\\n          speed: player.speed\\n        });\\n      });\\n\\n      // Quality change\\n      on.call(player, player.media, 'qualitychange', event => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n      });\\n\\n      // Update download link when ready and if quality changes\\n      on.call(player, player.media, 'ready qualitychange', () => {\\n        controls.setDownloadUrl.call(player);\\n      });\\n\\n      // Proxy events to container\\n      // Bubble up key events for Edge\\n      const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n      on.call(player, player.media, proxyEvents, event => {\\n        let {\\n          detail = {}\\n        } = event;\\n\\n        // Get error details from media\\n        if (event.type === 'error') {\\n          detail = player.media.error;\\n        }\\n        triggerEvent.call(player, elements.container, event.type, true, detail);\\n      });\\n    });\\n    // Run default and custom handlers\\n    _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      let returned = true;\\n\\n      // Execute custom handler\\n      if (hasCustomHandler) {\\n        returned = customHandler.call(player, event);\\n      }\\n\\n      // Only call default handler if not prevented in custom handler\\n      if (returned !== false && is.function(defaultHandler)) {\\n        defaultHandler.call(player, event);\\n      }\\n    });\\n    // Trigger custom and default handlers\\n    _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n    });\\n    // Listen for control events\\n    _defineProperty$1(this, \\\"controls\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      // IE doesn't support input event, so we fallback to change\\n      const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n      // Play/pause toggle\\n      if (elements.buttons.play) {\\n        Array.from(elements.buttons.play).forEach(button => {\\n          this.bind(button, 'click', () => {\\n            silencePromise(player.togglePlay());\\n          }, 'play');\\n        });\\n      }\\n\\n      // Pause\\n      this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n      // Rewind\\n      this.bind(elements.buttons.rewind, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      }, 'rewind');\\n\\n      // Rewind\\n      this.bind(elements.buttons.fastForward, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      }, 'fastForward');\\n\\n      // Mute toggle\\n      this.bind(elements.buttons.mute, 'click', () => {\\n        player.muted = !player.muted;\\n      }, 'mute');\\n\\n      // Captions toggle\\n      this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n      // Download\\n      this.bind(elements.buttons.download, 'click', () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      }, 'download');\\n\\n      // Fullscreen toggle\\n      this.bind(elements.buttons.fullscreen, 'click', () => {\\n        player.fullscreen.toggle();\\n      }, 'fullscreen');\\n\\n      // Picture-in-Picture\\n      this.bind(elements.buttons.pip, 'click', () => {\\n        player.pip = 'toggle';\\n      }, 'pip');\\n\\n      // Airplay\\n      this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n      // Settings menu - click toggle\\n      this.bind(elements.buttons.settings, 'click', event => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n        controls.toggleMenu.call(player, event);\\n      }, null, false); // Can't be passive as we're preventing default\\n\\n      // Settings menu - keyboard toggle\\n      // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n      // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n      this.bind(elements.buttons.settings, 'keyup', event => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      }, null, false // Can't be passive as we're preventing default\\n      );\\n\\n      // Escape closes menu\\n      this.bind(elements.settings.menu, 'keydown', event => {\\n        if (event.key === 'Escape') {\\n          controls.toggleMenu.call(player, event);\\n        }\\n      });\\n\\n      // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n      this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n        const rect = elements.progress.getBoundingClientRect();\\n        const percent = 100 / rect.width * (event.pageX - rect.left);\\n        event.currentTarget.setAttribute('seek-value', percent);\\n      });\\n\\n      // Pause while seeking\\n      this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n        const seek = event.currentTarget;\\n        const attribute = 'play-on-seeked';\\n        if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Record seek time so we can prevent hiding controls for a few seconds after seek\\n        player.lastSeekTime = Date.now();\\n\\n        // Was playing before?\\n        const play = seek.hasAttribute(attribute);\\n        // Done seeking\\n        const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n        // If we're done seeking and it was playing, resume playback\\n        if (play && done) {\\n          seek.removeAttribute(attribute);\\n          silencePromise(player.play());\\n        } else if (!done && player.playing) {\\n          seek.setAttribute(attribute, '');\\n          player.pause();\\n        }\\n      });\\n\\n      // Fix range inputs on iOS\\n      // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n      // it takes over further interactions on the page. This is a hack\\n      if (browser.isIos) {\\n        const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n        Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n      }\\n\\n      // Seek\\n      this.bind(elements.inputs.seek, inputEvent, event => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n        seek.removeAttribute('seek-value');\\n        player.currentTime = seekTo / seek.max * player.duration;\\n      }, 'seek');\\n\\n      // Seek tooltip\\n      this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n      // Preview thumbnails plugin\\n      // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n      this.bind(elements.progress, 'mousemove touchmove', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startMove(event);\\n        }\\n      });\\n\\n      // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n      this.bind(elements.progress, 'mouseleave touchend click', () => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endMove(false, true);\\n        }\\n      });\\n\\n      // Show scrubbing preview\\n      this.bind(elements.progress, 'mousedown touchstart', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startScrubbing(event);\\n        }\\n      });\\n      this.bind(elements.progress, 'mouseup touchend', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endScrubbing(event);\\n        }\\n      });\\n\\n      // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n      if (browser.isWebKit) {\\n        Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n          this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n        });\\n      }\\n\\n      // Current time invert\\n      // Only if one time element is used for both currentTime and duration\\n      if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n        this.bind(elements.display.currentTime, 'click', () => {\\n          // Do nothing if we're at the start\\n          if (player.currentTime === 0) {\\n            return;\\n          }\\n          player.config.invertTime = !player.config.invertTime;\\n          controls.timeUpdate.call(player);\\n        });\\n      }\\n\\n      // Volume\\n      this.bind(elements.inputs.volume, inputEvent, event => {\\n        player.volume = event.target.value;\\n      }, 'volume');\\n\\n      // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n        elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n      });\\n\\n      // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n      if (elements.fullscreen) {\\n        Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n          this.bind(child, 'mouseenter mouseleave', event => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n      }\\n\\n      // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n      });\\n\\n      // Show controls when they receive focus (e.g., when using keyboard tab key)\\n      this.bind(elements.controls, 'focusin', () => {\\n        const {\\n          config,\\n          timers\\n        } = player;\\n\\n        // Skip transition to prevent focus from scrolling the parent element\\n        toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n        // Toggle\\n        ui.toggleControls.call(player, true);\\n\\n        // Restore transition\\n        setTimeout(() => {\\n          toggleClass(elements.controls, config.classNames.noTransition, false);\\n        }, 0);\\n\\n        // Delay a little more for mouse users\\n        const delay = this.touch ? 3000 : 4000;\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Hide again after delay\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Mouse wheel for volume\\n      this.bind(elements.inputs.volume, 'wheel', event => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const {\\n          volume\\n        } = player.media;\\n        if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n          event.preventDefault();\\n        }\\n      }, 'volume', false);\\n    });\\n    this.player = _player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const {\\n      player\\n    } = this;\\n    const {\\n      elements\\n    } = player;\\n    const {\\n      key,\\n      type,\\n      altKey,\\n      ctrlKey,\\n      metaKey,\\n      shiftKey\\n    } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = increment => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = player.duration / 10 * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const {\\n          editable\\n        } = player.config.selectors;\\n        const {\\n          seek\\n        } = elements.inputs;\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n}\\n\\nvar loadjs_umd = createCommonjsModule(function (module, exports) {\\n  (function (root, factory) {\\n    {\\n      module.exports = factory();\\n    }\\n  })(commonjsGlobal, function () {\\n    /**\\n     * Global dependencies.\\n     * @global {Object} document - DOM\\n     */\\n\\n    var devnull = function () {},\\n      bundleIdCache = {},\\n      bundleResultCache = {},\\n      bundleCallbackQueue = {};\\n\\n    /**\\n     * Subscribe to bundle load event.\\n     * @param {string[]} bundleIds - Bundle ids\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function subscribe(bundleIds, callbackFn) {\\n      // listify\\n      bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n      var depsNotFound = [],\\n        i = bundleIds.length,\\n        numWaiting = i,\\n        fn,\\n        bundleId,\\n        r,\\n        q;\\n\\n      // define callback function\\n      fn = function (bundleId, pathsNotFound) {\\n        if (pathsNotFound.length) depsNotFound.push(bundleId);\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(depsNotFound);\\n      };\\n\\n      // register callback\\n      while (i--) {\\n        bundleId = bundleIds[i];\\n\\n        // execute callback if in result cache\\n        r = bundleResultCache[bundleId];\\n        if (r) {\\n          fn(bundleId, r);\\n          continue;\\n        }\\n\\n        // add to callback queue\\n        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n        q.push(fn);\\n      }\\n    }\\n\\n    /**\\n     * Publish bundle load event.\\n     * @param {string} bundleId - Bundle id\\n     * @param {string[]} pathsNotFound - List of files not found\\n     */\\n    function publish(bundleId, pathsNotFound) {\\n      // exit if id isn't defined\\n      if (!bundleId) return;\\n      var q = bundleCallbackQueue[bundleId];\\n\\n      // cache result\\n      bundleResultCache[bundleId] = pathsNotFound;\\n\\n      // exit if queue is empty\\n      if (!q) return;\\n\\n      // empty callback queue\\n      while (q.length) {\\n        q[0](bundleId, pathsNotFound);\\n        q.splice(0, 1);\\n      }\\n    }\\n\\n    /**\\n     * Execute callbacks.\\n     * @param {Object or Function} args - The callback args\\n     * @param {string[]} depsNotFound - List of dependencies not found\\n     */\\n    function executeCallbacks(args, depsNotFound) {\\n      // accept function as argument\\n      if (args.call) args = {\\n        success: args\\n      };\\n\\n      // success and error callbacks\\n      if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n    }\\n\\n    /**\\n     * Load individual file.\\n     * @param {string} path - The file path\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFile(path, callbackFn, args, numTries) {\\n      var doc = document,\\n        async = args.async,\\n        maxTries = (args.numRetries || 0) + 1,\\n        beforeCallbackFn = args.before || devnull,\\n        pathname = path.replace(/[\\\\?|#].*$/, ''),\\n        pathStripped = path.replace(/^(css|img)!/, ''),\\n        isLegacyIECss,\\n        e;\\n      numTries = numTries || 0;\\n      if (/(^css!|\\\\.css$)/.test(pathname)) {\\n        // css\\n        e = doc.createElement('link');\\n        e.rel = 'stylesheet';\\n        e.href = pathStripped;\\n\\n        // tag IE9+\\n        isLegacyIECss = 'hideFocus' in e;\\n\\n        // use preload in IE Edge (to detect load errors)\\n        if (isLegacyIECss && e.relList) {\\n          isLegacyIECss = 0;\\n          e.rel = 'preload';\\n          e.as = 'style';\\n        }\\n      } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n        // image\\n        e = doc.createElement('img');\\n        e.src = pathStripped;\\n      } else {\\n        // javascript\\n        e = doc.createElement('script');\\n        e.src = path;\\n        e.async = async === undefined ? true : async;\\n      }\\n      e.onload = e.onerror = e.onbeforeload = function (ev) {\\n        var result = ev.type[0];\\n\\n        // treat empty stylesheets as failures to get around lack of onerror\\n        // support in IE9-11\\n        if (isLegacyIECss) {\\n          try {\\n            if (!e.sheet.cssText.length) result = 'e';\\n          } catch (x) {\\n            // sheets objects created from load errors don't allow access to\\n            // `cssText` (unless error is Code:18 SecurityError)\\n            if (x.code != 18) result = 'e';\\n          }\\n        }\\n\\n        // handle retries in case of load failure\\n        if (result == 'e') {\\n          // increment counter\\n          numTries += 1;\\n\\n          // exit function and try again\\n          if (numTries < maxTries) {\\n            return loadFile(path, callbackFn, args, numTries);\\n          }\\n        } else if (e.rel == 'preload' && e.as == 'style') {\\n          // activate preloaded stylesheets\\n          return e.rel = 'stylesheet'; // jshint ignore:line\\n        }\\n\\n        // execute callback\\n        callbackFn(path, result, ev.defaultPrevented);\\n      };\\n\\n      // add to document (unless callback returns `false`)\\n      if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n    }\\n\\n    /**\\n     * Load multiple files.\\n     * @param {string[]} paths - The file paths\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFiles(paths, callbackFn, args) {\\n      // listify paths\\n      paths = paths.push ? paths : [paths];\\n      var numWaiting = paths.length,\\n        x = numWaiting,\\n        pathsNotFound = [],\\n        fn,\\n        i;\\n\\n      // define callback function\\n      fn = function (path, result, defaultPrevented) {\\n        // handle error\\n        if (result == 'e') pathsNotFound.push(path);\\n\\n        // handle beforeload event. If defaultPrevented then that means the load\\n        // will be blocked (ex. Ghostery/ABP on Safari)\\n        if (result == 'b') {\\n          if (defaultPrevented) pathsNotFound.push(path);else return;\\n        }\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(pathsNotFound);\\n      };\\n\\n      // load scripts\\n      for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n    }\\n\\n    /**\\n     * Initiate script load and register bundle.\\n     * @param {(string|string[])} paths - The file paths\\n     * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n     *   callback or (3) object literal with success/error arguments, numRetries,\\n     *   etc.\\n     * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n     *   literal with success/error arguments, numRetries, etc.\\n     */\\n    function loadjs(paths, arg1, arg2) {\\n      var bundleId, args;\\n\\n      // bundleId (if string)\\n      if (arg1 && arg1.trim) bundleId = arg1;\\n\\n      // args (default is {})\\n      args = (bundleId ? arg2 : arg1) || {};\\n\\n      // throw error if bundle is already defined\\n      if (bundleId) {\\n        if (bundleId in bundleIdCache) {\\n          throw \\\"LoadJS\\\";\\n        } else {\\n          bundleIdCache[bundleId] = true;\\n        }\\n      }\\n      function loadFn(resolve, reject) {\\n        loadFiles(paths, function (pathsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, pathsNotFound);\\n\\n          // resolve Promise\\n          if (resolve) {\\n            executeCallbacks({\\n              success: resolve,\\n              error: reject\\n            }, pathsNotFound);\\n          }\\n\\n          // publish bundle load event\\n          publish(bundleId, pathsNotFound);\\n        }, args);\\n      }\\n      if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n    }\\n\\n    /**\\n     * Execute callbacks when dependencies have been satisfied.\\n     * @param {(string|string[])} deps - List of bundle ids\\n     * @param {Object} args - success/error arguments\\n     */\\n    loadjs.ready = function ready(deps, args) {\\n      // subscribe to bundle load event\\n      subscribe(deps, function (depsNotFound) {\\n        // execute callbacks\\n        executeCallbacks(args, depsNotFound);\\n      });\\n      return loadjs;\\n    };\\n\\n    /**\\n     * Manually satisfy bundle dependencies.\\n     * @param {string} bundleId - The bundle id\\n     */\\n    loadjs.done = function done(bundleId) {\\n      publish(bundleId, []);\\n    };\\n\\n    /**\\n     * Reset loadjs dependencies statuses\\n     */\\n    loadjs.reset = function reset() {\\n      bundleIdCache = {};\\n      bundleResultCache = {};\\n      bundleCallbackQueue = {};\\n    };\\n\\n    /**\\n     * Determine if bundle has already been defined\\n     * @param String} bundleId - The bundle id\\n     */\\n    loadjs.isDefined = function isDefined(bundleId) {\\n      return bundleId in bundleIdCache;\\n    };\\n\\n    // export\\n    return loadjs;\\n  });\\n});\\n\\n// ==========================================================================\\nfunction loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs_umd(url, {\\n      success: resolve,\\n      error: reject\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Parse Vimeo ID from URL\\nfunction parseId$1(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState$1(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk).then(() => {\\n        vimeo.ready.call(player);\\n      }).catch(error => {\\n        player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n      });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const {\\n      premium,\\n      referrerPolicy,\\n      ...frameParams\\n    } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? {\\n      h: hash\\n    } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams\\n    });\\n    const id = parseId$1(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted\\n    });\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState$1.call(player, true);\\n      return player.embed.play();\\n    };\\n    player.media.pause = () => {\\n      assurePlaybackState$1.call(player, false);\\n      return player.embed.pause();\\n    };\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let {\\n      currentTime\\n    } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const {\\n          embed,\\n          media,\\n          paused,\\n          volume\\n        } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n        // Seek\\n        .then(() => embed.setCurrentTime(time))\\n        // Restore paused\\n        .then(() => restorePause && embed.pause())\\n        // Restore volume\\n        .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n          // Do nothing\\n        });\\n      }\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed.setPlaybackRate(input).then(() => {\\n          speed = input;\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        }).catch(() => {\\n          // Cannot set Playback Rate, Video is probably not on Pro account\\n          player.options.speed = [1];\\n        });\\n      }\\n    });\\n\\n    // Volume\\n    let {\\n      volume\\n    } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Muted\\n    let {\\n      muted\\n    } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Loop\\n    let {\\n      loop\\n    } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      }\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed.getVideoUrl().then(value => {\\n      currentSrc = value;\\n      controls.setDownloadUrl.call(player);\\n    }).catch(error => {\\n      this.debug.warn(error);\\n    });\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      }\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      }\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then(state => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then(title => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then(value => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then(value => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then(tracks => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n    player.embed.on('cuechange', ({\\n      cues = []\\n    }) => {\\n      const strippedCues = cues.map(cue => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then(paused => {\\n        assurePlaybackState$1.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('play', () => {\\n      assurePlaybackState$1.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('pause', () => {\\n      assurePlaybackState$1.call(player, false);\\n    });\\n    player.embed.on('timeupdate', data => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n    player.embed.on('progress', data => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then(value => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n    player.embed.on('error', detail => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch(error => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n    fetch(url).then(data => {\\n      if (is.object(data)) {\\n        const {\\n          title,\\n          height,\\n          width\\n        } = data;\\n\\n        // Set title\\n        this.config.title = title;\\n        ui.setTitle.call(this);\\n\\n        // Set aspect ratio\\n        this.embed.ratio = roundAspectRatio(width, height);\\n      }\\n      setAspectRatio.call(this);\\n    }).catch(() => {\\n      // Set aspect ratio\\n      setAspectRatio.call(this);\\n    });\\n  },\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', {\\n      id,\\n      'data-poster': config.customControls ? player.poster : undefined\\n    });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n      .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n      .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n      .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n        // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n        if (!src.includes('maxres')) {\\n          player.elements.poster.style.backgroundSize = 'cover';\\n        }\\n      }).catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend({}, {\\n        // Autoplay\\n        autoplay: player.config.autoplay ? 1 : 0,\\n        // iframe interface language\\n        hl: player.config.hl,\\n        // Only show controls if not fully supported or opted out\\n        controls: player.supported.ui && config.customControls ? 0 : 1,\\n        // Disable keyboard as we handle it\\n        disablekb: 1,\\n        // Allow iOS inline playback\\n        playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n        // Captions are flaky on YouTube\\n        cc_load_policy: player.captions.active ? 1 : 0,\\n        cc_lang_pref: player.config.captions.language,\\n        // Tracking for stats\\n        widget_referrer: window ? window.location.href : null\\n      }, config),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message = {\\n              2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n              5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n              100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n              101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n            }[code] || 'An unknown error occurred';\\n            player.media.error = {\\n              code,\\n              message\\n            };\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            }\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            }\\n          });\\n\\n          // Volume\\n          let {\\n            volume\\n          } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Muted\\n          let {\\n            muted\\n          } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            }\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            }\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n              break;\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n              break;\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n              break;\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n              break;\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n              break;\\n          }\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data\\n          });\\n        }\\n      }\\n    });\\n  }\\n};\\n\\n// ==========================================================================\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster\\n      });\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  }\\n};\\n\\nconst destroy = instance => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n  instance.elements.container.remove();\\n};\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    /**\\n     * Load the IMA SDK\\n     */\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Check if the Google IMA3 SDK is loaded or load it ourselves\\n      if (!is.object(window.google) || !is.object(window.google.ima)) {\\n        loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n          this.ready();\\n        }).catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n      } else {\\n        this.ready();\\n      }\\n    });\\n    /**\\n     * Get the ads instance ready\\n     */\\n    _defineProperty$1(this, \\\"ready\\\", () => {\\n      // Double check we're enabled\\n      if (!this.enabled) {\\n        destroy(this);\\n      }\\n\\n      // Start ticking our safety timer. If the whole advertisement\\n      // thing doesn't resolve within our set time; we bail\\n      this.startSafetyTimer(12000, 'ready()');\\n\\n      // Clear the safety timer\\n      this.managerPromise.then(() => {\\n        this.clearSafetyTimer('onAdsManagerLoaded()');\\n      });\\n\\n      // Set listeners on the Plyr instance\\n      this.listeners();\\n\\n      // Setup the IMA SDK\\n      this.setupIMA();\\n    });\\n    /**\\n     * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n     * so here we define our ad container. This div is set up to render on top of the video player.\\n     * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n     * handle to the content video player - the SDK will poll the current time of our player to\\n     * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n     * mobile devices, this initialization is done as the result of a user action.\\n     */\\n    _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n      // Create the container for our advertisements\\n      this.elements.container = createElement('div', {\\n        class: this.player.config.classNames.ads\\n      });\\n      this.player.elements.container.appendChild(this.elements.container);\\n\\n      // So we can run VPAID2\\n      google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n      // Set language\\n      google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n      // Set playback for iOS10+\\n      google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n      // We assume the adContainer is the video container of the plyr element that will house the ads\\n      this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n      // Create ads loader\\n      this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n      // Listen and respond to ads loaded and error events\\n      this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n      this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n      // Request video ads to be pre-loaded\\n      this.requestAds();\\n    });\\n    /**\\n     * Request advertisements\\n     */\\n    _defineProperty$1(this, \\\"requestAds\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      try {\\n        // Request video ads\\n        const request = new google.ima.AdsRequest();\\n        request.adTagUrl = this.tagUrl;\\n\\n        // Specify the linear and nonlinear slot sizes. This helps the SDK\\n        // to select the correct creative if multiple are returned\\n        request.linearAdSlotWidth = container.offsetWidth;\\n        request.linearAdSlotHeight = container.offsetHeight;\\n        request.nonLinearAdSlotWidth = container.offsetWidth;\\n        request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n        // We only overlay ads as we only support video.\\n        request.forceNonLinearFullSlot = false;\\n\\n        // Mute based on current state\\n        request.setAdWillPlayMuted(!this.player.muted);\\n        this.loader.requestAds(request);\\n      } catch (error) {\\n        this.onAdError(error);\\n      }\\n    });\\n    /**\\n     * Update the ad countdown\\n     * @param {Boolean} start\\n     */\\n    _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n      if (!start) {\\n        clearInterval(this.countdownTimer);\\n        this.elements.container.removeAttribute('data-badge-text');\\n        return;\\n      }\\n      const update = () => {\\n        const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n        const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n        this.elements.container.setAttribute('data-badge-text', label);\\n      };\\n      this.countdownTimer = setInterval(update, 100);\\n    });\\n    /**\\n     * This method is called whenever the ads are ready inside the AdDisplayContainer\\n     * @param {Event} event - adsManagerLoadedEvent\\n     */\\n    _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n      // Load could occur after a source change (race condition)\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Get the ads manager\\n      const settings = new google.ima.AdsRenderingSettings();\\n\\n      // Tell the SDK to save and restore content video state on our behalf\\n      settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n      settings.enablePreloading = true;\\n\\n      // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n      // so it can determine when to start the mid- and post-roll\\n      this.manager = event.getAdsManager(this.player, settings);\\n\\n      // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n      this.cuePoints = this.manager.getCuePoints();\\n\\n      // Add listeners to the required events\\n      // Advertisement error events\\n      this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n      // Advertisement regular events\\n      Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n        this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n      });\\n\\n      // Resolve our adsManager\\n      this.trigger('loaded');\\n    });\\n    _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n      // Add advertisement cue's within the time line if available\\n      if (!is.empty(this.cuePoints)) {\\n        this.cuePoints.forEach(cuePoint => {\\n          if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n            const seekElement = this.player.elements.progress;\\n            if (is.element(seekElement)) {\\n              const cuePercentage = 100 / this.player.duration * cuePoint;\\n              const cue = createElement('span', {\\n                class: this.player.config.classNames.cues\\n              });\\n              cue.style.left = `${cuePercentage.toString()}%`;\\n              seekElement.appendChild(cue);\\n            }\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n     * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n     * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n      // don't have ad object associated\\n      const ad = event.getAd();\\n      const adData = event.getAdData();\\n\\n      // Proxy event\\n      const dispatchEvent = type => {\\n        triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n      };\\n\\n      // Bubble the event\\n      dispatchEvent(event.type);\\n      switch (event.type) {\\n        case google.ima.AdEvent.Type.LOADED:\\n          // This is the first event sent for an ad - it is possible to determine whether the\\n          // ad is a video ad or an overlay\\n          this.trigger('loaded');\\n\\n          // Start countdown\\n          this.pollCountdown(true);\\n          if (!ad.isLinear()) {\\n            // Position AdDisplayContainer correctly for overlay\\n            ad.width = container.offsetWidth;\\n            ad.height = container.offsetHeight;\\n          }\\n\\n          // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n          // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n          break;\\n        case google.ima.AdEvent.Type.STARTED:\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n          break;\\n        case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n          // All ads for the current videos are done. We can now request new advertisements\\n          // in case the video is re-played\\n\\n          // TODO: Example for what happens when a next video in a playlist would be loaded.\\n          // So here we load a new video when all ads are done.\\n          // Then we load new ads within a new adsManager. When the video\\n          // Is started - after - the ads are loaded, then we get ads.\\n          // You can also easily test cancelling and reloading by running\\n          // player.ads.cancel() and player.ads.play from the console I guess.\\n          // this.player.source = {\\n          //     type: 'video',\\n          //     title: 'View From A Blue Moon',\\n          //     sources: [{\\n          //         src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n          // 'video/mp4', }], poster:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n          // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n          // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n          // };\\n\\n          // TODO: So there is still this thing where a video should only be allowed to start\\n          // playing when the IMA SDK is ready or has failed\\n\\n          if (this.player.ended) {\\n            this.loadAds();\\n          } else {\\n            // The SDK won't allow new ads to be called without receiving a contentComplete()\\n            this.loader.contentComplete();\\n          }\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n          // This event indicates the ad has started - the video player can adjust the UI,\\n          // for example display a pause button and remaining time. Fired when content should\\n          // be paused. This usually happens right before an ad is about to cover the content\\n\\n          this.pauseContent();\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n          // This event indicates the ad has finished - the video player can perform\\n          // appropriate UI actions, such as removing the timer for remaining time detection.\\n          // Fired when content should be resumed. This usually happens when an ad finishes\\n          // or collapses\\n\\n          this.pollCountdown();\\n          this.resumeContent();\\n          break;\\n        case google.ima.AdEvent.Type.LOG:\\n          if (adData.adError) {\\n            this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n          }\\n          break;\\n      }\\n    });\\n    /**\\n     * Any ad error handling comes through here\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdError\\\", event => {\\n      this.cancel();\\n      this.player.debug.warn('Ads error', event);\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events. This ensures\\n     * the mid- and post-roll launch at the correct time. And\\n     * resize the advertisement when the player resizes\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      let time;\\n      this.player.on('canplay', () => {\\n        this.addCuePoints();\\n      });\\n      this.player.on('ended', () => {\\n        this.loader.contentComplete();\\n      });\\n      this.player.on('timeupdate', () => {\\n        time = this.player.currentTime;\\n      });\\n      this.player.on('seeked', () => {\\n        const seekedTime = this.player.currentTime;\\n        if (is.empty(this.cuePoints)) {\\n          return;\\n        }\\n        this.cuePoints.forEach((cuePoint, index) => {\\n          if (time < cuePoint && cuePoint < seekedTime) {\\n            this.manager.discardAdBreak();\\n            this.cuePoints.splice(index, 1);\\n          }\\n        });\\n      });\\n\\n      // Listen to the resizing of the window. And resize ad accordingly\\n      // TODO: eventually implement ResizeObserver\\n      window.addEventListener('resize', () => {\\n        if (this.manager) {\\n          this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n        }\\n      });\\n    });\\n    /**\\n     * Initialize the adsManager and start playing advertisements\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      if (!this.managerPromise) {\\n        this.resumeContent();\\n      }\\n\\n      // Play the requested advertisement whenever the adsManager is ready\\n      this.managerPromise.then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Resume our video\\n     */\\n    _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n      // Hide the advertisement container\\n      this.elements.container.style.zIndex = '';\\n\\n      // Ad is stopped\\n      this.playing = false;\\n\\n      // Play video\\n      silencePromise(this.player.media.play());\\n    });\\n    /**\\n     * Pause our video\\n     */\\n    _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n      // Show the advertisement container\\n      this.elements.container.style.zIndex = 3;\\n\\n      // Ad is playing\\n      this.playing = true;\\n\\n      // Pause our video.\\n      this.player.media.pause();\\n    });\\n    /**\\n     * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n     * allowed to call new ads based on google policies, as they interpret this as an accidental\\n     * video requests. https://developers.google.com/interactive-\\n     * media-ads/docs/sdks/android/faq#8\\n     */\\n    _defineProperty$1(this, \\\"cancel\\\", () => {\\n      // Pause our video\\n      if (this.initialized) {\\n        this.resumeContent();\\n      }\\n\\n      // Tell our instance that we're done for now\\n      this.trigger('error');\\n\\n      // Re-create our adsManager\\n      this.loadAds();\\n    });\\n    /**\\n     * Re-create our adsManager\\n     */\\n    _defineProperty$1(this, \\\"loadAds\\\", () => {\\n      // Tell our adsManager to go bye bye\\n      this.managerPromise.then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise(resolve => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Handles callbacks after an ad event was invoked\\n     * @param {String} event - Event type\\n     * @param args\\n     */\\n    _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n      const handlers = this.events[event];\\n      if (is.array(handlers)) {\\n        handlers.forEach(handler => {\\n          if (is.function(handler)) {\\n            handler.apply(this, args);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     * @return {Ads}\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      if (!is.array(this.events[event])) {\\n        this.events[event] = [];\\n      }\\n      this.events[event].push(callback);\\n      return this;\\n    });\\n    /**\\n     * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n     * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n     * advertisement is playing, or when a user action is required to start, then we clear the\\n     * timer on ad ready\\n     * @param {Number} time\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n      this.player.debug.log(`Safety timer invoked from: ${from}`);\\n      this.safetyTimer = setTimeout(() => {\\n        this.cancel();\\n        this.clearSafetyTimer('startSafetyTimer()');\\n      }, time);\\n    });\\n    /**\\n     * Clear our safety timer(s)\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n      if (!is.nullOrUndefined(this.safetyTimer)) {\\n        this.player.debug.log(`Safety timer cleared from: ${from}`);\\n        clearTimeout(this.safetyTimer);\\n        this.safetyTimer = null;\\n      }\\n    });\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n    this.load();\\n  }\\n  get enabled() {\\n    const {\\n      config\\n    } = this;\\n    return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n  }\\n  // Build the tag URL\\n  get tagUrl() {\\n    const {\\n      config\\n    } = this;\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId\\n    };\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n}\\n\\n/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nfunction clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = vttDataString => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n  frames.forEach(frame => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n    lines.forEach(line => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n          result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = 1 / ratio * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n  return result;\\n};\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      // Toggle the regular seek tooltip\\n      if (this.player.elements.display.seekTooltip) {\\n        this.player.elements.display.seekTooltip.hidden = this.enabled;\\n      }\\n      if (!this.enabled) return;\\n      this.getThumbnails().then(() => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Render DOM elements\\n        this.render();\\n\\n        // Check to see if thumb container size was specified manually in CSS\\n        this.determineContainerAutoSizing();\\n\\n        // Set up listeners\\n        this.listeners();\\n        this.loaded = true;\\n      });\\n    });\\n    // Download VTT files and parse them\\n    _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n      return new Promise(resolve => {\\n        const {\\n          src\\n        } = this.player.config.previewThumbnails;\\n        if (is.empty(src)) {\\n          throw new Error('Missing previewThumbnails.src config attribute');\\n        }\\n\\n        // Resolve promise\\n        const sortAndResolve = () => {\\n          // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n          this.thumbnails.sort((x, y) => x.height - y.height);\\n          this.player.debug.log('Preview thumbnails', this.thumbnails);\\n          resolve();\\n        };\\n\\n        // Via callback()\\n        if (is.function(src)) {\\n          src(thumbnails => {\\n            this.thumbnails = thumbnails;\\n            sortAndResolve();\\n          });\\n        }\\n        // VTT urls\\n        else {\\n          // If string, convert into single-element list\\n          const urls = is.string(src) ? [src] : src;\\n          // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n          const promises = urls.map(u => this.getThumbnail(u));\\n          // Resolve\\n          Promise.all(promises).then(sortAndResolve);\\n        }\\n      });\\n    });\\n    // Process individual VTT file\\n    _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n      return new Promise(resolve => {\\n        fetch(url).then(response => {\\n          const thumbnail = {\\n            frames: parseVtt(response),\\n            height: null,\\n            urlPrefix: ''\\n          };\\n\\n          // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n          // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n          // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n          if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n            thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n          }\\n\\n          // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n          const tempImage = new Image();\\n          tempImage.onload = () => {\\n            thumbnail.height = tempImage.naturalHeight;\\n            thumbnail.width = tempImage.naturalWidth;\\n            this.thumbnails.push(thumbnail);\\n            resolve();\\n          };\\n          tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n        });\\n      });\\n    });\\n    _defineProperty$1(this, \\\"startMove\\\", event => {\\n      if (!this.loaded) return;\\n      if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n      // Wait until media has a duration\\n      if (!this.player.media.duration) return;\\n      if (event.type === 'touchmove') {\\n        // Calculate seek hover position as approx video seconds\\n        this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n      } else {\\n        var _this$player$config$m, _this$player$config$m2;\\n        // Calculate seek hover position as approx video seconds\\n        const clientRect = this.player.elements.progress.getBoundingClientRect();\\n        const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n        this.seekTime = this.player.media.duration * (percentage / 100);\\n        if (this.seekTime < 0) {\\n          // The mousemove fires for 10+px out to the left\\n          this.seekTime = 0;\\n        }\\n        if (this.seekTime > this.player.media.duration - 1) {\\n          // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n          this.seekTime = this.player.media.duration - 1;\\n        }\\n        this.mousePosX = event.pageX;\\n\\n        // Set time text inside image container\\n        this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n        // Get marker point for time\\n        const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n          time: t\\n        }) => t === Math.round(this.seekTime));\\n\\n        // Append the point label to the tooltip\\n        if (point) {\\n          // this.elements.thumb.time.innerText.concat('\\\\n');\\n          this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n        }\\n      }\\n\\n      // Download and show image\\n      this.showImageAtCurrentTime();\\n    });\\n    _defineProperty$1(this, \\\"endMove\\\", () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n    _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n      // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n      if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n        this.mouseDown = true;\\n\\n        // Wait until media has a duration\\n        if (this.player.media.duration) {\\n          this.toggleScrubbingContainer(true);\\n          this.toggleThumbContainer(false, true);\\n\\n          // Download and show image\\n          this.showImageAtCurrentTime();\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n      this.mouseDown = false;\\n\\n      // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n      if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n        // The video was already seeked/loaded at the chosen time - hide immediately\\n        this.toggleScrubbingContainer(false);\\n      } else {\\n        // The video hasn't seeked yet. Wait for that\\n        once.call(this.player, this.player.media, 'timeupdate', () => {\\n          // Re-check mousedown - we might have already started scrubbing again\\n          if (!this.mouseDown) {\\n            this.toggleScrubbingContainer(false);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n      this.player.on('play', () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      this.player.on('seeked', () => {\\n        this.toggleThumbContainer(false);\\n      });\\n      this.player.on('timeupdate', () => {\\n        this.lastTime = this.player.media.currentTime;\\n      });\\n    });\\n    /**\\n     * Create HTML elements for image containers\\n     */\\n    _defineProperty$1(this, \\\"render\\\", () => {\\n      // Create HTML element: plyr__preview-thumbnail-container\\n      this.elements.thumb.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.thumbContainer\\n      });\\n\\n      // Wrapper for the image for styling\\n      this.elements.thumb.imageContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.imageContainer\\n      });\\n      this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n      // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n      const timeContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.timeContainer\\n      });\\n      this.elements.thumb.time = createElement('span', {}, '00:00');\\n      timeContainer.appendChild(this.elements.thumb.time);\\n      this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n      // Inject the whole thumb\\n      if (is.element(this.player.elements.progress)) {\\n        this.player.elements.progress.appendChild(this.elements.thumb.container);\\n      }\\n\\n      // Create HTML element: plyr__preview-scrubbing-container\\n      this.elements.scrubbing.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n      });\\n      this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n    });\\n    _defineProperty$1(this, \\\"destroy\\\", () => {\\n      if (this.elements.thumb.container) {\\n        this.elements.thumb.container.remove();\\n      }\\n      if (this.elements.scrubbing.container) {\\n        this.elements.scrubbing.container.remove();\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n      if (this.mouseDown) {\\n        this.setScrubbingContainerSize();\\n      } else {\\n        this.setThumbContainerSizeAndPos();\\n      }\\n\\n      // Find the desired thumbnail index\\n      // TODO: Handle a video longer than the thumbs where thumbNum is null\\n      const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n      const hasThumb = thumbNum >= 0;\\n      let qualityIndex = 0;\\n\\n      // Show the thumb container if we're not scrubbing\\n      if (!this.mouseDown) {\\n        this.toggleThumbContainer(hasThumb);\\n      }\\n\\n      // No matching thumb found\\n      if (!hasThumb) {\\n        return;\\n      }\\n\\n      // Check to see if we've already downloaded higher quality versions of this image\\n      this.thumbnails.forEach((thumbnail, index) => {\\n        if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n          qualityIndex = index;\\n        }\\n      });\\n\\n      // Only proceed if either thumb num or thumbfilename has changed\\n      if (thumbNum !== this.showingThumb) {\\n        this.showingThumb = thumbNum;\\n        this.loadImage(qualityIndex);\\n      }\\n    });\\n    // Show the image that's currently specified in this.showingThumb\\n    _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n      const thumbNum = this.showingThumb;\\n      const thumbnail = this.thumbnails[qualityIndex];\\n      const {\\n        urlPrefix\\n      } = thumbnail;\\n      const frame = thumbnail.frames[thumbNum];\\n      const thumbFilename = thumbnail.frames[thumbNum].text;\\n      const thumbUrl = urlPrefix + thumbFilename;\\n      if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n        // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n        // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n        if (this.loadingImage && this.usingSprites) {\\n          this.loadingImage.onload = null;\\n        }\\n\\n        // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n        // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n        // images causes a flicker. Putting a new image over the top does not\\n        const previewImage = new Image();\\n        previewImage.src = thumbUrl;\\n        previewImage.dataset.index = thumbNum;\\n        previewImage.dataset.filename = thumbFilename;\\n        this.showingThumbFilename = thumbFilename;\\n        this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n        // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n        previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n        this.loadingImage = previewImage;\\n        this.removeOldImages(previewImage);\\n      } else {\\n        // Update the existing image\\n        this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n        this.currentImageElement.dataset.index = thumbNum;\\n        this.removeOldImages(this.currentImageElement);\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n      this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n      this.setImageSizeAndOffset(previewImage, frame);\\n      if (newImage) {\\n        this.currentImageContainer.appendChild(previewImage);\\n        this.currentImageElement = previewImage;\\n        if (!this.loadedImages.includes(thumbFilename)) {\\n          this.loadedImages.push(thumbFilename);\\n        }\\n      }\\n\\n      // Preload images before and after the current one\\n      // Show higher quality of the same frame\\n      // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n      this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n    });\\n    // Remove all preview images that aren't the designated current image\\n    _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n      // Get a list of all images, convert it from a DOM list to an array\\n      Array.from(this.currentImageContainer.children).forEach(image => {\\n        if (image.tagName.toLowerCase() !== 'img') {\\n          return;\\n        }\\n        const removeDelay = this.usingSprites ? 500 : 1000;\\n        if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n          // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n          // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n          // eslint-disable-next-line no-param-reassign\\n          image.dataset.deleting = true;\\n\\n          // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n          const {\\n            currentImageContainer\\n          } = this;\\n          setTimeout(() => {\\n            currentImageContainer.removeChild(image);\\n            this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n          }, removeDelay);\\n        }\\n      });\\n    });\\n    // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n    // This will only preload the lowest quality\\n    _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n      return new Promise(resolve => {\\n        setTimeout(() => {\\n          const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n          if (this.showingThumbFilename === oldThumbFilename) {\\n            // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n            let thumbnailsClone;\\n            if (forward) {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n            } else {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n            }\\n            let foundOne = false;\\n            thumbnailsClone.forEach(frame => {\\n              const newThumbFilename = frame.text;\\n              if (newThumbFilename !== oldThumbFilename) {\\n                // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                if (!this.loadedImages.includes(newThumbFilename)) {\\n                  foundOne = true;\\n                  this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                  const {\\n                    urlPrefix\\n                  } = this.thumbnails[0];\\n                  const thumbURL = urlPrefix + newThumbFilename;\\n                  const previewImage = new Image();\\n                  previewImage.src = thumbURL;\\n                  previewImage.onload = () => {\\n                    this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                    if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                    // We don't resolve until the thumb is loaded\\n                    resolve();\\n                  };\\n                }\\n              }\\n            });\\n\\n            // If there are none to preload then we want to resolve immediately\\n            if (!foundOne) {\\n              resolve();\\n            }\\n          }\\n        }, 300);\\n      });\\n    });\\n    // If user has been hovering current image for half a second, look for a higher quality one\\n    _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n      if (currentQualityIndex < this.thumbnails.length - 1) {\\n        // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n        let previewImageHeight = previewImage.naturalHeight;\\n        if (this.usingSprites) {\\n          previewImageHeight = frame.h;\\n        }\\n        if (previewImageHeight < this.thumbContainerHeight) {\\n          // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n          setTimeout(() => {\\n            // Make sure the mouse hasn't already moved on and started hovering at another image\\n            if (this.showingThumbFilename === thumbFilename) {\\n              this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n              this.loadImage(currentQualityIndex + 1);\\n            }\\n          }, 300);\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n      this.elements.thumb.container.classList.toggle(className, toggle);\\n      if (!toggle && clearShowing) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n      this.elements.scrubbing.container.classList.toggle(className, toggle);\\n      if (!toggle) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n      if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n        // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n        this.sizeSpecifiedInCSS = true;\\n      }\\n    });\\n    // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n    _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n      const {\\n        imageContainer\\n      } = this.elements.thumb;\\n      if (!this.sizeSpecifiedInCSS) {\\n        const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n        imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n        const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n        const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n        imageContainer.style.height = `${thumbHeight}px`;\\n      }\\n      this.setThumbContainerPos();\\n    });\\n    _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n      const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n      const containerRect = this.player.elements.container.getBoundingClientRect();\\n      const {\\n        container\\n      } = this.elements.thumb;\\n      // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n      const min = containerRect.left - scrubberRect.left + 10;\\n      const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n      // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n      const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n      const clamped = clamp(position, min, max);\\n\\n      // Move the popover position\\n      container.style.left = `${clamped}px`;\\n\\n      // The arrow can follow the cursor\\n      container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n    });\\n    // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n    _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n      const {\\n        width,\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      this.elements.scrubbing.container.style.width = `${width}px`;\\n      this.elements.scrubbing.container.style.height = `${height}px`;\\n    });\\n    // Sprites need to be offset to the correct location\\n    _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n      if (!this.usingSprites) return;\\n\\n      // Find difference between height and preview container height\\n      const multiplier = this.thumbContainerHeight / frame.h;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.left = `-${frame.x * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.top = `-${frame.y * multiplier}px`;\\n    });\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {}\\n    };\\n    this.load();\\n  }\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const {\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach(attribute => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(this, () => {\\n      // Reset quality options\\n      this.options.quality = [];\\n\\n      // Remove elements\\n      removeElement(this.media);\\n      this.media = null;\\n\\n      // Reset class name\\n      if (is.element(this.elements.container)) {\\n        this.elements.container.removeAttribute('class');\\n      }\\n\\n      // Set the type and provider\\n      const {\\n        sources,\\n        type\\n      } = input;\\n      const [{\\n        provider = providers.html5,\\n        src\\n      }] = sources;\\n      const tagName = provider === 'html5' ? type : 'div';\\n      const attributes = provider === 'html5' ? {} : {\\n        src\\n      };\\n      Object.assign(this, {\\n        provider,\\n        type,\\n        // Check for support\\n        supported: support.check(type, provider, this.config.playsinline),\\n        // Create new element\\n        media: createElement(tagName, attributes)\\n      });\\n\\n      // Inject the new element\\n      this.elements.container.appendChild(this.media);\\n\\n      // Autoplay the new source?\\n      if (is.boolean(input.autoplay)) {\\n        this.config.autoplay = input.autoplay;\\n      }\\n\\n      // Set attributes for audio and video\\n      if (this.isHTML5) {\\n        if (this.config.crossorigin) {\\n          this.media.setAttribute('crossorigin', '');\\n        }\\n        if (this.config.autoplay) {\\n          this.media.setAttribute('autoplay', '');\\n        }\\n        if (!is.empty(input.poster)) {\\n          this.poster = input.poster;\\n        }\\n        if (this.config.loop.active) {\\n          this.media.setAttribute('loop', '');\\n        }\\n        if (this.config.muted) {\\n          this.media.setAttribute('muted', '');\\n        }\\n        if (this.config.playsinline) {\\n          this.media.setAttribute('playsinline', '');\\n        }\\n      }\\n\\n      // Restore class hook\\n      ui.addStyleHook.call(this);\\n\\n      // Set new sources for html5\\n      if (this.isHTML5) {\\n        source.insertElements.call(this, 'source', sources);\\n      }\\n\\n      // Set video title\\n      this.config.title = input.title;\\n\\n      // Set up from scratch\\n      media.setup.call(this);\\n\\n      // HTML5 stuff\\n      if (this.isHTML5) {\\n        // Setup captions\\n        if (Object.keys(input).includes('tracks')) {\\n          source.insertElements.call(this, 'track', input.tracks);\\n        }\\n      }\\n\\n      // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        // Setup interface\\n        ui.build.call(this);\\n      }\\n\\n      // Load HTML5 sources\\n      if (this.isHTML5) {\\n        this.media.load();\\n      }\\n\\n      // Update previewThumbnails config & reload plugin\\n      if (!is.empty(input.previewThumbnails)) {\\n        Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n        // Cleanup previewThumbnails plugin if it was loaded\\n        if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n          this.previewThumbnails.destroy();\\n          this.previewThumbnails = null;\\n        }\\n\\n        // Create new instance if it is still enabled\\n        if (this.config.previewThumbnails.enabled) {\\n          this.previewThumbnails = new PreviewThumbnails(this);\\n        }\\n      }\\n\\n      // Update the fullscreen support\\n      this.fullscreen.update();\\n    }, true);\\n  }\\n};\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    /**\\n     * Play the media, or play the advertisement (if they are not blocked)\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      if (!is.function(this.media.play)) {\\n        return null;\\n      }\\n\\n      // Intecept play with ads\\n      if (this.ads && this.ads.enabled) {\\n        this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n      }\\n\\n      // Return the promise (for HTML5)\\n      return this.media.play();\\n    });\\n    /**\\n     * Pause the media\\n     */\\n    _defineProperty$1(this, \\\"pause\\\", () => {\\n      if (!this.playing || !is.function(this.media.pause)) {\\n        return null;\\n      }\\n      return this.media.pause();\\n    });\\n    /**\\n     * Toggle playback based on current status\\n     * @param {Boolean} input\\n     */\\n    _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n      // Toggle based on current state if nothing passed\\n      const toggle = is.boolean(input) ? input : !this.playing;\\n      if (toggle) {\\n        return this.play();\\n      }\\n      return this.pause();\\n    });\\n    /**\\n     * Stop playback\\n     */\\n    _defineProperty$1(this, \\\"stop\\\", () => {\\n      if (this.isHTML5) {\\n        this.pause();\\n        this.restart();\\n      } else if (is.function(this.media.stop)) {\\n        this.media.stop();\\n      }\\n    });\\n    /**\\n     * Restart playback\\n     */\\n    _defineProperty$1(this, \\\"restart\\\", () => {\\n      this.currentTime = 0;\\n    });\\n    /**\\n     * Rewind\\n     * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n      this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Fast forward\\n     * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n      this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Increase volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n      const volume = this.media.muted ? 0 : this.volume;\\n      this.volume = volume + (is.number(step) ? step : 0);\\n    });\\n    /**\\n     * Decrease volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n      this.increaseVolume(-step);\\n    });\\n    /**\\n     * Trigger the airplay dialog\\n     * TODO: update player with state, support, enabled\\n     */\\n    _defineProperty$1(this, \\\"airplay\\\", () => {\\n      // Show dialog if supported\\n      if (support.airplay) {\\n        this.media.webkitShowPlaybackTargetPicker();\\n      }\\n    });\\n    /**\\n     * Toggle the player controls\\n     * @param {Boolean} [toggle] - Whether to show the controls\\n     */\\n    _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n      // Don't toggle if missing UI support or if it's audio\\n      if (this.supported.ui && !this.isAudio) {\\n        // Get state before change\\n        const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n        // Negate the argument if not undefined since adding the class to hides the controls\\n        const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n        // Apply and get updated state\\n        const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n        // Close menu\\n        if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n          controls.toggleMenu.call(this, false);\\n        }\\n\\n        // Trigger event on change\\n        if (hiding !== isHidden) {\\n          const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n          triggerEvent.call(this, this.media, eventName);\\n        }\\n        return !hiding;\\n      }\\n      return false;\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      on.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Add event listeners once\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n      once.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Remove event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n      off(this.elements.container, event, callback);\\n    });\\n    /**\\n     * Destroy an instance\\n     * Event listeners are removed when elements are removed\\n     * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n     * @param {Function} callback - Callback for when destroy is complete\\n     * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n     */\\n    _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n      if (!this.ready) {\\n        return;\\n      }\\n      const done = () => {\\n        // Reset overflow (incase destroyed while in fullscreen)\\n        document.body.style.overflow = '';\\n\\n        // GC for embed\\n        this.embed = null;\\n\\n        // If it's a soft destroy, make minimal changes\\n        if (soft) {\\n          if (Object.keys(this.elements).length) {\\n            // Remove elements\\n            removeElement(this.elements.buttons.play);\\n            removeElement(this.elements.captions);\\n            removeElement(this.elements.controls);\\n            removeElement(this.elements.wrapper);\\n\\n            // Clear for GC\\n            this.elements.buttons.play = null;\\n            this.elements.captions = null;\\n            this.elements.controls = null;\\n            this.elements.wrapper = null;\\n          }\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n        } else {\\n          // Unbind listeners\\n          unbindListeners.call(this);\\n\\n          // Cancel current network requests\\n          html5.cancelRequests.call(this);\\n\\n          // Replace the container with the original element provided\\n          replaceElement(this.elements.original, this.elements.container);\\n\\n          // Event\\n          triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback.call(this.elements.original);\\n          }\\n\\n          // Reset state\\n          this.ready = false;\\n\\n          // Clear for garbage collection\\n          setTimeout(() => {\\n            this.elements = null;\\n            this.media = null;\\n          }, 200);\\n        }\\n      };\\n\\n      // Stop playback\\n      this.stop();\\n\\n      // Clear timeouts\\n      clearTimeout(this.timers.loading);\\n      clearTimeout(this.timers.controls);\\n      clearTimeout(this.timers.resized);\\n\\n      // Provider specific stuff\\n      if (this.isHTML5) {\\n        // Restore native video controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Clean up\\n        done();\\n      } else if (this.isYouTube) {\\n        // Clear timers\\n        clearInterval(this.timers.buffering);\\n        clearInterval(this.timers.playing);\\n\\n        // Destroy YouTube API\\n        if (this.embed !== null && is.function(this.embed.destroy)) {\\n          this.embed.destroy();\\n        }\\n\\n        // Clean up\\n        done();\\n      } else if (this.isVimeo) {\\n        // Destroy Vimeo API\\n        // then clean up (wait, to prevent postmessage errors)\\n        if (this.embed !== null) {\\n          this.embed.unload().then(done);\\n        }\\n\\n        // Vimeo does not always return\\n        setTimeout(done, 200);\\n      }\\n    });\\n    /**\\n     * Check for support for a mime type (HTML5 only)\\n     * @param {String} type - Mime type\\n     */\\n    _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n      try {\\n        return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n      } catch (_) {\\n        return {};\\n      }\\n    })());\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {}\\n      }\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap()\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: []\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const _type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (_type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n        break;\\n      case 'video':\\n      case 'audio':\\n        this.type = _type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n        break;\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const {\\n      buffered\\n    } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({\\n        volume\\n      } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const {\\n      minimumSpeed: min,\\n      maximumSpeed: max\\n    } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n    if (!options.length) {\\n      return;\\n    }\\n    let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n    let updateStorage = true;\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({\\n        quality\\n      });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n         switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n             case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n             case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n             case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n             default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const {\\n      download\\n    } = this.config.urls;\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n    this.config.urls.download = input;\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n    this.config.ratio = reduceAspectRatio(input);\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const {\\n      toggled,\\n      currentTrack\\n    } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n    return targets.map(t => new Plyr(t, options));\\n  }\\n}\\nPlyr.defaults = cloneDeep(defaults);\\n\\n// ==========================================================================\\n\\nexport { Plyr as default };\\n//# sourceMappingURL=plyr.polyfilled.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"(function(global) {\\r\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\r\\n\\r\\n  var checkIfIteratorIsSupported = function() {\\r\\n    try {\\r\\n      return !!Symbol.iterator;\\r\\n    } catch (error) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var iteratorSupported = checkIfIteratorIsSupported();\\r\\n\\r\\n  var createIterator = function(items) {\\r\\n    var iterator = {\\r\\n      next: function() {\\r\\n        var value = items.shift();\\r\\n        return { done: value === void 0, value: value };\\r\\n      }\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      iterator[Symbol.iterator] = function() {\\r\\n        return iterator;\\r\\n      };\\r\\n    }\\r\\n\\r\\n    return iterator;\\r\\n  };\\r\\n\\r\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\r\\n  var serializeParam = function(value) {\\r\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\r\\n  };\\r\\n\\r\\n  var deserializeParam = function(value) {\\r\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\r\\n  };\\r\\n\\r\\n  var polyfillURLSearchParams = function() {\\r\\n\\r\\n    var URLSearchParams = function(searchString) {\\r\\n      Object.defineProperty(this, '_entries', { writable: true, value: {} });\\r\\n      var typeofSearchString = typeof searchString;\\r\\n\\r\\n      if (typeofSearchString === 'undefined') {\\r\\n        // do nothing\\r\\n      } else if (typeofSearchString === 'string') {\\r\\n        if (searchString !== '') {\\r\\n          this._fromString(searchString);\\r\\n        }\\r\\n      } else if (searchString instanceof URLSearchParams) {\\r\\n        var _this = this;\\r\\n        searchString.forEach(function(value, name) {\\r\\n          _this.append(name, value);\\r\\n        });\\r\\n      } else if ((searchString !== null) && (typeofSearchString === 'object')) {\\r\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\r\\n          for (var i = 0; i < searchString.length; i++) {\\r\\n            var entry = searchString[i];\\r\\n            if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) {\\r\\n              this.append(entry[0], entry[1]);\\r\\n            } else {\\r\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\r\\n            }\\r\\n          }\\r\\n        } else {\\r\\n          for (var key in searchString) {\\r\\n            if (searchString.hasOwnProperty(key)) {\\r\\n              this.append(key, searchString[key]);\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      } else {\\r\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\r\\n      }\\r\\n    };\\r\\n\\r\\n    var proto = URLSearchParams.prototype;\\r\\n\\r\\n    proto.append = function(name, value) {\\r\\n      if (name in this._entries) {\\r\\n        this._entries[name].push(String(value));\\r\\n      } else {\\r\\n        this._entries[name] = [String(value)];\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.delete = function(name) {\\r\\n      delete this._entries[name];\\r\\n    };\\r\\n\\r\\n    proto.get = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name][0] : null;\\r\\n    };\\r\\n\\r\\n    proto.getAll = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name].slice(0) : [];\\r\\n    };\\r\\n\\r\\n    proto.has = function(name) {\\r\\n      return (name in this._entries);\\r\\n    };\\r\\n\\r\\n    proto.set = function(name, value) {\\r\\n      this._entries[name] = [String(value)];\\r\\n    };\\r\\n\\r\\n    proto.forEach = function(callback, thisArg) {\\r\\n      var entries;\\r\\n      for (var name in this._entries) {\\r\\n        if (this._entries.hasOwnProperty(name)) {\\r\\n          entries = this._entries[name];\\r\\n          for (var i = 0; i < entries.length; i++) {\\r\\n            callback.call(thisArg, entries[i], name, this);\\r\\n          }\\r\\n        }\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.keys = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push(name);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.values = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value) {\\r\\n        items.push(value);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.entries = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      proto[Symbol.iterator] = proto.entries;\\r\\n    }\\r\\n\\r\\n    proto.toString = function() {\\r\\n      var searchArray = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\r\\n      });\\r\\n      return searchArray.join('&');\\r\\n    };\\r\\n\\r\\n\\r\\n    global.URLSearchParams = URLSearchParams;\\r\\n  };\\r\\n\\r\\n  var checkIfURLSearchParamsSupported = function() {\\r\\n    try {\\r\\n      var URLSearchParams = global.URLSearchParams;\\r\\n\\r\\n      return (\\r\\n        (new URLSearchParams('?a=1').toString() === 'a=1') &&\\r\\n        (typeof URLSearchParams.prototype.set === 'function') &&\\r\\n        (typeof URLSearchParams.prototype.entries === 'function')\\r\\n      );\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLSearchParamsSupported()) {\\r\\n    polyfillURLSearchParams();\\r\\n  }\\r\\n\\r\\n  var proto = global.URLSearchParams.prototype;\\r\\n\\r\\n  if (typeof proto.sort !== 'function') {\\r\\n    proto.sort = function() {\\r\\n      var _this = this;\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n        if (!_this._entries) {\\r\\n          _this.delete(name);\\r\\n        }\\r\\n      });\\r\\n      items.sort(function(a, b) {\\r\\n        if (a[0] < b[0]) {\\r\\n          return -1;\\r\\n        } else if (a[0] > b[0]) {\\r\\n          return +1;\\r\\n        } else {\\r\\n          return 0;\\r\\n        }\\r\\n      });\\r\\n      if (_this._entries) { // force reset because IE keeps keys index\\r\\n        _this._entries = {};\\r\\n      }\\r\\n      for (var i = 0; i < items.length; i++) {\\r\\n        this.append(items[i][0], items[i][1]);\\r\\n      }\\r\\n    };\\r\\n  }\\r\\n\\r\\n  if (typeof proto._fromString !== 'function') {\\r\\n    Object.defineProperty(proto, '_fromString', {\\r\\n      enumerable: false,\\r\\n      configurable: false,\\r\\n      writable: false,\\r\\n      value: function(searchString) {\\r\\n        if (this._entries) {\\r\\n          this._entries = {};\\r\\n        } else {\\r\\n          var keys = [];\\r\\n          this.forEach(function(value, name) {\\r\\n            keys.push(name);\\r\\n          });\\r\\n          for (var i = 0; i < keys.length; i++) {\\r\\n            this.delete(keys[i]);\\r\\n          }\\r\\n        }\\r\\n\\r\\n        searchString = searchString.replace(/^\\\\?/, '');\\r\\n        var attributes = searchString.split('&');\\r\\n        var attribute;\\r\\n        for (var i = 0; i < attributes.length; i++) {\\r\\n          attribute = attributes[i].split('=');\\r\\n          this.append(\\r\\n            deserializeParam(attribute[0]),\\r\\n            (attribute.length > 1) ? deserializeParam(attribute[1]) : ''\\r\\n          );\\r\\n        }\\r\\n      }\\r\\n    });\\r\\n  }\\r\\n\\r\\n  // HTMLAnchorElement\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\\r\\n(function(global) {\\r\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\r\\n\\r\\n  var checkIfURLIsSupported = function() {\\r\\n    try {\\r\\n      var u = new global.URL('b', 'http://a');\\r\\n      u.pathname = 'c d';\\r\\n      return (u.href === 'http://a/c%20d') && u.searchParams;\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var polyfillURL = function() {\\r\\n    var _URL = global.URL;\\r\\n\\r\\n    var URL = function(url, base) {\\r\\n      if (typeof url !== 'string') url = String(url);\\r\\n      if (base && typeof base !== 'string') base = String(base);\\r\\n\\r\\n      // Only create another document if the base is different from current location.\\r\\n      var doc = document, baseElement;\\r\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\r\\n        base = base.toLowerCase();\\r\\n        doc = document.implementation.createHTMLDocument('');\\r\\n        baseElement = doc.createElement('base');\\r\\n        baseElement.href = base;\\r\\n        doc.head.appendChild(baseElement);\\r\\n        try {\\r\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\r\\n        } catch (err) {\\r\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\r\\n        }\\r\\n      }\\r\\n\\r\\n      var anchorElement = doc.createElement('a');\\r\\n      anchorElement.href = url;\\r\\n      if (baseElement) {\\r\\n        doc.body.appendChild(anchorElement);\\r\\n        anchorElement.href = anchorElement.href; // force href to refresh\\r\\n      }\\r\\n\\r\\n      var inputElement = doc.createElement('input');\\r\\n      inputElement.type = 'url';\\r\\n      inputElement.value = url;\\r\\n\\r\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || (!inputElement.checkValidity() && !base)) {\\r\\n        throw new TypeError('Invalid URL');\\r\\n      }\\r\\n\\r\\n      Object.defineProperty(this, '_anchorElement', {\\r\\n        value: anchorElement\\r\\n      });\\r\\n\\r\\n\\r\\n      // create a linked searchParams which reflect its changes on URL\\r\\n      var searchParams = new global.URLSearchParams(this.search);\\r\\n      var enableSearchUpdate = true;\\r\\n      var enableSearchParamsUpdate = true;\\r\\n      var _this = this;\\r\\n      ['append', 'delete', 'set'].forEach(function(methodName) {\\r\\n        var method = searchParams[methodName];\\r\\n        searchParams[methodName] = function() {\\r\\n          method.apply(searchParams, arguments);\\r\\n          if (enableSearchUpdate) {\\r\\n            enableSearchParamsUpdate = false;\\r\\n            _this.search = searchParams.toString();\\r\\n            enableSearchParamsUpdate = true;\\r\\n          }\\r\\n        };\\r\\n      });\\r\\n\\r\\n      Object.defineProperty(this, 'searchParams', {\\r\\n        value: searchParams,\\r\\n        enumerable: true\\r\\n      });\\r\\n\\r\\n      var search = void 0;\\r\\n      Object.defineProperty(this, '_updateSearchParams', {\\r\\n        enumerable: false,\\r\\n        configurable: false,\\r\\n        writable: false,\\r\\n        value: function() {\\r\\n          if (this.search !== search) {\\r\\n            search = this.search;\\r\\n            if (enableSearchParamsUpdate) {\\r\\n              enableSearchUpdate = false;\\r\\n              this.searchParams._fromString(this.search);\\r\\n              enableSearchUpdate = true;\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      });\\r\\n    };\\r\\n\\r\\n    var proto = URL.prototype;\\r\\n\\r\\n    var linkURLWithAnchorAttribute = function(attributeName) {\\r\\n      Object.defineProperty(proto, attributeName, {\\r\\n        get: function() {\\r\\n          return this._anchorElement[attributeName];\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement[attributeName] = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      });\\r\\n    };\\r\\n\\r\\n    ['hash', 'host', 'hostname', 'port', 'protocol']\\r\\n      .forEach(function(attributeName) {\\r\\n        linkURLWithAnchorAttribute(attributeName);\\r\\n      });\\r\\n\\r\\n    Object.defineProperty(proto, 'search', {\\r\\n      get: function() {\\r\\n        return this._anchorElement['search'];\\r\\n      },\\r\\n      set: function(value) {\\r\\n        this._anchorElement['search'] = value;\\r\\n        this._updateSearchParams();\\r\\n      },\\r\\n      enumerable: true\\r\\n    });\\r\\n\\r\\n    Object.defineProperties(proto, {\\r\\n\\r\\n      'toString': {\\r\\n        get: function() {\\r\\n          var _this = this;\\r\\n          return function() {\\r\\n            return _this.href;\\r\\n          };\\r\\n        }\\r\\n      },\\r\\n\\r\\n      'href': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.href = value;\\r\\n          this._updateSearchParams();\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'pathname': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.pathname = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'origin': {\\r\\n        get: function() {\\r\\n          // get expected port from protocol\\r\\n          var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol];\\r\\n          // add port to origin if, expected port is different than actual port\\r\\n          // and it is not empty f.e http://foo:8080\\r\\n          // 8080 != 80 && 8080 != ''\\r\\n          var addPortToOrigin = this._anchorElement.port != expectedPort &&\\r\\n            this._anchorElement.port !== '';\\r\\n\\r\\n          return this._anchorElement.protocol +\\r\\n            '//' +\\r\\n            this._anchorElement.hostname +\\r\\n            (addPortToOrigin ? (':' + this._anchorElement.port) : '');\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'password': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'username': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n    });\\r\\n\\r\\n    URL.createObjectURL = function(blob) {\\r\\n      return _URL.createObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    URL.revokeObjectURL = function(url) {\\r\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    global.URL = URL;\\r\\n\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLIsSupported()) {\\r\\n    polyfillURL();\\r\\n  }\\r\\n\\r\\n  if ((global.location !== void 0) && !('origin' in global.location)) {\\r\\n    var getOrigin = function() {\\r\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');\\r\\n    };\\r\\n\\r\\n    try {\\r\\n      Object.defineProperty(global.location, 'origin', {\\r\\n        get: getOrigin,\\r\\n        enumerable: true\\r\\n      });\\r\\n    } catch (e) {\\r\\n      setInterval(function() {\\r\\n        global.location.origin = getOrigin();\\r\\n      }, 100);\\r\\n    }\\r\\n  }\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: {\\n    download: null,\\n    vimeo: {\\n      sdk: 'https://player.vimeo.com/api/player.js',\\n      iframe: 'https://player.vimeo.com/video/{0}?{1}',\\n      api: 'https://vimeo.com/api/oembed.json?url={0}',\\n    },\\n    youtube: {\\n      sdk: 'https://www.youtube.com/iframe_api',\\n      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',\\n    },\\n    googleIMA: {\\n      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',\\n    },\\n  },\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\n+{\"version\":3,\"sources\":[\"node_modules/.pnpm/custom-event-polyfill@1.0.7/node_modules/custom-event-polyfill/polyfill.js\",\"plyr.polyfilled.mjs\",\"node_modules/.pnpm/rangetouch@2.0.1/node_modules/rangetouch/dist/rangetouch.mjs\",\"node_modules/.pnpm/url-polyfill@1.1.12/node_modules/url-polyfill/url-polyfill.js\",\"src/js/utils/is.js\",\"src/js/utils/animation.js\",\"src/js/utils/browser.js\",\"src/js/utils/objects.js\",\"src/js/utils/elements.js\",\"src/js/support.js\",\"src/js/utils/events.js\",\"src/js/utils/promise.js\",\"src/js/utils/arrays.js\",\"src/js/utils/style.js\",\"src/js/html5.js\",\"src/js/utils/strings.js\",\"src/js/utils/i18n.js\",\"src/js/storage.js\",\"src/js/utils/fetch.js\",\"src/js/utils/load-sprite.js\",\"src/js/utils/time.js\",\"src/js/controls.js\",\"src/js/utils/urls.js\",\"src/js/captions.js\",\"src/js/config/defaults.js\",\"src/js/config/states.js\",\"src/js/config/types.js\",\"src/js/console.js\",\"src/js/fullscreen.js\",\"src/js/utils/load-image.js\",\"src/js/ui.js\",\"src/js/listeners.js\",\"node_modules/.pnpm/loadjs@4.2.0/node_modules/loadjs/dist/loadjs.umd.js\",\"src/js/utils/load-script.js\",\"src/js/plugins/vimeo.js\",\"src/js/plugins/youtube.js\",\"src/js/media.js\",\"src/js/plugins/ads.js\",\"src/js/utils/numbers.js\",\"src/js/plugins/preview-thumbnails.js\",\"src/js/source.js\",\"src/js/plyr.js\"],\"names\":[\"window\",\"ce\",\"CustomEvent\",\"cancelable\",\"preventDefault\",\"defaultPrevented\",\"Error\",\"e\",\"event\",\"params\",\"evt\",\"origPrevent\",\"bubbles\",\"document\",\"createEvent\",\"initCustomEvent\",\"detail\",\"call\",\"this\",\"Object\",\"defineProperty\",\"get\",\"prototype\",\"Event\",\"commonjsGlobal\",\"globalThis\",\"global\",\"self\",\"createCommonjsModule\",\"fn\",\"module\",\"exports\",\"_defineProperty$1\",\"obj\",\"key\",\"value\",\"_toPropertyKey\",\"enumerable\",\"configurable\",\"writable\",\"_toPrimitive\",\"input\",\"hint\",\"prim\",\"Symbol\",\"toPrimitive\",\"undefined\",\"res\",\"TypeError\",\"String\",\"Number\",\"arg\",\"_classCallCheck\",\"t\",\"_defineProperties\",\"n\",\"length\",\"r\",\"_createClass\",\"_defineProperty\",\"ownKeys\",\"keys\",\"getOwnPropertySymbols\",\"filter\",\"getOwnPropertyDescriptor\",\"push\",\"apply\",\"_objectSpread2\",\"arguments\",\"forEach\",\"getOwnPropertyDescriptors\",\"defineProperties\",\"iteratorSupported\",\"iterator\",\"error\",\"checkIfIteratorIsSupported\",\"createIterator\",\"items\",\"next\",\"shift\",\"done\",\"serializeParam\",\"encodeURIComponent\",\"replace\",\"deserializeParam\",\"decodeURIComponent\",\"URLSearchParams\",\"toString\",\"set\",\"entries\",\"checkIfURLSearchParamsSupported\",\"searchString\",\"typeofSearchString\",\"_fromString\",\"_this\",\"name\",\"append\",\"i\",\"entry\",\"hasOwnProperty\",\"proto\",\"_entries\",\"delete\",\"getAll\",\"slice\",\"has\",\"callback\",\"thisArg\",\"values\",\"searchArray\",\"join\",\"polyfillURLSearchParams\",\"sort\",\"a\",\"b\",\"attribute\",\"attributes\",\"split\",\"u\",\"URL\",\"pathname\",\"href\",\"searchParams\",\"checkIfURLIsSupported\",\"_URL\",\"url\",\"base\",\"baseElement\",\"doc\",\"location\",\"toLowerCase\",\"implementation\",\"createHTMLDocument\",\"createElement\",\"head\",\"appendChild\",\"indexOf\",\"err\",\"anchorElement\",\"body\",\"inputElement\",\"type\",\"protocol\",\"test\",\"checkValidity\",\"search\",\"enableSearchUpdate\",\"enableSearchParamsUpdate\",\"methodName\",\"method\",\"attributeName\",\"_anchorElement\",\"linkURLWithAnchorAttribute\",\"_updateSearchParams\",\"origin\",\"expectedPort\",\"addPortToOrigin\",\"port\",\"hostname\",\"password\",\"username\",\"createObjectURL\",\"blob\",\"revokeObjectURL\",\"polyfillURL\",\"getOrigin\",\"setInterval\",\"defaults\",\"addCSS\",\"thumbWidth\",\"watch\",\"matches\",\"Array\",\"from\",\"querySelectorAll\",\"includes\",\"trigger\",\"dispatchEvent\",\"getConstructor\",\"constructor\",\"instanceOf\",\"isNullOrUndefined\",\"isObject\",\"isNumber\",\"isNaN\",\"isString\",\"isBoolean\",\"Boolean\",\"isFunction\",\"Function\",\"isArray\",\"isNodeList\",\"NodeList\",\"isElement\",\"Element\",\"isEvent\",\"isEmpty\",\"is\",\"nullOrUndefined\",\"object\",\"number\",\"string\",\"boolean\",\"function\",\"array\",\"nodeList\",\"element\",\"empty\",\"getDecimalPlaces\",\"concat\",\"match\",\"Math\",\"max\",\"round\",\"parseFloat\",\"toFixed\",\"RangeTouch\",\"querySelector\",\"rangeTouch\",\"config\",\"init\",\"enabled\",\"style\",\"userSelect\",\"webKitUserSelect\",\"touchAction\",\"listeners\",\"target\",\"changedTouches\",\"o\",\"getAttribute\",\"s\",\"c\",\"getBoundingClientRect\",\"width\",\"clientX\",\"left\",\"disabled\",\"MutationObserver\",\"addedNodes\",\"observe\",\"childList\",\"subtree\",\"map\",\"documentElement\",\"isWeakMap\",\"WeakMap\",\"isTextNode\",\"Text\",\"isKeyboardEvent\",\"KeyboardEvent\",\"isCue\",\"TextTrackCue\",\"VTTCue\",\"isTrack\",\"TextTrack\",\"kind\",\"isPromise\",\"Promise\",\"then\",\"nodeType\",\"ownerDocument\",\"isUrl\",\"startsWith\",\"_\",\"weakMap\",\"textNode\",\"keyboardEvent\",\"cue\",\"track\",\"promise\",\"transitionEndEvent\",\"events\",\"WebkitTransition\",\"MozTransition\",\"OTransition\",\"transition\",\"find\",\"repaint\",\"delay\",\"setTimeout\",\"hidden\",\"offsetHeight\",\"isIE\",\"documentMode\",\"isEdge\",\"navigator\",\"userAgent\",\"isWebKit\",\"isIPhone\",\"maxTouchPoints\",\"isIPadOS\",\"platform\",\"isIos\",\"browser\",\"cloneDeep\",\"JSON\",\"parse\",\"stringify\",\"getDeep\",\"path\",\"reduce\",\"extend\",\"sources\",\"source\",\"assign\",\"wrap\",\"elements\",\"wrapper\",\"targets\",\"reverse\",\"index\",\"child\",\"cloneNode\",\"parent\",\"parentNode\",\"sibling\",\"nextSibling\",\"insertBefore\",\"setAttributes\",\"setAttribute\",\"text\",\"innerText\",\"insertAfter\",\"insertElement\",\"removeElement\",\"removeChild\",\"emptyElement\",\"childNodes\",\"lastChild\",\"replaceElement\",\"newChild\",\"oldChild\",\"replaceChild\",\"getAttributesFromSelector\",\"sel\",\"existingAttributes\",\"existing\",\"selector\",\"trim\",\"className\",\"parts\",\"charAt\",\"class\",\"id\",\"toggleHidden\",\"hide\",\"toggleClass\",\"force\",\"classList\",\"contains\",\"hasClass\",\"webkitMatchesSelector\",\"mozMatchesSelector\",\"msMatchesSelector\",\"closest\",\"el\",\"parentElement\",\"getElements\",\"container\",\"getElement\",\"setFocus\",\"focusVisible\",\"focus\",\"preventScroll\",\"defaultCodecs\",\"support\",\"audio\",\"video\",\"check\",\"provider\",\"api\",\"ui\",\"rangeInput\",\"pip\",\"webkitSetPresentationMode\",\"pictureInPictureEnabled\",\"disablePictureInPicture\",\"airplay\",\"WebKitPlaybackTargetAvailabilityEvent\",\"playsinline\",\"mime\",\"mediaType\",\"isHTML5\",\"media\",\"canPlayType\",\"textTracks\",\"range\",\"touch\",\"transitions\",\"reducedMotion\",\"matchMedia\",\"supportsPassiveListeners\",\"supported\",\"options\",\"addEventListener\",\"removeEventListener\",\"toggleListener\",\"toggle\",\"passive\",\"capture\",\"eventListeners\",\"on\",\"off\",\"once\",\"onceCallback\",\"args\",\"triggerEvent\",\"plyr\",\"unbindListeners\",\"item\",\"ready\",\"resolve\",\"silencePromise\",\"dedupe\",\"prev\",\"curr\",\"abs\",\"supportsCSS\",\"declaration\",\"CSS\",\"supports\",\"standardRatios\",\"out\",\"x\",\"y\",\"validateAspectRatio\",\"every\",\"reduceAspectRatio\",\"ratio\",\"height\",\"getDivider\",\"w\",\"h\",\"divider\",\"getAspectRatio\",\"embed\",\"videoWidth\",\"videoHeight\",\"setAspectRatio\",\"isVideo\",\"padding\",\"aspectRatio\",\"paddingBottom\",\"isVimeo\",\"vimeo\",\"premium\",\"offsetWidth\",\"parseInt\",\"getComputedStyle\",\"offset\",\"fullscreen\",\"active\",\"transform\",\"add\",\"classNames\",\"videoFixedRatio\",\"roundAspectRatio\",\"tolerance\",\"closestRatio\",\"getViewportSize\",\"clientWidth\",\"innerWidth\",\"clientHeight\",\"innerHeight\",\"html5\",\"getSources\",\"getQualityOptions\",\"quality\",\"forced\",\"setup\",\"player\",\"speed\",\"onChange\",\"currentTime\",\"paused\",\"preload\",\"readyState\",\"playbackRate\",\"src\",\"play\",\"load\",\"cancelRequests\",\"blankVideo\",\"debug\",\"log\",\"generateId\",\"prefix\",\"floor\",\"random\",\"format\",\"getPercentage\",\"current\",\"replaceAll\",\"RegExp\",\"toTitleCase\",\"toUpperCase\",\"toPascalCase\",\"toCamelCase\",\"stripHTML\",\"fragment\",\"createDocumentFragment\",\"innerHTML\",\"firstChild\",\"getHTML\",\"resources\",\"youtube\",\"i18n\",\"seekTime\",\"title\",\"k\",\"v\",\"Storage\",\"store\",\"localStorage\",\"getItem\",\"json\",\"storage\",\"setItem\",\"removeItem\",\"fetch\",\"responseType\",\"reject\",\"request\",\"XMLHttpRequest\",\"responseText\",\"response\",\"status\",\"open\",\"send\",\"loadSprite\",\"hasId\",\"isCached\",\"exists\",\"getElementById\",\"update\",\"data\",\"insertAdjacentElement\",\"useStorage\",\"cached\",\"content\",\"result\",\"catch\",\"getHours\",\"trunc\",\"getMinutes\",\"getSeconds\",\"formatTime\",\"time\",\"displayHours\",\"inverted\",\"hours\",\"mins\",\"secs\",\"controls\",\"getIconUrl\",\"iconUrl\",\"host\",\"top\",\"cors\",\"svg4everybody\",\"findElements\",\"selectors\",\"buttons\",\"pause\",\"restart\",\"rewind\",\"fastForward\",\"mute\",\"settings\",\"captions\",\"progress\",\"inputs\",\"seek\",\"volume\",\"display\",\"buffer\",\"duration\",\"seekTooltip\",\"tooltip\",\"warn\",\"toggleNativeControls\",\"createIcon\",\"namespace\",\"iconPath\",\"iconPrefix\",\"icon\",\"createElementNS\",\"focusable\",\"use\",\"setAttributeNS\",\"createLabel\",\"attr\",\"createBadge\",\"badge\",\"menu\",\"createButton\",\"buttonType\",\"props\",\"label\",\"labelPressed\",\"iconPressed\",\"some\",\"control\",\"button\",\"createRange\",\"min\",\"step\",\"autocomplete\",\"role\",\"updateRangeFill\",\"createProgress\",\"suffixKey\",\"played\",\"suffix\",\"createTime\",\"attrs\",\"bindMenuItemShortcuts\",\"menuItem\",\"stopPropagation\",\"isRadioButton\",\"showMenuPanel\",\"nextElementSibling\",\"firstElementChild\",\"previousElementSibling\",\"lastElementChild\",\"focusFirstMenuItem\",\"createMenuItem\",\"list\",\"checked\",\"flex\",\"children\",\"node\",\"bind\",\"currentTrack\",\"updateTimeDisplay\",\"updateVolume\",\"setRange\",\"muted\",\"pressed\",\"updateProgress\",\"setProgress\",\"val\",\"getElementsByTagName\",\"nodeValue\",\"buffered\",\"percent\",\"setProperty\",\"updateSeekTooltip\",\"_this$config$markers\",\"_this$config$markers$\",\"tooltips\",\"tipElement\",\"visible\",\"show\",\"clientRect\",\"pageX\",\"point\",\"markers\",\"points\",\"insertAdjacentHTML\",\"timeUpdate\",\"invert\",\"invertTime\",\"seeking\",\"durationUpdate\",\"hasDuration\",\"displayDuration\",\"setMarkers\",\"toggleMenuButton\",\"setting\",\"updateSetting\",\"pane\",\"panels\",\"default\",\"getLabel\",\"setQualityMenu\",\"checkMenu\",\"getBadge\",\"sorting\",\"setCaptionsMenu\",\"tracks\",\"getTracks\",\"toggled\",\"language\",\"unshift\",\"setSpeedMenu\",\"minimumSpeed\",\"maximumSpeed\",\"popup\",\"p\",\"firstItem\",\"toggleMenu\",\"composedPath\",\"isMenuItem\",\"getMenuSize\",\"tab\",\"clone\",\"position\",\"opacity\",\"removeAttribute\",\"scrollWidth\",\"scrollHeight\",\"size\",\"restore\",\"propertyName\",\"setDownloadUrl\",\"download\",\"create\",\"defaultAttributes\",\"progressContainer\",\"inner\",\"home\",\"backButton\",\"urls\",\"isEmbed\",\"inject\",\"seektime\",\"addProperty\",\"controlPressed\",\"labels\",\"setMediaMetadata\",\"mediaSession\",\"metadata\",\"MediaMetadata\",\"mediaMetadata\",\"artist\",\"album\",\"artwork\",\"_this$config$markers2\",\"_this$config$markers3\",\"containerFragment\",\"pointsFragment\",\"tipVisible\",\"toggleTip\",\"markerElement\",\"marker\",\"tip\",\"parseUrl\",\"safe\",\"parser\",\"buildUrlParams\",\"isYouTube\",\"languages\",\"userLanguage\",\"trackEvents\",\"meta\",\"currentTrackNode\",\"languageExists\",\"mode\",\"updateCues\",\"setLanguage\",\"activeClass\",\"findTrack\",\"enableTextTrack\",\"sortIsDefault\",\"sorted\",\"getCurrentTrack\",\"cues\",\"activeCues\",\"getCueAsHTML\",\"cueText\",\"caption\",\"autoplay\",\"autopause\",\"toggleInvert\",\"clickToPlay\",\"hideControls\",\"resetOnEnd\",\"disableContextMenu\",\"loop\",\"selected\",\"keyboard\",\"focused\",\"fallback\",\"iosNative\",\"seekLabel\",\"unmute\",\"enableCaptions\",\"disableCaptions\",\"enterFullscreen\",\"exitFullscreen\",\"frameTitle\",\"menuBack\",\"normal\",\"start\",\"end\",\"all\",\"reset\",\"advertisement\",\"qualityBadge\",\"sdk\",\"iframe\",\"googleIMA\",\"editable\",\"embedContainer\",\"poster\",\"posterEnabled\",\"ads\",\"playing\",\"stopped\",\"loading\",\"hover\",\"isTouch\",\"uiSupported\",\"noTransition\",\"previewThumbnails\",\"thumbContainer\",\"thumbContainerShown\",\"imageContainer\",\"timeContainer\",\"scrubbingContainer\",\"scrubbingContainerShown\",\"hash\",\"publisherId\",\"tagUrl\",\"byline\",\"portrait\",\"transparent\",\"customControls\",\"referrerPolicy\",\"rel\",\"showinfo\",\"iv_load_policy\",\"modestbranding\",\"noCookie\",\"inactive\",\"providers\",\"types\",\"getProviderByUrl\",\"noop\",\"Console\",\"console\",\"Fullscreen\",\"scrollPosition\",\"scrollX\",\"scrollY\",\"scrollTo\",\"overflow\",\"viewport\",\"property\",\"hasProperty\",\"cleanupViewport\",\"part\",\"activeElement\",\"first\",\"last\",\"shiftKey\",\"forceFallback\",\"nativeSupported\",\"requestFullscreen\",\"webkitEnterFullscreen\",\"toggleFallback\",\"navigationUI\",\"action\",\"cancelFullScreen\",\"exit\",\"enter\",\"proxy\",\"trapFocus\",\"fullscreenEnabled\",\"webkitFullscreenEnabled\",\"mozFullScreenEnabled\",\"msFullscreenEnabled\",\"useNative\",\"pre\",\"getRootNode\",\"fullscreenElement\",\"shadowRoot\",\"loadImage\",\"minWidth\",\"image\",\"Image\",\"handler\",\"onload\",\"onerror\",\"naturalWidth\",\"addStyleHook\",\"build\",\"checkPlaying\",\"setTitle\",\"setPoster\",\"togglePoster\",\"enable\",\"backgroundImage\",\"backgroundSize\",\"toggleControls\",\"checkLoading\",\"clearTimeout\",\"timers\",\"controlsElement\",\"recentTouchSeek\",\"lastSeekTime\",\"Date\",\"now\",\"migrateStyles\",\"getPropertyValue\",\"removeProperty\",\"Listeners\",\"handleKey\",\"firstTouch\",\"setGutter\",\"useNativeAspectRatio\",\"maxWidth\",\"margin\",\"viewportWidth\",\"viewportHeight\",\"resized\",\"isAudio\",\"ended\",\"togglePlay\",\"proxyEvents\",\"defaultHandler\",\"customHandlerKey\",\"customHandler\",\"returned\",\"hasCustomHandler\",\"inputEvent\",\"forward\",\"toggleCaptions\",\"rect\",\"currentTarget\",\"hasAttribute\",\"seekTo\",\"loaded\",\"startMove\",\"endMove\",\"startScrubbing\",\"endScrubbing\",\"webkitDirectionInvertedFromDevice\",\"deltaX\",\"deltaY\",\"direction\",\"sign\",\"increaseVolume\",\"lastKey\",\"focusTimer\",\"lastKeyDown\",\"altKey\",\"ctrlKey\",\"metaKey\",\"repeat\",\"increment\",\"decreaseVolume\",\"usingNative\",\"loadjs_umd\",\"devnull\",\"bundleIdCache\",\"bundleResultCache\",\"bundleCallbackQueue\",\"subscribe\",\"bundleIds\",\"callbackFn\",\"bundleId\",\"depsNotFound\",\"numWaiting\",\"pathsNotFound\",\"publish\",\"q\",\"splice\",\"executeCallbacks\",\"success\",\"loadFile\",\"numTries\",\"isLegacyIECss\",\"async\",\"maxTries\",\"numRetries\",\"beforeCallbackFn\",\"before\",\"pathStripped\",\"relList\",\"as\",\"onbeforeload\",\"ev\",\"sheet\",\"cssText\",\"code\",\"loadFiles\",\"paths\",\"loadjs\",\"arg1\",\"arg2\",\"loadFn\",\"returnPromise\",\"deps\",\"isDefined\",\"factory\",\"loadScript\",\"parseId\",\"$2\",\"parseHash\",\"found\",\"assurePlaybackState\",\"hasPlayed\",\"Vimeo\",\"frameParams\",\"hashParam\",\"sidedock\",\"gesture\",\"thumbnail_url\",\"Player\",\"disableTextTrack\",\"stop\",\"restorePause\",\"setVolume\",\"setCurrentTime\",\"setPlaybackRate\",\"setMuted\",\"currentSrc\",\"setLoop\",\"getVideoUrl\",\"getVideoWidth\",\"getVideoHeight\",\"dimensions\",\"setAutopause\",\"state\",\"getVideoTitle\",\"getCurrentTime\",\"getDuration\",\"getTextTracks\",\"strippedCues\",\"getPaused\",\"seconds\",\"getHost\",\"YT\",\"onYouTubeIframeAPIReady\",\"getTitle\",\"videoId\",\"currentId\",\"posterSrc\",\"playerVars\",\"hl\",\"disablekb\",\"cc_load_policy\",\"cc_lang_pref\",\"widget_referrer\",\"onError\",\"message\",\"onPlaybackRateChange\",\"instance\",\"getPlaybackRate\",\"onReady\",\"playVideo\",\"pauseVideo\",\"stopVideo\",\"speeds\",\"getAvailablePlaybackRates\",\"clearInterval\",\"buffering\",\"getVideoLoadedFraction\",\"lastBuffered\",\"onStateChange\",\"unMute\",\"destroy\",\"manager\",\"displayContainer\",\"remove\",\"Ads\",\"google\",\"ima\",\"startSafetyTimer\",\"managerPromise\",\"clearSafetyTimer\",\"setupIMA\",\"setVpaidMode\",\"ImaSdkSettings\",\"VpaidMode\",\"ENABLED\",\"setLocale\",\"setDisableCustomPlaybackForIOS10Plus\",\"AdDisplayContainer\",\"loader\",\"AdsLoader\",\"AdsManagerLoadedEvent\",\"Type\",\"ADS_MANAGER_LOADED\",\"onAdsManagerLoaded\",\"AdErrorEvent\",\"AD_ERROR\",\"onAdError\",\"requestAds\",\"AdsRequest\",\"adTagUrl\",\"linearAdSlotWidth\",\"linearAdSlotHeight\",\"nonLinearAdSlotWidth\",\"nonLinearAdSlotHeight\",\"forceNonLinearFullSlot\",\"setAdWillPlayMuted\",\"countdownTimer\",\"getRemainingTime\",\"AdsRenderingSettings\",\"restoreCustomPlaybackStateOnAdBreakComplete\",\"enablePreloading\",\"getAdsManager\",\"cuePoints\",\"getCuePoints\",\"AdEvent\",\"onAdEvent\",\"cuePoint\",\"seekElement\",\"cuePercentage\",\"ad\",\"getAd\",\"adData\",\"getAdData\",\"LOADED\",\"pollCountdown\",\"isLinear\",\"STARTED\",\"ALL_ADS_COMPLETED\",\"loadAds\",\"contentComplete\",\"CONTENT_PAUSE_REQUESTED\",\"pauseContent\",\"CONTENT_RESUME_REQUESTED\",\"resumeContent\",\"LOG\",\"adError\",\"getMessage\",\"cancel\",\"addCuePoints\",\"seekedTime\",\"discardAdBreak\",\"resize\",\"ViewMode\",\"NORMAL\",\"initialize\",\"initialized\",\"zIndex\",\"handlers\",\"safetyTimer\",\"AV_PUBLISHERID\",\"AV_CHANNELID\",\"AV_URL\",\"cb\",\"AV_WIDTH\",\"AV_HEIGHT\",\"AV_CDIM2\",\"clamp\",\"parseVtt\",\"vttDataString\",\"processedList\",\"frame\",\"line\",\"startTime\",\"lineSplit\",\"matchTimes\",\"endTime\",\"fitRatio\",\"outer\",\"PreviewThumbnails\",\"getThumbnails\",\"render\",\"determineContainerAutoSizing\",\"sortAndResolve\",\"thumbnails\",\"promises\",\"getThumbnail\",\"thumbnail\",\"frames\",\"urlPrefix\",\"substring\",\"lastIndexOf\",\"tempImage\",\"naturalHeight\",\"_this$player$config$m\",\"_this$player$config$m2\",\"percentage\",\"mousePosX\",\"thumb\",\"showImageAtCurrentTime\",\"toggleThumbContainer\",\"mouseDown\",\"toggleScrubbingContainer\",\"ceil\",\"lastTime\",\"scrubbing\",\"setScrubbingContainerSize\",\"setThumbContainerSizeAndPos\",\"thumbNum\",\"findIndex\",\"hasThumb\",\"qualityIndex\",\"loadedImages\",\"showingThumb\",\"thumbFilename\",\"thumbUrl\",\"currentImageElement\",\"dataset\",\"filename\",\"showImage\",\"removeOldImages\",\"loadingImage\",\"usingSprites\",\"previewImage\",\"showingThumbFilename\",\"newImage\",\"setImageSizeAndOffset\",\"currentImageContainer\",\"preloadNearby\",\"getHigherQuality\",\"currentImage\",\"tagName\",\"removeDelay\",\"deleting\",\"oldThumbFilename\",\"thumbnailsClone\",\"foundOne\",\"newThumbFilename\",\"thumbURL\",\"currentQualityIndex\",\"previewImageHeight\",\"thumbContainerHeight\",\"clearShowing\",\"sizeSpecifiedInCSS\",\"thumbAspectRatio\",\"thumbHeight\",\"setThumbContainerPos\",\"scrubberRect\",\"containerRect\",\"right\",\"clamped\",\"multiplier\",\"lastMouseMoveTime\",\"currentScrubbingImageElement\",\"currentThumbnailImageElement\",\"insertElements\",\"change\",\"crossorigin\",\"Plyr\",\"webkitShowPlaybackTargetPicker\",\"isHidden\",\"hiding\",\"eventName\",\"soft\",\"original\",\"unload\",\"failed\",\"jQuery\",\"truthy\",\"inputIsValid\",\"fauxDuration\",\"realDuration\",\"Infinity\",\"hasAudio\",\"mozHasAudio\",\"webkitAudioDecodedByteCount\",\"audioTracks\",\"updateStorage\",\"requestPictureInPicture\",\"exitPictureInPicture\",\"webkitPresentationMode\",\"pictureInPictureElement\",\"setPreviewThumbnails\",\"thumbnailSource\",\"static\"],\"mappings\":\"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\",\"file\":\"plyr.polyfilled.min.mjs\",\"sourcesContent\":[\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function() {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n\\n  try {\\n    var ce = new window.CustomEvent('test', { cancelable: true });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function(event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(\\n        event,\\n        params.bubbles,\\n        params.cancelable,\\n        params.detail\\n      );\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function() {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function() {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\",\"// Polyfill for creating CustomEvents on IE9/10/11\\n\\n// code pulled from:\\n// https://github.com/d4tocchini/customevent-polyfill\\n// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent#Polyfill\\n\\n(function () {\\n  if (typeof window === 'undefined') {\\n    return;\\n  }\\n  try {\\n    var ce = new window.CustomEvent('test', {\\n      cancelable: true\\n    });\\n    ce.preventDefault();\\n    if (ce.defaultPrevented !== true) {\\n      // IE has problems with .preventDefault() on custom events\\n      // http://stackoverflow.com/questions/23349191\\n      throw new Error('Could not prevent default');\\n    }\\n  } catch (e) {\\n    var CustomEvent = function (event, params) {\\n      var evt, origPrevent;\\n      params = params || {};\\n      params.bubbles = !!params.bubbles;\\n      params.cancelable = !!params.cancelable;\\n      evt = document.createEvent('CustomEvent');\\n      evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);\\n      origPrevent = evt.preventDefault;\\n      evt.preventDefault = function () {\\n        origPrevent.call(this);\\n        try {\\n          Object.defineProperty(this, 'defaultPrevented', {\\n            get: function () {\\n              return true;\\n            }\\n          });\\n        } catch (e) {\\n          this.defaultPrevented = true;\\n        }\\n      };\\n      return evt;\\n    };\\n    CustomEvent.prototype = window.Event.prototype;\\n    window.CustomEvent = CustomEvent; // expose definition to window\\n  }\\n})();\\n\\nvar commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};\\n\\nfunction createCommonjsModule(fn, module) {\\n\\treturn module = { exports: {} }, fn(module, module.exports), module.exports;\\n}\\n\\n(function (global) {\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\n\\n  var checkIfIteratorIsSupported = function () {\\n    try {\\n      return !!Symbol.iterator;\\n    } catch (error) {\\n      return false;\\n    }\\n  };\\n  var iteratorSupported = checkIfIteratorIsSupported();\\n  var createIterator = function (items) {\\n    var iterator = {\\n      next: function () {\\n        var value = items.shift();\\n        return {\\n          done: value === void 0,\\n          value: value\\n        };\\n      }\\n    };\\n    if (iteratorSupported) {\\n      iterator[Symbol.iterator] = function () {\\n        return iterator;\\n      };\\n    }\\n    return iterator;\\n  };\\n\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\n  var serializeParam = function (value) {\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\n  };\\n  var deserializeParam = function (value) {\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\n  };\\n  var polyfillURLSearchParams = function () {\\n    var URLSearchParams = function (searchString) {\\n      Object.defineProperty(this, '_entries', {\\n        writable: true,\\n        value: {}\\n      });\\n      var typeofSearchString = typeof searchString;\\n      if (typeofSearchString === 'undefined') ; else if (typeofSearchString === 'string') {\\n        if (searchString !== '') {\\n          this._fromString(searchString);\\n        }\\n      } else if (searchString instanceof URLSearchParams) {\\n        var _this = this;\\n        searchString.forEach(function (value, name) {\\n          _this.append(name, value);\\n        });\\n      } else if (searchString !== null && typeofSearchString === 'object') {\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\n          for (var i = 0; i < searchString.length; i++) {\\n            var entry = searchString[i];\\n            if (Object.prototype.toString.call(entry) === '[object Array]' || entry.length !== 2) {\\n              this.append(entry[0], entry[1]);\\n            } else {\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\n            }\\n          }\\n        } else {\\n          for (var key in searchString) {\\n            if (searchString.hasOwnProperty(key)) {\\n              this.append(key, searchString[key]);\\n            }\\n          }\\n        }\\n      } else {\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\n      }\\n    };\\n    var proto = URLSearchParams.prototype;\\n    proto.append = function (name, value) {\\n      if (name in this._entries) {\\n        this._entries[name].push(String(value));\\n      } else {\\n        this._entries[name] = [String(value)];\\n      }\\n    };\\n    proto.delete = function (name) {\\n      delete this._entries[name];\\n    };\\n    proto.get = function (name) {\\n      return name in this._entries ? this._entries[name][0] : null;\\n    };\\n    proto.getAll = function (name) {\\n      return name in this._entries ? this._entries[name].slice(0) : [];\\n    };\\n    proto.has = function (name) {\\n      return name in this._entries;\\n    };\\n    proto.set = function (name, value) {\\n      this._entries[name] = [String(value)];\\n    };\\n    proto.forEach = function (callback, thisArg) {\\n      var entries;\\n      for (var name in this._entries) {\\n        if (this._entries.hasOwnProperty(name)) {\\n          entries = this._entries[name];\\n          for (var i = 0; i < entries.length; i++) {\\n            callback.call(thisArg, entries[i], name, this);\\n          }\\n        }\\n      }\\n    };\\n    proto.keys = function () {\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push(name);\\n      });\\n      return createIterator(items);\\n    };\\n    proto.values = function () {\\n      var items = [];\\n      this.forEach(function (value) {\\n        items.push(value);\\n      });\\n      return createIterator(items);\\n    };\\n    proto.entries = function () {\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push([name, value]);\\n      });\\n      return createIterator(items);\\n    };\\n    if (iteratorSupported) {\\n      proto[Symbol.iterator] = proto.entries;\\n    }\\n    proto.toString = function () {\\n      var searchArray = [];\\n      this.forEach(function (value, name) {\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\n      });\\n      return searchArray.join('&');\\n    };\\n    global.URLSearchParams = URLSearchParams;\\n  };\\n  var checkIfURLSearchParamsSupported = function () {\\n    try {\\n      var URLSearchParams = global.URLSearchParams;\\n      return new URLSearchParams('?a=1').toString() === 'a=1' && typeof URLSearchParams.prototype.set === 'function' && typeof URLSearchParams.prototype.entries === 'function';\\n    } catch (e) {\\n      return false;\\n    }\\n  };\\n  if (!checkIfURLSearchParamsSupported()) {\\n    polyfillURLSearchParams();\\n  }\\n  var proto = global.URLSearchParams.prototype;\\n  if (typeof proto.sort !== 'function') {\\n    proto.sort = function () {\\n      var _this = this;\\n      var items = [];\\n      this.forEach(function (value, name) {\\n        items.push([name, value]);\\n        if (!_this._entries) {\\n          _this.delete(name);\\n        }\\n      });\\n      items.sort(function (a, b) {\\n        if (a[0] < b[0]) {\\n          return -1;\\n        } else if (a[0] > b[0]) {\\n          return +1;\\n        } else {\\n          return 0;\\n        }\\n      });\\n      if (_this._entries) {\\n        // force reset because IE keeps keys index\\n        _this._entries = {};\\n      }\\n      for (var i = 0; i < items.length; i++) {\\n        this.append(items[i][0], items[i][1]);\\n      }\\n    };\\n  }\\n  if (typeof proto._fromString !== 'function') {\\n    Object.defineProperty(proto, '_fromString', {\\n      enumerable: false,\\n      configurable: false,\\n      writable: false,\\n      value: function (searchString) {\\n        if (this._entries) {\\n          this._entries = {};\\n        } else {\\n          var keys = [];\\n          this.forEach(function (value, name) {\\n            keys.push(name);\\n          });\\n          for (var i = 0; i < keys.length; i++) {\\n            this.delete(keys[i]);\\n          }\\n        }\\n        searchString = searchString.replace(/^\\\\?/, '');\\n        var attributes = searchString.split('&');\\n        var attribute;\\n        for (var i = 0; i < attributes.length; i++) {\\n          attribute = attributes[i].split('=');\\n          this.append(deserializeParam(attribute[0]), attribute.length > 1 ? deserializeParam(attribute[1]) : '');\\n        }\\n      }\\n    });\\n  }\\n\\n  // HTMLAnchorElement\\n})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n(function (global) {\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\n\\n  var checkIfURLIsSupported = function () {\\n    try {\\n      var u = new global.URL('b', 'http://a');\\n      u.pathname = 'c d';\\n      return u.href === 'http://a/c%20d' && u.searchParams;\\n    } catch (e) {\\n      return false;\\n    }\\n  };\\n  var polyfillURL = function () {\\n    var _URL = global.URL;\\n    var URL = function (url, base) {\\n      if (typeof url !== 'string') url = String(url);\\n      if (base && typeof base !== 'string') base = String(base);\\n\\n      // Only create another document if the base is different from current location.\\n      var doc = document,\\n        baseElement;\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\n        base = base.toLowerCase();\\n        doc = document.implementation.createHTMLDocument('');\\n        baseElement = doc.createElement('base');\\n        baseElement.href = base;\\n        doc.head.appendChild(baseElement);\\n        try {\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\n        } catch (err) {\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\n        }\\n      }\\n      var anchorElement = doc.createElement('a');\\n      anchorElement.href = url;\\n      if (baseElement) {\\n        doc.body.appendChild(anchorElement);\\n        anchorElement.href = anchorElement.href; // force href to refresh\\n      }\\n\\n      var inputElement = doc.createElement('input');\\n      inputElement.type = 'url';\\n      inputElement.value = url;\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || !inputElement.checkValidity() && !base) {\\n        throw new TypeError('Invalid URL');\\n      }\\n      Object.defineProperty(this, '_anchorElement', {\\n        value: anchorElement\\n      });\\n\\n      // create a linked searchParams which reflect its changes on URL\\n      var searchParams = new global.URLSearchParams(this.search);\\n      var enableSearchUpdate = true;\\n      var enableSearchParamsUpdate = true;\\n      var _this = this;\\n      ['append', 'delete', 'set'].forEach(function (methodName) {\\n        var method = searchParams[methodName];\\n        searchParams[methodName] = function () {\\n          method.apply(searchParams, arguments);\\n          if (enableSearchUpdate) {\\n            enableSearchParamsUpdate = false;\\n            _this.search = searchParams.toString();\\n            enableSearchParamsUpdate = true;\\n          }\\n        };\\n      });\\n      Object.defineProperty(this, 'searchParams', {\\n        value: searchParams,\\n        enumerable: true\\n      });\\n      var search = void 0;\\n      Object.defineProperty(this, '_updateSearchParams', {\\n        enumerable: false,\\n        configurable: false,\\n        writable: false,\\n        value: function () {\\n          if (this.search !== search) {\\n            search = this.search;\\n            if (enableSearchParamsUpdate) {\\n              enableSearchUpdate = false;\\n              this.searchParams._fromString(this.search);\\n              enableSearchUpdate = true;\\n            }\\n          }\\n        }\\n      });\\n    };\\n    var proto = URL.prototype;\\n    var linkURLWithAnchorAttribute = function (attributeName) {\\n      Object.defineProperty(proto, attributeName, {\\n        get: function () {\\n          return this._anchorElement[attributeName];\\n        },\\n        set: function (value) {\\n          this._anchorElement[attributeName] = value;\\n        },\\n        enumerable: true\\n      });\\n    };\\n    ['hash', 'host', 'hostname', 'port', 'protocol'].forEach(function (attributeName) {\\n      linkURLWithAnchorAttribute(attributeName);\\n    });\\n    Object.defineProperty(proto, 'search', {\\n      get: function () {\\n        return this._anchorElement['search'];\\n      },\\n      set: function (value) {\\n        this._anchorElement['search'] = value;\\n        this._updateSearchParams();\\n      },\\n      enumerable: true\\n    });\\n    Object.defineProperties(proto, {\\n      'toString': {\\n        get: function () {\\n          var _this = this;\\n          return function () {\\n            return _this.href;\\n          };\\n        }\\n      },\\n      'href': {\\n        get: function () {\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\n        },\\n        set: function (value) {\\n          this._anchorElement.href = value;\\n          this._updateSearchParams();\\n        },\\n        enumerable: true\\n      },\\n      'pathname': {\\n        get: function () {\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\n        },\\n        set: function (value) {\\n          this._anchorElement.pathname = value;\\n        },\\n        enumerable: true\\n      },\\n      'origin': {\\n        get: function () {\\n          // get expected port from protocol\\n          var expectedPort = {\\n            'http:': 80,\\n            'https:': 443,\\n            'ftp:': 21\\n          }[this._anchorElement.protocol];\\n          // add port to origin if, expected port is different than actual port\\n          // and it is not empty f.e http://foo:8080\\n          // 8080 != 80 && 8080 != ''\\n          var addPortToOrigin = this._anchorElement.port != expectedPort && this._anchorElement.port !== '';\\n          return this._anchorElement.protocol + '//' + this._anchorElement.hostname + (addPortToOrigin ? ':' + this._anchorElement.port : '');\\n        },\\n        enumerable: true\\n      },\\n      'password': {\\n        // TODO\\n        get: function () {\\n          return '';\\n        },\\n        set: function (value) {},\\n        enumerable: true\\n      },\\n      'username': {\\n        // TODO\\n        get: function () {\\n          return '';\\n        },\\n        set: function (value) {},\\n        enumerable: true\\n      }\\n    });\\n    URL.createObjectURL = function (blob) {\\n      return _URL.createObjectURL.apply(_URL, arguments);\\n    };\\n    URL.revokeObjectURL = function (url) {\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\n    };\\n    global.URL = URL;\\n  };\\n  if (!checkIfURLIsSupported()) {\\n    polyfillURL();\\n  }\\n  if (global.location !== void 0 && !('origin' in global.location)) {\\n    var getOrigin = function () {\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? ':' + global.location.port : '');\\n    };\\n    try {\\n      Object.defineProperty(global.location, 'origin', {\\n        get: getOrigin,\\n        enumerable: true\\n      });\\n    } catch (e) {\\n      setInterval(function () {\\n        global.location.origin = getOrigin();\\n      }, 100);\\n    }\\n  }\\n})(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : commonjsGlobal);\\n\\nfunction _defineProperty$1(obj, key, value) {\\n  key = _toPropertyKey(key);\\n  if (key in obj) {\\n    Object.defineProperty(obj, key, {\\n      value: value,\\n      enumerable: true,\\n      configurable: true,\\n      writable: true\\n    });\\n  } else {\\n    obj[key] = value;\\n  }\\n  return obj;\\n}\\nfunction _toPrimitive(input, hint) {\\n  if (typeof input !== \\\"object\\\" || input === null) return input;\\n  var prim = input[Symbol.toPrimitive];\\n  if (prim !== undefined) {\\n    var res = prim.call(input, hint || \\\"default\\\");\\n    if (typeof res !== \\\"object\\\") return res;\\n    throw new TypeError(\\\"@@toPrimitive must return a primitive value.\\\");\\n  }\\n  return (hint === \\\"string\\\" ? String : Number)(input);\\n}\\nfunction _toPropertyKey(arg) {\\n  var key = _toPrimitive(arg, \\\"string\\\");\\n  return typeof key === \\\"symbol\\\" ? key : String(key);\\n}\\n\\nfunction _classCallCheck(e, t) {\\n  if (!(e instanceof t)) throw new TypeError(\\\"Cannot call a class as a function\\\");\\n}\\nfunction _defineProperties(e, t) {\\n  for (var n = 0; n < t.length; n++) {\\n    var r = t[n];\\n    r.enumerable = r.enumerable || !1, r.configurable = !0, \\\"value\\\" in r && (r.writable = !0), Object.defineProperty(e, r.key, r);\\n  }\\n}\\nfunction _createClass(e, t, n) {\\n  return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e;\\n}\\nfunction _defineProperty(e, t, n) {\\n  return t in e ? Object.defineProperty(e, t, {\\n    value: n,\\n    enumerable: !0,\\n    configurable: !0,\\n    writable: !0\\n  }) : e[t] = n, e;\\n}\\nfunction ownKeys(e, t) {\\n  var n = Object.keys(e);\\n  if (Object.getOwnPropertySymbols) {\\n    var r = Object.getOwnPropertySymbols(e);\\n    t && (r = r.filter(function (t) {\\n      return Object.getOwnPropertyDescriptor(e, t).enumerable;\\n    })), n.push.apply(n, r);\\n  }\\n  return n;\\n}\\nfunction _objectSpread2(e) {\\n  for (var t = 1; t < arguments.length; t++) {\\n    var n = null != arguments[t] ? arguments[t] : {};\\n    t % 2 ? ownKeys(Object(n), !0).forEach(function (t) {\\n      _defineProperty(e, t, n[t]);\\n    }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) {\\n      Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t));\\n    });\\n  }\\n  return e;\\n}\\nvar defaults$1 = {\\n  addCSS: !0,\\n  thumbWidth: 15,\\n  watch: !0\\n};\\nfunction matches$1(e, t) {\\n  return function () {\\n    return Array.from(document.querySelectorAll(t)).includes(this);\\n  }.call(e, t);\\n}\\nfunction trigger(e, t) {\\n  if (e && t) {\\n    var n = new Event(t, {\\n      bubbles: !0\\n    });\\n    e.dispatchEvent(n);\\n  }\\n}\\nvar getConstructor$1 = function (e) {\\n    return null != e ? e.constructor : null;\\n  },\\n  instanceOf$1 = function (e, t) {\\n    return !!(e && t && e instanceof t);\\n  },\\n  isNullOrUndefined$1 = function (e) {\\n    return null == e;\\n  },\\n  isObject$1 = function (e) {\\n    return getConstructor$1(e) === Object;\\n  },\\n  isNumber$1 = function (e) {\\n    return getConstructor$1(e) === Number && !Number.isNaN(e);\\n  },\\n  isString$1 = function (e) {\\n    return getConstructor$1(e) === String;\\n  },\\n  isBoolean$1 = function (e) {\\n    return getConstructor$1(e) === Boolean;\\n  },\\n  isFunction$1 = function (e) {\\n    return getConstructor$1(e) === Function;\\n  },\\n  isArray$1 = function (e) {\\n    return Array.isArray(e);\\n  },\\n  isNodeList$1 = function (e) {\\n    return instanceOf$1(e, NodeList);\\n  },\\n  isElement$1 = function (e) {\\n    return instanceOf$1(e, Element);\\n  },\\n  isEvent$1 = function (e) {\\n    return instanceOf$1(e, Event);\\n  },\\n  isEmpty$1 = function (e) {\\n    return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length;\\n  },\\n  is$1 = {\\n    nullOrUndefined: isNullOrUndefined$1,\\n    object: isObject$1,\\n    number: isNumber$1,\\n    string: isString$1,\\n    boolean: isBoolean$1,\\n    function: isFunction$1,\\n    array: isArray$1,\\n    nodeList: isNodeList$1,\\n    element: isElement$1,\\n    event: isEvent$1,\\n    empty: isEmpty$1\\n  };\\nfunction getDecimalPlaces(e) {\\n  var t = \\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);\\n  return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0;\\n}\\nfunction round(e, t) {\\n  if (1 > t) {\\n    var n = getDecimalPlaces(t);\\n    return parseFloat(e.toFixed(n));\\n  }\\n  return Math.round(e / t) * t;\\n}\\nvar RangeTouch = function () {\\n  function e(t, n) {\\n    _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init());\\n  }\\n  return _createClass(e, [{\\n    key: \\\"init\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"none\\\", this.element.style.webKitUserSelect = \\\"none\\\", this.element.style.touchAction = \\\"manipulation\\\"), this.listeners(!0), this.element.rangeTouch = this);\\n    }\\n  }, {\\n    key: \\\"destroy\\\",\\n    value: function () {\\n      e.enabled && (this.config.addCSS && (this.element.style.userSelect = \\\"\\\", this.element.style.webKitUserSelect = \\\"\\\", this.element.style.touchAction = \\\"\\\"), this.listeners(!1), this.element.rangeTouch = null);\\n    }\\n  }, {\\n    key: \\\"listeners\\\",\\n    value: function (e) {\\n      var t = this,\\n        n = e ? \\\"addEventListener\\\" : \\\"removeEventListener\\\";\\n      [\\\"touchstart\\\", \\\"touchmove\\\", \\\"touchend\\\"].forEach(function (e) {\\n        t.element[n](e, function (e) {\\n          return t.set(e);\\n        }, !1);\\n      });\\n    }\\n  }, {\\n    key: \\\"get\\\",\\n    value: function (t) {\\n      if (!e.enabled || !is$1.event(t)) return null;\\n      var n,\\n        r = t.target,\\n        i = t.changedTouches[0],\\n        o = parseFloat(r.getAttribute(\\\"min\\\")) || 0,\\n        s = parseFloat(r.getAttribute(\\\"max\\\")) || 100,\\n        u = parseFloat(r.getAttribute(\\\"step\\\")) || 1,\\n        c = r.getBoundingClientRect(),\\n        a = 100 / c.width * (this.config.thumbWidth / 2) / 100;\\n      return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u);\\n    }\\n  }, {\\n    key: \\\"set\\\",\\n    value: function (t) {\\n      e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, \\\"touchend\\\" === t.type ? \\\"change\\\" : \\\"input\\\"));\\n    }\\n  }], [{\\n    key: \\\"setup\\\",\\n    value: function (t) {\\n      var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {},\\n        r = null;\\n      if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type=\\\"range\\\"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null;\\n      var i = _objectSpread2({}, defaults$1, {}, n);\\n      if (is$1.string(t) && i.watch) {\\n        var o = new MutationObserver(function (n) {\\n          Array.from(n).forEach(function (n) {\\n            Array.from(n.addedNodes).forEach(function (n) {\\n              is$1.element(n) && matches$1(n, t) && new e(n, i);\\n            });\\n          });\\n        });\\n        o.observe(document.body, {\\n          childList: !0,\\n          subtree: !0\\n        });\\n      }\\n      return r.map(function (t) {\\n        return new e(t, n);\\n      });\\n    }\\n  }, {\\n    key: \\\"enabled\\\",\\n    get: function () {\\n      return \\\"ontouchstart\\\" in document.documentElement;\\n    }\\n  }]), e;\\n}();\\n\\n// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null;\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = input => input === null || typeof input === 'undefined';\\nconst isObject = input => getConstructor(input) === Object;\\nconst isNumber = input => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = input => getConstructor(input) === String;\\nconst isBoolean = input => getConstructor(input) === Boolean;\\nconst isFunction = input => typeof input === 'function';\\nconst isArray = input => Array.isArray(input);\\nconst isWeakMap = input => instanceOf(input, WeakMap);\\nconst isNodeList = input => instanceOf(input, NodeList);\\nconst isTextNode = input => getConstructor(input) === Text;\\nconst isEvent = input => instanceOf(input, Event);\\nconst isKeyboardEvent = input => instanceOf(input, KeyboardEvent);\\nconst isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind);\\nconst isPromise = input => instanceOf(input, Promise) && isFunction(input.then);\\nconst isElement = input => input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object';\\nconst isEmpty = input => isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length;\\nconst isUrl = input => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\nvar is = {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty\\n};\\n\\n// ==========================================================================\\nconst transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend'\\n  };\\n  const type = Object.keys(events).find(event => element.style[event] !== undefined);\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nfunction repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\\n// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\nvar browser = {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos\\n};\\n\\n// ==========================================================================\\n\\n// Clone nested objects\\nfunction cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nfunction getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nfunction extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n  const source = sources.shift();\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n  Object.keys(source).forEach(key => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, {\\n          [key]: {}\\n        });\\n      }\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, {\\n        [key]: source[key]\\n      });\\n    }\\n  });\\n  return extend(target, ...sources);\\n}\\n\\n// ==========================================================================\\n\\n// Wrap an element\\nfunction wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets).reverse().forEach((element, index) => {\\n    const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n    // Cache the current parent and sibling.\\n    const parent = element.parentNode;\\n    const sibling = element.nextSibling;\\n\\n    // Wrap the element (is automatically removed from its current\\n    // parent).\\n    child.appendChild(element);\\n\\n    // If the element had a sibling, insert the wrapper before\\n    // the sibling to maintain the HTML structure; otherwise, just\\n    // append it to the parent.\\n    if (sibling) {\\n      parent.insertBefore(child, sibling);\\n    } else {\\n      parent.appendChild(child);\\n    }\\n  });\\n}\\n\\n// Set attributes\\nfunction setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nfunction createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nfunction insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nfunction insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nfunction removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nfunction emptyElement(element) {\\n  if (!is.element(element)) return;\\n  let {\\n    length\\n  } = element.childNodes;\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nfunction replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nfunction getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n  sel.split(',').forEach(s => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n        break;\\n    }\\n  });\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nfunction toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n  let hide = hidden;\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nfunction toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map(e => toggleClass(e, className, force));\\n  }\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n  return false;\\n}\\n\\n// Has class name\\nfunction hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nfunction matches(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n  const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match;\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nfunction closest$1(element, selector) {\\n  const {\\n    prototype\\n  } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n  const method = prototype.closest || closestElement;\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nfunction getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nfunction getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nfunction setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({\\n    preventScroll: true,\\n    focusVisible\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora'\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n    return {\\n      api,\\n      ui\\n    };\\n  },\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n    return false;\\n  })(),\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches\\n};\\n\\n// ==========================================================================\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      }\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nfunction toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach(type => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({\\n        element,\\n        type,\\n        callback,\\n        options\\n      });\\n    }\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nfunction on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nfunction off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nfunction once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nfunction triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: {\\n      ...detail,\\n      plyr: this\\n    }\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nfunction unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach(item => {\\n      const {\\n        element,\\n        type,\\n        callback,\\n        options\\n      } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nfunction ready() {\\n  return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {});\\n}\\n\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nfunction silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Remove duplicates in an array\\nfunction dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nfunction closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n  return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);\\n}\\n\\n// ==========================================================================\\n\\n// Check support for a CSS declaration\\nfunction supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({\\n  ...out,\\n  [x / y]: [x, y]\\n}), {});\\n\\n// Validate an aspect ratio\\nfunction validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n  const ratio = is.array(input) ? input : input.split(':');\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nfunction reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h);\\n  const divider = getDivider(width, height);\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nfunction getAspectRatio(input) {\\n  const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null;\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({\\n      ratio\\n    } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const {\\n      videoWidth,\\n      videoHeight\\n    } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nfunction setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n  const {\\n    wrapper\\n  } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = 100 / x * y;\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = 100 / this.media.offsetWidth * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n  return {\\n    padding,\\n    ratio\\n  };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nfunction roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nfunction getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\\n// ==========================================================================\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter(source => {\\n      const type = source.getAttribute('type');\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n      return support.mime.call(this, type);\\n    });\\n  },\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean);\\n  },\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find(s => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find(s => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const {\\n            currentTime,\\n            paused,\\n            preload,\\n            readyState,\\n            playbackRate\\n          } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input\\n        });\\n      }\\n    });\\n  },\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Generate a random ID\\nfunction generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nfunction format(input, ...args) {\\n  if (is.empty(input)) return input;\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nfunction getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n  return (current / max * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nconst replaceAll = (input = '', find = '', replace = '') => input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nconst toTitleCase = (input = '') => input.toString().replace(/\\\\w\\\\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nfunction toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nfunction toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nfunction stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nfunction getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\\n// ==========================================================================\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube'\\n};\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n    let string = getDeep(config.i18n, key);\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n      return '';\\n    }\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title\\n    };\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n    return string;\\n  }\\n};\\n\\nclass Storage {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"get\\\", key => {\\n      if (!Storage.supported || !this.enabled) {\\n        return null;\\n      }\\n      const store = window.localStorage.getItem(this.key);\\n      if (is.empty(store)) {\\n        return null;\\n      }\\n      const json = JSON.parse(store);\\n      return is.string(key) && key.length ? json[key] : json;\\n    });\\n    _defineProperty$1(this, \\\"set\\\", object => {\\n      // Bail if we don't have localStorage support or it's disabled\\n      if (!Storage.supported || !this.enabled) {\\n        return;\\n      }\\n\\n      // Can only store objectst\\n      if (!is.object(object)) {\\n        return;\\n      }\\n\\n      // Get current storage\\n      let storage = this.get();\\n\\n      // Default to empty object\\n      if (is.empty(storage)) {\\n        storage = {};\\n      }\\n\\n      // Update the working copy of the values\\n      extend(storage, object);\\n\\n      // Update storage\\n      try {\\n        window.localStorage.setItem(this.key, JSON.stringify(storage));\\n      } catch (_) {\\n        // Do nothing\\n      }\\n    });\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nfunction fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Load an external SVG sprite\\nfunction loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url).then(result => {\\n      if (is.empty(result)) {\\n        return;\\n      }\\n      if (useStorage) {\\n        try {\\n          window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({\\n            content: result\\n          }));\\n        } catch (_) {\\n          // Do nothing\\n        }\\n      }\\n      update(container, result);\\n    }).catch(() => {});\\n  }\\n}\\n\\n// ==========================================================================\\n\\n// Time helpers\\nconst getHours = value => Math.trunc(value / 60 / 60 % 60, 10);\\nconst getMinutes = value => Math.trunc(value / 60 % 60, 10);\\nconst getSeconds = value => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nfunction formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = value => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\\n// ==========================================================================\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || browser.isIE && !window.svg4everybody;\\n    return {\\n      url: this.config.iconUrl,\\n      cors\\n    };\\n  },\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen)\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume)\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration)\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n      return false;\\n    }\\n  },\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(icon, extend(attributes, {\\n      'aria-hidden': 'true',\\n      focusable: 'false'\\n    }));\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n    return icon;\\n  },\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = {\\n      ...attr,\\n      class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ')\\n    };\\n    return createElement('span', attributes, text);\\n  },\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value\\n    });\\n    badge.appendChild(createElement('span', {\\n      class: this.config.classNames.menu.badge\\n    }, text));\\n    return badge;\\n  },\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null\\n    };\\n    ['element', 'icon', 'label'].forEach(key => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some(c => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(controls.createIcon.call(this, props.iconPressed, {\\n        class: 'icon--pressed'\\n      }));\\n      button.appendChild(controls.createIcon.call(this, props.icon, {\\n        class: 'icon--not-pressed'\\n      }));\\n\\n      // Label/Tooltip\\n      button.appendChild(controls.createLabel.call(this, props.labelPressed, {\\n        class: 'label--pressed'\\n      }));\\n      button.appendChild(controls.createLabel.call(this, props.label, {\\n        class: 'label--not-pressed'\\n      }));\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n    return button;\\n  },\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement('input', extend(getAttributesFromSelector(this.config.selectors.inputs[type]), {\\n      type: 'range',\\n      min: 0,\\n      max: 100,\\n      step: 0.01,\\n      value: 0,\\n      autocomplete: 'off',\\n      // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n      role: 'slider',\\n      'aria-label': i18n.get(type, this.config),\\n      'aria-valuemin': 0,\\n      'aria-valuemax': 100,\\n      'aria-valuenow': 0\\n    }, attributes));\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n    return input;\\n  },\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement('progress', extend(getAttributesFromSelector(this.config.selectors.display[type]), {\\n      min: 0,\\n      max: 100,\\n      value: 0,\\n      role: 'progressbar',\\n      'aria-hidden': true\\n    }, attributes));\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered'\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n    this.elements.display[type] = progress;\\n    return progress;\\n  },\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n    const container = createElement('div', extend(attributes, {\\n      class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n      'aria-label': i18n.get(type, this.config),\\n      role: 'timer'\\n    }), '00:00');\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n    return container;\\n  },\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(this, menuItem, 'keydown keyup', event => {\\n      // We only care about space and ⬆️ ⬇️️ ➡️\\n      if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Prevent play / seek\\n      event.preventDefault();\\n      event.stopPropagation();\\n\\n      // We're just here to prevent the keydown bubbling\\n      if (event.type === 'keydown') {\\n        return;\\n      }\\n      const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n      // Show the respective menu\\n      if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n        controls.showMenuPanel.call(this, type, true);\\n      } else {\\n        let target;\\n        if (event.key !== ' ') {\\n          if (event.key === 'ArrowDown' || isRadioButton && event.key === 'ArrowRight') {\\n            target = menuItem.nextElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.firstElementChild;\\n            }\\n          } else {\\n            target = menuItem.previousElementSibling;\\n            if (!is.element(target)) {\\n              target = menuItem.parentNode.lastElementChild;\\n            }\\n          }\\n          setFocus.call(this, target, true);\\n        }\\n      }\\n    }, false);\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', event => {\\n      if (event.key !== 'Return') return;\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n  // Create a settings menu item\\n  createMenuItem({\\n    value,\\n    list,\\n    type,\\n    title,\\n    badge = null,\\n    checked = false\\n  }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n    const menuItem = createElement('button', extend(attributes, {\\n      type: 'button',\\n      role: 'menuitemradio',\\n      class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n      'aria-checked': checked,\\n      value\\n    }));\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children).filter(node => matches(node, '[role=\\\"menuitemradio\\\"]')).forEach(node => node.setAttribute('aria-checked', 'false'));\\n        }\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      }\\n    });\\n    this.listeners.bind(menuItem, 'click keyup', event => {\\n      if (is.keyboardEvent(event) && event.key !== ' ') {\\n        return;\\n      }\\n      event.preventDefault();\\n      event.stopPropagation();\\n      menuItem.checked = true;\\n      switch (type) {\\n        case 'language':\\n          this.currentTrack = Number(value);\\n          break;\\n        case 'quality':\\n          this.quality = value;\\n          break;\\n        case 'speed':\\n          this.speed = parseFloat(value);\\n          break;\\n      }\\n      controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n    }, type, false);\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n    list.appendChild(menuItem);\\n  },\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n    return formatTime(time, forceHours, inverted);\\n  },\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n    let value = 0;\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n          break;\\n      }\\n    }\\n  },\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute('aria-valuetext', format.replace('{currentTime}', currentTime).replace('{duration}', duration));\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${range.value / range.max * 100}%`);\\n  },\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    var _this$config$markers, _this$config$markers$;\\n    // Bail if setting not true\\n    if (!this.config.tooltips.seek || !is.element(this.elements.inputs.seek) || !is.element(this.elements.display.seekTooltip) || this.duration === 0) {\\n      return;\\n    }\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = show => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n    if (is.event(event)) {\\n      percent = 100 / clientRect.width * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n    const time = this.duration / 100 * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = (_this$config$markers = this.config.markers) === null || _this$config$markers === void 0 ? void 0 : (_this$config$markers$ = _this$config$markers.points) === null || _this$config$markers$ === void 0 ? void 0 : _this$config$markers$.find(({\\n      time: t\\n    }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert);\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || !this.config.invertTime && this.currentTime) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n          return label;\\n        }\\n        return toTitleCase(value);\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n      default:\\n        return null;\\n    }\\n  },\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = quality => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n      if (!label.length) {\\n        return null;\\n      }\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality.sort((a, b) => {\\n      const sorting = this.config.quality.options;\\n      return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n    }).forEach(quality => {\\n      controls.createMenuItem.call(this, {\\n        value: quality,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'quality', quality),\\n        badge: getBadge(quality)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n         const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n         // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n         // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n         // Empty the menu\\n        emptyElement(list);\\n         options.forEach(option => {\\n            const item = createElement('li');\\n             const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n             if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n             item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language'\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language'\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach(speed => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed)\\n      });\\n    });\\n    controls.updateSetting.call(this, type, list);\\n  },\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const {\\n      buttons\\n    } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden);\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n    let target = pane;\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find(p => !p.hidden);\\n    }\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const {\\n      popup\\n    } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const {\\n      hidden\\n    } = popup;\\n    let show = hidden;\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || !isMenuItem && input.target !== button && show) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n    return {\\n      width,\\n      height\\n    };\\n  },\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find(node => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = event => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = {\\n      class: 'plyr__controls__item'\\n    };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`\\n        });\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(createRange.call(this, 'seek', {\\n          id: `plyr-seek-${data.id}`\\n        }));\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement('span', {\\n            class: this.config.classNames.tooltip\\n          }, '00:00');\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let {\\n          volume\\n        } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement('div', extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__volume`.trim()\\n          }));\\n          this.elements.volume = volume;\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(createRange.call(this, 'volume', extend(attributes, {\\n            id: `plyr-volume-${data.id}`\\n          })));\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement('div', extend({}, defaultAttributes, {\\n          class: `${defaultAttributes.class} plyr__menu`.trim(),\\n          hidden: ''\\n        }));\\n        wrapper.appendChild(createButton.call(this, 'settings', {\\n          'aria-haspopup': true,\\n          'aria-controls': `plyr-settings-${data.id}`,\\n          'aria-expanded': false\\n        }));\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: ''\\n        });\\n        const inner = createElement('div');\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu'\\n        });\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach(type => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n            role: 'menuitem',\\n            'aria-haspopup': true,\\n            hidden: ''\\n          }));\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: ''\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(createElement('span', {\\n            'aria-hidden': true\\n          }, i18n.get(type, this.config)));\\n\\n          // Screen reader label\\n          backButton.appendChild(createElement('span', {\\n            class: this.config.classNames.hidden\\n          }, i18n.get('menuBack', this.config)));\\n\\n          // Go back via keyboard\\n          on.call(this, pane, 'keydown', event => {\\n            if (event.key !== 'ArrowLeft') return;\\n\\n            // Prevent seek\\n            event.preventDefault();\\n            event.stopPropagation();\\n\\n            // Show the respective menu\\n            showMenuPanel.call(this, 'home', true);\\n          }, false);\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(createElement('div', {\\n            role: 'menu'\\n          }));\\n          inner.appendChild(pane);\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank'\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n        const {\\n          download\\n        } = this.config.urls;\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider\\n          });\\n        }\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n    setSpeedMenu.call(this);\\n    return container;\\n  },\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this)\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = input => {\\n      let result = input;\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = button => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          }\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons).filter(Boolean).forEach(button => {\\n        if (is.array(button) || is.nodeList(button)) {\\n          Array.from(button).filter(Boolean).forEach(addProperty);\\n        } else {\\n          addProperty(button);\\n        }\\n      });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const {\\n        classNames,\\n        selectors\\n      } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n      Array.from(labels).forEach(label => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n  // Add markers\\n  setMarkers() {\\n    var _this$config$markers2, _this$config$markers3;\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({\\n      time\\n    }) => time > 0 && time < this.duration);\\n    if (!(points !== null && points !== void 0 && points.length)) return;\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = show => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach(point => {\\n      const markerElement = createElement('span', {\\n        class: this.config.classNames.marker\\n      }, '');\\n      const left = `${point.time / this.duration * 100}%`;\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement('span', {\\n        class: this.config.classNames.tooltip\\n      }, '');\\n      containerFragment.appendChild(tipElement);\\n    }\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement\\n    };\\n    this.elements.progress.appendChild(containerFragment);\\n  }\\n};\\n\\n// ==========================================================================\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nfunction parseUrl(input, safe = true) {\\n  let url = input;\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nfunction buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n  return params;\\n}\\n\\n// ==========================================================================\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || this.isHTML5 && !support.textTracks) {\\n      // Clear menu and hide\\n      if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n      Array.from(elements).forEach(track => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n        if (url !== null && url.hostname !== window.location.href.hostname && ['http:', 'https:'].includes(url.protocol)) {\\n          fetch(src, 'blob').then(blob => {\\n            track.setAttribute('src', window.URL.createObjectURL(blob));\\n          }).catch(() => {\\n            removeElement(track);\\n          });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map(language => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({\\n        active\\n      } = this.config.captions);\\n    }\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const {\\n      active,\\n      language,\\n      meta,\\n      currentTrackNode\\n    } = this.captions;\\n    const languageExists = Boolean(tracks.find(track => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks.filter(track => !meta.get(track)).forEach(track => {\\n        this.debug.log('Track added', track);\\n\\n        // Attempt to store if the original dom element was \\\"default\\\"\\n        meta.set(track, {\\n          default: track.mode === 'showing'\\n        });\\n\\n        // Turn off native caption rendering to avoid double captions\\n        // Note: mode='hidden' forces a track to download. To ensure every track\\n        // isn't downloaded at once, only 'showing' tracks should be reassigned\\n        // eslint-disable-next-line no-param-reassign\\n        if (track.mode === 'showing') {\\n          // eslint-disable-next-line no-param-reassign\\n          track.mode = 'hidden';\\n        }\\n\\n        // Add event listener for cue changes\\n        on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n      });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if (languageExists && this.language !== language || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (is.array(this.config.controls) && this.config.controls.includes('settings') && this.config.settings.includes('captions')) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    const {\\n      toggled\\n    } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({\\n          captions: active\\n        });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const {\\n        language\\n      } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({\\n          language\\n        });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks.filter(track => !this.isHTML5 || update || this.captions.meta.has(track)).filter(track => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n    languages.every(language => {\\n      track = sorted.find(t => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n      return i18n.get('enabled', this.config);\\n    }\\n    return i18n.get('disabled', this.config);\\n  },\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n      cues = Array.from((track || {}).activeCues || []).map(cue => cue.getCueAsHTML()).map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map(cueText => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n  // Custom media title\\n  title: '',\\n  // Logging to console\\n  debug: false,\\n  // Auto play (if supported)\\n  autoplay: false,\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n  // Pass a custom duration\\n  duration: null,\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n  // Auto hide the controls\\n  hideControls: true,\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null\\n  },\\n  // Set loops\\n  loop: {\\n    active: false\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4]\\n  },\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false\\n  },\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true\\n  },\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false\\n  },\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true,\\n    // Allow fullscreen?\\n    fallback: true,\\n    // Fallback using full viewport/window\\n    iosNative: false // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr'\\n  },\\n  // Default controls\\n  controls: ['play-large',\\n  // 'restart',\\n  // 'rewind',\\n  'play',\\n  // 'fast-forward',\\n  'progress', 'current-time',\\n  // 'duration',\\n  'mute', 'volume', 'captions', 'settings', 'pip', 'airplay',\\n  // 'download',\\n  'fullscreen'],\\n  settings: ['captions', 'quality', 'speed'],\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD'\\n    }\\n  },\\n  // URLs\\n  urls: null,\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null\\n  },\\n  // Events to watch and bubble\\n  events: [\\n  // Events to watch on HTML5 media elements and bubble\\n  // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n  'ended', 'progress', 'stalled', 'playing', 'waiting', 'canplay', 'canplaythrough', 'loadstart', 'loadeddata', 'loadedmetadata', 'timeupdate', 'volumechange', 'play', 'pause', 'error', 'seeking', 'seeked', 'emptied', 'ratechange', 'cuechange',\\n  // Custom events\\n  'download', 'enterfullscreen', 'exitfullscreen', 'captionsenabled', 'captionsdisabled', 'languagechange', 'controlshidden', 'controlsshown', 'ready',\\n  // YouTube\\n  'statechange',\\n  // Quality\\n  'qualitychange',\\n  // Ads\\n  'adsloaded', 'adscontentpause', 'adscontentresume', 'adstarted', 'adsmidpoint', 'adscomplete', 'adsallcomplete', 'adsimpression', 'adsclick'],\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls'\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]'\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]'\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop',\\n      // Used later\\n      volume: '.plyr__volume--display'\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption'\\n  },\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time'\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open'\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active'\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback'\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active'\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active'\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown'\\n    }\\n  },\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash'\\n    }\\n  },\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: ''\\n  },\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: ''\\n  },\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null,\\n    // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false\\n  },\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0,\\n    // No related vids\\n    showinfo: 0,\\n    // Hide info\\n    iv_load_policy: 3,\\n    // Hide annotations\\n    modestbranding: 1,\\n    // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: []\\n  },\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: []\\n  }\\n};\\n\\n// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nconst pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline'\\n};\\n\\n// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nconst providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo'\\n};\\nconst types = {\\n  audio: 'audio',\\n  video: 'video'\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nfunction getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n  return null;\\n}\\n\\n// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\nclass Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"onChange\\\", () => {\\n      if (!this.supported) return;\\n\\n      // Update toggle button\\n      const button = this.player.elements.buttons.fullscreen;\\n      if (is.element(button)) {\\n        button.pressed = this.active;\\n      }\\n\\n      // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n      const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n      // Trigger an event\\n      triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n    });\\n    _defineProperty$1(this, \\\"toggleFallback\\\", (toggle = false) => {\\n      // Store or restore scroll position\\n      if (toggle) {\\n        this.scrollPosition = {\\n          x: window.scrollX ?? 0,\\n          y: window.scrollY ?? 0\\n        };\\n      } else {\\n        window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n      }\\n\\n      // Toggle scroll\\n      document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n      // Toggle class hook\\n      toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n      // Force full viewport on iPhone X+\\n      if (browser.isIos) {\\n        let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n        const property = 'viewport-fit=cover';\\n\\n        // Inject the viewport meta if required\\n        if (!viewport) {\\n          viewport = document.createElement('meta');\\n          viewport.setAttribute('name', 'viewport');\\n        }\\n\\n        // Check if the property already exists\\n        const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n        if (toggle) {\\n          this.cleanupViewport = !hasProperty;\\n          if (!hasProperty) viewport.content += `,${property}`;\\n        } else if (this.cleanupViewport) {\\n          viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(',');\\n        }\\n      }\\n\\n      // Toggle button and fire events\\n      this.onChange();\\n    });\\n    // Trap focus inside container\\n    _defineProperty$1(this, \\\"trapFocus\\\", event => {\\n      // Bail if iOS/iPadOS, not active, not the tab key\\n      if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n      // Get the current focused element\\n      const focused = document.activeElement;\\n      const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n      const [first] = focusable;\\n      const last = focusable[focusable.length - 1];\\n      if (focused === last && !event.shiftKey) {\\n        // Move focus to first element that can be tabbed if Shift isn't used\\n        first.focus();\\n        event.preventDefault();\\n      } else if (focused === first && event.shiftKey) {\\n        // Move focus to last element that can be tabbed if Shift is used\\n        last.focus();\\n        event.preventDefault();\\n      }\\n    });\\n    // Update UI\\n    _defineProperty$1(this, \\\"update\\\", () => {\\n      if (this.supported) {\\n        let mode;\\n        if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback';\\n        this.player.debug.log(`${mode} fullscreen enabled`);\\n      } else {\\n        this.player.debug.log('Fullscreen not supported and fallback disabled');\\n      }\\n\\n      // Add styling hook to show button\\n      toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n    });\\n    // Make an element fullscreen\\n    _defineProperty$1(this, \\\"enter\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen doesn't need the request step\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.requestFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(true);\\n      } else if (!this.prefix) {\\n        this.target.requestFullscreen({\\n          navigationUI: 'hide'\\n        });\\n      } else if (!is.empty(this.prefix)) {\\n        this.target[`${this.prefix}Request${this.property}`]();\\n      }\\n    });\\n    // Bail from fullscreen\\n    _defineProperty$1(this, \\\"exit\\\", () => {\\n      if (!this.supported) return;\\n\\n      // iOS native fullscreen\\n      if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n        if (this.player.isVimeo) {\\n          this.player.embed.exitFullscreen();\\n        } else {\\n          this.target.webkitEnterFullscreen();\\n        }\\n        silencePromise(this.player.play());\\n      } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n        this.toggleFallback(false);\\n      } else if (!this.prefix) {\\n        (document.cancelFullScreen || document.exitFullscreen).call(document);\\n      } else if (!is.empty(this.prefix)) {\\n        const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n        document[`${this.prefix}${action}${this.property}`]();\\n      }\\n    });\\n    // Toggle state\\n    _defineProperty$1(this, \\\"toggle\\\", () => {\\n      if (!this.active) this.enter();else this.exit();\\n    });\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = {\\n      x: 0,\\n      y: 0\\n    };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => {\\n      // TODO: Filter for target??\\n      this.onChange();\\n    });\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', event => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n    prefixes.some(pre => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n      return false;\\n    });\\n    return value;\\n  }\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n    // Fullscreen is enabled in config\\n    this.player.config.fullscreen.enabled,\\n    // Must be a video\\n    this.player.isVideo,\\n    // Either native is supported or fallback enabled\\n    Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n    // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n    // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n    !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n    const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n}\\n\\n// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nfunction loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n    Object.assign(image, {\\n      onload: handler,\\n      onerror: handler,\\n      src\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo);\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach(button => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return ready.call(this)\\n    // Load image\\n    .then(() => loadImage(poster)).catch(error => {\\n      // Hide poster on error unless it's been set by another call\\n      if (poster === this.poster) {\\n        ui.togglePoster.call(this, false);\\n      }\\n      // Rethrow\\n      throw error;\\n    }).then(() => {\\n      // Prevent race conditions\\n      if (poster !== this.poster) {\\n        throw new Error('setPoster cancelled by later call to setPoster');\\n      }\\n    }).then(() => {\\n      Object.assign(this.elements.poster.style, {\\n        backgroundImage: `url('${poster}')`,\\n        // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n        backgroundSize: ''\\n      });\\n      ui.togglePoster.call(this, true);\\n      return poster;\\n    });\\n  },\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach(target => {\\n      Object.assign(target, {\\n        pressed: this.playing\\n      });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(() => {\\n      // Update progress bar loading class state\\n      toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n      // Update controls visibility\\n      ui.toggleControls.call(this);\\n    }, this.loading ? 250 : 0);\\n  },\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const {\\n      controls: controlsElement\\n    } = this.elements;\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek));\\n    }\\n  },\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({\\n      ...this.media.style\\n    })\\n    // We're only fussed about Plyr specific properties\\n    .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => {\\n      // Set on the container\\n      this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n      // Clean up from media element\\n      this.media.style.removeProperty(key);\\n    });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  }\\n};\\n\\nclass Listeners {\\n  constructor(_player) {\\n    // Device is touch enabled\\n    _defineProperty$1(this, \\\"firstTouch\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      player.touch = true;\\n\\n      // Add touch class\\n      toggleClass(elements.container, player.config.classNames.isTouch, true);\\n    });\\n    // Global window & document listeners\\n    _defineProperty$1(this, \\\"global\\\", (toggle = true) => {\\n      const {\\n        player\\n      } = this;\\n\\n      // Keyboard shortcuts\\n      if (player.config.keyboard.global) {\\n        toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n      }\\n\\n      // Click anywhere closes menu\\n      toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n      // Detect touch by events\\n      once.call(player, document.body, 'touchstart', this.firstTouch);\\n    });\\n    // Container listeners\\n    _defineProperty$1(this, \\\"container\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        config,\\n        elements,\\n        timers\\n      } = player;\\n\\n      // Keyboard shortcuts\\n      if (!config.keyboard.global && config.keyboard.focused) {\\n        on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n      }\\n\\n      // Toggle controls on mouse events and entering fullscreen\\n      on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => {\\n        const {\\n          controls: controlsElement\\n        } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Set a gutter for Vimeo\\n      const setGutter = () => {\\n        if (!player.isVimeo || player.config.vimeo.premium) {\\n          return;\\n        }\\n        const target = elements.wrapper;\\n        const {\\n          active\\n        } = player.fullscreen;\\n        const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n        const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n        // If not active, remove styles\\n        if (!active) {\\n          if (useNativeAspectRatio) {\\n            target.style.width = null;\\n            target.style.height = null;\\n          } else {\\n            target.style.maxWidth = null;\\n            target.style.margin = null;\\n          }\\n          return;\\n        }\\n\\n        // Determine which dimension will overflow and constrain view\\n        const [viewportWidth, viewportHeight] = getViewportSize();\\n        const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n        if (useNativeAspectRatio) {\\n          target.style.width = overflow ? 'auto' : '100%';\\n          target.style.height = overflow ? '100%' : 'auto';\\n        } else {\\n          target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null;\\n          target.style.margin = overflow ? '0 auto' : null;\\n        }\\n      };\\n\\n      // Handle resizing\\n      const resized = () => {\\n        clearTimeout(timers.resized);\\n        timers.resized = setTimeout(setGutter, 50);\\n      };\\n      on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => {\\n        const {\\n          target\\n        } = player.fullscreen;\\n\\n        // Ignore events not from target\\n        if (target !== elements.container) {\\n          return;\\n        }\\n\\n        // If it's not an embed and no ratio specified\\n        if (!player.isEmbed && is.empty(player.config.ratio)) {\\n          return;\\n        }\\n\\n        // Set Vimeo gutter\\n        setGutter();\\n\\n        // Watch for resizes\\n        const method = event.type === 'enterfullscreen' ? on : off;\\n        method.call(player, window, 'resize', resized);\\n      });\\n    });\\n    // Listen for media events\\n    _defineProperty$1(this, \\\"media\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n\\n      // Time change on media\\n      on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event));\\n\\n      // Display duration\\n      on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event));\\n\\n      // Handle the media finishing\\n      on.call(player, player.media, 'ended', () => {\\n        // Show poster on end\\n        if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n          // Restart\\n          player.restart();\\n\\n          // Call pause otherwise IE11 will start playing the video again\\n          player.pause();\\n        }\\n      });\\n\\n      // Check for buffer progress\\n      on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event));\\n\\n      // Handle volume changes\\n      on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event));\\n\\n      // Handle play/pause\\n      on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event));\\n\\n      // Loading state\\n      on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event));\\n\\n      // Click video\\n      if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n        // Re-fetch the wrapper\\n        const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n        // Bail if there's no wrapper (this should never happen)\\n        if (!is.element(wrapper)) {\\n          return;\\n        }\\n\\n        // On click play, pause or restart\\n        on.call(player, elements.container, 'click', event => {\\n          const targets = [elements.container, wrapper];\\n\\n          // Ignore if click if not container or in video wrapper\\n          if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n            return;\\n          }\\n\\n          // Touch devices will just show controls (if hidden)\\n          if (player.touch && player.config.hideControls) {\\n            return;\\n          }\\n          if (player.ended) {\\n            this.proxy(event, player.restart, 'restart');\\n            this.proxy(event, () => {\\n              silencePromise(player.play());\\n            }, 'play');\\n          } else {\\n            this.proxy(event, () => {\\n              silencePromise(player.togglePlay());\\n            }, 'play');\\n          }\\n        });\\n      }\\n\\n      // Disable right click\\n      if (player.supported.ui && player.config.disableContextMenu) {\\n        on.call(player, elements.wrapper, 'contextmenu', event => {\\n          event.preventDefault();\\n        }, false);\\n      }\\n\\n      // Volume change\\n      on.call(player, player.media, 'volumechange', () => {\\n        // Save to storage\\n        player.storage.set({\\n          volume: player.volume,\\n          muted: player.muted\\n        });\\n      });\\n\\n      // Speed change\\n      on.call(player, player.media, 'ratechange', () => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'speed');\\n\\n        // Save to storage\\n        player.storage.set({\\n          speed: player.speed\\n        });\\n      });\\n\\n      // Quality change\\n      on.call(player, player.media, 'qualitychange', event => {\\n        // Update UI\\n        controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n      });\\n\\n      // Update download link when ready and if quality changes\\n      on.call(player, player.media, 'ready qualitychange', () => {\\n        controls.setDownloadUrl.call(player);\\n      });\\n\\n      // Proxy events to container\\n      // Bubble up key events for Edge\\n      const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n      on.call(player, player.media, proxyEvents, event => {\\n        let {\\n          detail = {}\\n        } = event;\\n\\n        // Get error details from media\\n        if (event.type === 'error') {\\n          detail = player.media.error;\\n        }\\n        triggerEvent.call(player, elements.container, event.type, true, detail);\\n      });\\n    });\\n    // Run default and custom handlers\\n    _defineProperty$1(this, \\\"proxy\\\", (event, defaultHandler, customHandlerKey) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      let returned = true;\\n\\n      // Execute custom handler\\n      if (hasCustomHandler) {\\n        returned = customHandler.call(player, event);\\n      }\\n\\n      // Only call default handler if not prevented in custom handler\\n      if (returned !== false && is.function(defaultHandler)) {\\n        defaultHandler.call(player, event);\\n      }\\n    });\\n    // Trigger custom and default handlers\\n    _defineProperty$1(this, \\\"bind\\\", (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n      const {\\n        player\\n      } = this;\\n      const customHandler = player.config.listeners[customHandlerKey];\\n      const hasCustomHandler = is.function(customHandler);\\n      on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);\\n    });\\n    // Listen for control events\\n    _defineProperty$1(this, \\\"controls\\\", () => {\\n      const {\\n        player\\n      } = this;\\n      const {\\n        elements\\n      } = player;\\n      // IE doesn't support input event, so we fallback to change\\n      const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n      // Play/pause toggle\\n      if (elements.buttons.play) {\\n        Array.from(elements.buttons.play).forEach(button => {\\n          this.bind(button, 'click', () => {\\n            silencePromise(player.togglePlay());\\n          }, 'play');\\n        });\\n      }\\n\\n      // Pause\\n      this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n      // Rewind\\n      this.bind(elements.buttons.rewind, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      }, 'rewind');\\n\\n      // Rewind\\n      this.bind(elements.buttons.fastForward, 'click', () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      }, 'fastForward');\\n\\n      // Mute toggle\\n      this.bind(elements.buttons.mute, 'click', () => {\\n        player.muted = !player.muted;\\n      }, 'mute');\\n\\n      // Captions toggle\\n      this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n      // Download\\n      this.bind(elements.buttons.download, 'click', () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      }, 'download');\\n\\n      // Fullscreen toggle\\n      this.bind(elements.buttons.fullscreen, 'click', () => {\\n        player.fullscreen.toggle();\\n      }, 'fullscreen');\\n\\n      // Picture-in-Picture\\n      this.bind(elements.buttons.pip, 'click', () => {\\n        player.pip = 'toggle';\\n      }, 'pip');\\n\\n      // Airplay\\n      this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n      // Settings menu - click toggle\\n      this.bind(elements.buttons.settings, 'click', event => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n        controls.toggleMenu.call(player, event);\\n      }, null, false); // Can't be passive as we're preventing default\\n\\n      // Settings menu - keyboard toggle\\n      // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n      // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n      this.bind(elements.buttons.settings, 'keyup', event => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      }, null, false // Can't be passive as we're preventing default\\n      );\\n\\n      // Escape closes menu\\n      this.bind(elements.settings.menu, 'keydown', event => {\\n        if (event.key === 'Escape') {\\n          controls.toggleMenu.call(player, event);\\n        }\\n      });\\n\\n      // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n      this.bind(elements.inputs.seek, 'mousedown mousemove', event => {\\n        const rect = elements.progress.getBoundingClientRect();\\n        const percent = 100 / rect.width * (event.pageX - rect.left);\\n        event.currentTarget.setAttribute('seek-value', percent);\\n      });\\n\\n      // Pause while seeking\\n      this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => {\\n        const seek = event.currentTarget;\\n        const attribute = 'play-on-seeked';\\n        if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Record seek time so we can prevent hiding controls for a few seconds after seek\\n        player.lastSeekTime = Date.now();\\n\\n        // Was playing before?\\n        const play = seek.hasAttribute(attribute);\\n        // Done seeking\\n        const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n        // If we're done seeking and it was playing, resume playback\\n        if (play && done) {\\n          seek.removeAttribute(attribute);\\n          silencePromise(player.play());\\n        } else if (!done && player.playing) {\\n          seek.setAttribute(attribute, '');\\n          player.pause();\\n        }\\n      });\\n\\n      // Fix range inputs on iOS\\n      // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n      // it takes over further interactions on the page. This is a hack\\n      if (browser.isIos) {\\n        const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n        Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target)));\\n      }\\n\\n      // Seek\\n      this.bind(elements.inputs.seek, inputEvent, event => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n        seek.removeAttribute('seek-value');\\n        player.currentTime = seekTo / seek.max * player.duration;\\n      }, 'seek');\\n\\n      // Seek tooltip\\n      this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event));\\n\\n      // Preview thumbnails plugin\\n      // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n      this.bind(elements.progress, 'mousemove touchmove', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startMove(event);\\n        }\\n      });\\n\\n      // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n      this.bind(elements.progress, 'mouseleave touchend click', () => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endMove(false, true);\\n        }\\n      });\\n\\n      // Show scrubbing preview\\n      this.bind(elements.progress, 'mousedown touchstart', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.startScrubbing(event);\\n        }\\n      });\\n      this.bind(elements.progress, 'mouseup touchend', event => {\\n        const {\\n          previewThumbnails\\n        } = player;\\n        if (previewThumbnails && previewThumbnails.loaded) {\\n          previewThumbnails.endScrubbing(event);\\n        }\\n      });\\n\\n      // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n      if (browser.isWebKit) {\\n        Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach(element => {\\n          this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target));\\n        });\\n      }\\n\\n      // Current time invert\\n      // Only if one time element is used for both currentTime and duration\\n      if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n        this.bind(elements.display.currentTime, 'click', () => {\\n          // Do nothing if we're at the start\\n          if (player.currentTime === 0) {\\n            return;\\n          }\\n          player.config.invertTime = !player.config.invertTime;\\n          controls.timeUpdate.call(player);\\n        });\\n      }\\n\\n      // Volume\\n      this.bind(elements.inputs.volume, inputEvent, event => {\\n        player.volume = event.target.value;\\n      }, 'volume');\\n\\n      // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mouseenter mouseleave', event => {\\n        elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n      });\\n\\n      // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n      if (elements.fullscreen) {\\n        Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => {\\n          this.bind(child, 'mouseenter mouseleave', event => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n      }\\n\\n      // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n      this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {\\n        elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n      });\\n\\n      // Show controls when they receive focus (e.g., when using keyboard tab key)\\n      this.bind(elements.controls, 'focusin', () => {\\n        const {\\n          config,\\n          timers\\n        } = player;\\n\\n        // Skip transition to prevent focus from scrolling the parent element\\n        toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n        // Toggle\\n        ui.toggleControls.call(player, true);\\n\\n        // Restore transition\\n        setTimeout(() => {\\n          toggleClass(elements.controls, config.classNames.noTransition, false);\\n        }, 0);\\n\\n        // Delay a little more for mouse users\\n        const delay = this.touch ? 3000 : 4000;\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Hide again after delay\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      });\\n\\n      // Mouse wheel for volume\\n      this.bind(elements.inputs.volume, 'wheel', event => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value);\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const {\\n          volume\\n        } = player.media;\\n        if (direction === 1 && volume < 1 || direction === -1 && volume > 0) {\\n          event.preventDefault();\\n        }\\n      }, 'volume', false);\\n    });\\n    this.player = _player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const {\\n      player\\n    } = this;\\n    const {\\n      elements\\n    } = player;\\n    const {\\n      key,\\n      type,\\n      altKey,\\n      ctrlKey,\\n      metaKey,\\n      shiftKey\\n    } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = increment => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = player.duration / 10 * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const {\\n          editable\\n        } = player.config.selectors;\\n        const {\\n          seek\\n        } = elements.inputs;\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm'];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n}\\n\\nvar loadjs_umd = createCommonjsModule(function (module, exports) {\\n  (function (root, factory) {\\n    {\\n      module.exports = factory();\\n    }\\n  })(commonjsGlobal, function () {\\n    /**\\n     * Global dependencies.\\n     * @global {Object} document - DOM\\n     */\\n\\n    var devnull = function () {},\\n      bundleIdCache = {},\\n      bundleResultCache = {},\\n      bundleCallbackQueue = {};\\n\\n    /**\\n     * Subscribe to bundle load event.\\n     * @param {string[]} bundleIds - Bundle ids\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function subscribe(bundleIds, callbackFn) {\\n      // listify\\n      bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n      var depsNotFound = [],\\n        i = bundleIds.length,\\n        numWaiting = i,\\n        fn,\\n        bundleId,\\n        r,\\n        q;\\n\\n      // define callback function\\n      fn = function (bundleId, pathsNotFound) {\\n        if (pathsNotFound.length) depsNotFound.push(bundleId);\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(depsNotFound);\\n      };\\n\\n      // register callback\\n      while (i--) {\\n        bundleId = bundleIds[i];\\n\\n        // execute callback if in result cache\\n        r = bundleResultCache[bundleId];\\n        if (r) {\\n          fn(bundleId, r);\\n          continue;\\n        }\\n\\n        // add to callback queue\\n        q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n        q.push(fn);\\n      }\\n    }\\n\\n    /**\\n     * Publish bundle load event.\\n     * @param {string} bundleId - Bundle id\\n     * @param {string[]} pathsNotFound - List of files not found\\n     */\\n    function publish(bundleId, pathsNotFound) {\\n      // exit if id isn't defined\\n      if (!bundleId) return;\\n      var q = bundleCallbackQueue[bundleId];\\n\\n      // cache result\\n      bundleResultCache[bundleId] = pathsNotFound;\\n\\n      // exit if queue is empty\\n      if (!q) return;\\n\\n      // empty callback queue\\n      while (q.length) {\\n        q[0](bundleId, pathsNotFound);\\n        q.splice(0, 1);\\n      }\\n    }\\n\\n    /**\\n     * Execute callbacks.\\n     * @param {Object or Function} args - The callback args\\n     * @param {string[]} depsNotFound - List of dependencies not found\\n     */\\n    function executeCallbacks(args, depsNotFound) {\\n      // accept function as argument\\n      if (args.call) args = {\\n        success: args\\n      };\\n\\n      // success and error callbacks\\n      if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args);\\n    }\\n\\n    /**\\n     * Load individual file.\\n     * @param {string} path - The file path\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFile(path, callbackFn, args, numTries) {\\n      var doc = document,\\n        async = args.async,\\n        maxTries = (args.numRetries || 0) + 1,\\n        beforeCallbackFn = args.before || devnull,\\n        pathname = path.replace(/[\\\\?|#].*$/, ''),\\n        pathStripped = path.replace(/^(css|img)!/, ''),\\n        isLegacyIECss,\\n        e;\\n      numTries = numTries || 0;\\n      if (/(^css!|\\\\.css$)/.test(pathname)) {\\n        // css\\n        e = doc.createElement('link');\\n        e.rel = 'stylesheet';\\n        e.href = pathStripped;\\n\\n        // tag IE9+\\n        isLegacyIECss = 'hideFocus' in e;\\n\\n        // use preload in IE Edge (to detect load errors)\\n        if (isLegacyIECss && e.relList) {\\n          isLegacyIECss = 0;\\n          e.rel = 'preload';\\n          e.as = 'style';\\n        }\\n      } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n        // image\\n        e = doc.createElement('img');\\n        e.src = pathStripped;\\n      } else {\\n        // javascript\\n        e = doc.createElement('script');\\n        e.src = path;\\n        e.async = async === undefined ? true : async;\\n      }\\n      e.onload = e.onerror = e.onbeforeload = function (ev) {\\n        var result = ev.type[0];\\n\\n        // treat empty stylesheets as failures to get around lack of onerror\\n        // support in IE9-11\\n        if (isLegacyIECss) {\\n          try {\\n            if (!e.sheet.cssText.length) result = 'e';\\n          } catch (x) {\\n            // sheets objects created from load errors don't allow access to\\n            // `cssText` (unless error is Code:18 SecurityError)\\n            if (x.code != 18) result = 'e';\\n          }\\n        }\\n\\n        // handle retries in case of load failure\\n        if (result == 'e') {\\n          // increment counter\\n          numTries += 1;\\n\\n          // exit function and try again\\n          if (numTries < maxTries) {\\n            return loadFile(path, callbackFn, args, numTries);\\n          }\\n        } else if (e.rel == 'preload' && e.as == 'style') {\\n          // activate preloaded stylesheets\\n          return e.rel = 'stylesheet'; // jshint ignore:line\\n        }\\n\\n        // execute callback\\n        callbackFn(path, result, ev.defaultPrevented);\\n      };\\n\\n      // add to document (unless callback returns `false`)\\n      if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n    }\\n\\n    /**\\n     * Load multiple files.\\n     * @param {string[]} paths - The file paths\\n     * @param {Function} callbackFn - The callback function\\n     */\\n    function loadFiles(paths, callbackFn, args) {\\n      // listify paths\\n      paths = paths.push ? paths : [paths];\\n      var numWaiting = paths.length,\\n        x = numWaiting,\\n        pathsNotFound = [],\\n        fn,\\n        i;\\n\\n      // define callback function\\n      fn = function (path, result, defaultPrevented) {\\n        // handle error\\n        if (result == 'e') pathsNotFound.push(path);\\n\\n        // handle beforeload event. If defaultPrevented then that means the load\\n        // will be blocked (ex. Ghostery/ABP on Safari)\\n        if (result == 'b') {\\n          if (defaultPrevented) pathsNotFound.push(path);else return;\\n        }\\n        numWaiting--;\\n        if (!numWaiting) callbackFn(pathsNotFound);\\n      };\\n\\n      // load scripts\\n      for (i = 0; i < x; i++) loadFile(paths[i], fn, args);\\n    }\\n\\n    /**\\n     * Initiate script load and register bundle.\\n     * @param {(string|string[])} paths - The file paths\\n     * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n     *   callback or (3) object literal with success/error arguments, numRetries,\\n     *   etc.\\n     * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n     *   literal with success/error arguments, numRetries, etc.\\n     */\\n    function loadjs(paths, arg1, arg2) {\\n      var bundleId, args;\\n\\n      // bundleId (if string)\\n      if (arg1 && arg1.trim) bundleId = arg1;\\n\\n      // args (default is {})\\n      args = (bundleId ? arg2 : arg1) || {};\\n\\n      // throw error if bundle is already defined\\n      if (bundleId) {\\n        if (bundleId in bundleIdCache) {\\n          throw \\\"LoadJS\\\";\\n        } else {\\n          bundleIdCache[bundleId] = true;\\n        }\\n      }\\n      function loadFn(resolve, reject) {\\n        loadFiles(paths, function (pathsNotFound) {\\n          // execute callbacks\\n          executeCallbacks(args, pathsNotFound);\\n\\n          // resolve Promise\\n          if (resolve) {\\n            executeCallbacks({\\n              success: resolve,\\n              error: reject\\n            }, pathsNotFound);\\n          }\\n\\n          // publish bundle load event\\n          publish(bundleId, pathsNotFound);\\n        }, args);\\n      }\\n      if (args.returnPromise) return new Promise(loadFn);else loadFn();\\n    }\\n\\n    /**\\n     * Execute callbacks when dependencies have been satisfied.\\n     * @param {(string|string[])} deps - List of bundle ids\\n     * @param {Object} args - success/error arguments\\n     */\\n    loadjs.ready = function ready(deps, args) {\\n      // subscribe to bundle load event\\n      subscribe(deps, function (depsNotFound) {\\n        // execute callbacks\\n        executeCallbacks(args, depsNotFound);\\n      });\\n      return loadjs;\\n    };\\n\\n    /**\\n     * Manually satisfy bundle dependencies.\\n     * @param {string} bundleId - The bundle id\\n     */\\n    loadjs.done = function done(bundleId) {\\n      publish(bundleId, []);\\n    };\\n\\n    /**\\n     * Reset loadjs dependencies statuses\\n     */\\n    loadjs.reset = function reset() {\\n      bundleIdCache = {};\\n      bundleResultCache = {};\\n      bundleCallbackQueue = {};\\n    };\\n\\n    /**\\n     * Determine if bundle has already been defined\\n     * @param String} bundleId - The bundle id\\n     */\\n    loadjs.isDefined = function isDefined(bundleId) {\\n      return bundleId in bundleIdCache;\\n    };\\n\\n    // export\\n    return loadjs;\\n  });\\n});\\n\\n// ==========================================================================\\nfunction loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs_umd(url, {\\n      success: resolve,\\n      error: reject\\n    });\\n  });\\n}\\n\\n// ==========================================================================\\n\\n// Parse Vimeo ID from URL\\nfunction parseId$1(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState$1(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk).then(() => {\\n        vimeo.ready.call(player);\\n      }).catch(error => {\\n        player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n      });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const {\\n      premium,\\n      referrerPolicy,\\n      ...frameParams\\n    } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? {\\n      h: hash\\n    } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams\\n    });\\n    const id = parseId$1(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '));\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then(response => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted\\n    });\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState$1.call(player, true);\\n      return player.embed.play();\\n    };\\n    player.media.pause = () => {\\n      assurePlaybackState$1.call(player, false);\\n      return player.embed.pause();\\n    };\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let {\\n      currentTime\\n    } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const {\\n          embed,\\n          media,\\n          paused,\\n          volume\\n        } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n        // Seek\\n        .then(() => embed.setCurrentTime(time))\\n        // Restore paused\\n        .then(() => restorePause && embed.pause())\\n        // Restore volume\\n        .then(() => restorePause && embed.setVolume(volume)).catch(() => {\\n          // Do nothing\\n        });\\n      }\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed.setPlaybackRate(input).then(() => {\\n          speed = input;\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        }).catch(() => {\\n          // Cannot set Playback Rate, Video is probably not on Pro account\\n          player.options.speed = [1];\\n        });\\n      }\\n    });\\n\\n    // Volume\\n    let {\\n      volume\\n    } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Muted\\n    let {\\n      muted\\n    } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      }\\n    });\\n\\n    // Loop\\n    let {\\n      loop\\n    } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      }\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed.getVideoUrl().then(value => {\\n      currentSrc = value;\\n      controls.setDownloadUrl.call(player);\\n    }).catch(error => {\\n      this.debug.warn(error);\\n    });\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      }\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      }\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then(state => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then(title => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then(value => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then(value => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then(tracks => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n    player.embed.on('cuechange', ({\\n      cues = []\\n    }) => {\\n      const strippedCues = cues.map(cue => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then(paused => {\\n        assurePlaybackState$1.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('play', () => {\\n      assurePlaybackState$1.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n    player.embed.on('pause', () => {\\n      assurePlaybackState$1.call(player, false);\\n    });\\n    player.embed.on('timeupdate', data => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n    player.embed.on('progress', data => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then(value => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n    player.embed.on('error', detail => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  }\\n};\\n\\n// ==========================================================================\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch(error => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n    fetch(url).then(data => {\\n      if (is.object(data)) {\\n        const {\\n          title,\\n          height,\\n          width\\n        } = data;\\n\\n        // Set title\\n        this.config.title = title;\\n        ui.setTitle.call(this);\\n\\n        // Set aspect ratio\\n        this.embed.ratio = roundAspectRatio(width, height);\\n      }\\n      setAspectRatio.call(this);\\n    }).catch(() => {\\n      // Set aspect ratio\\n      setAspectRatio.call(this);\\n    });\\n  },\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', {\\n      id,\\n      'data-poster': config.customControls ? player.poster : undefined\\n    });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = s => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n      .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n      .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n      .then(image => ui.setPoster.call(player, image.src)).then(src => {\\n        // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n        if (!src.includes('maxres')) {\\n          player.elements.poster.style.backgroundSize = 'cover';\\n        }\\n      }).catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend({}, {\\n        // Autoplay\\n        autoplay: player.config.autoplay ? 1 : 0,\\n        // iframe interface language\\n        hl: player.config.hl,\\n        // Only show controls if not fully supported or opted out\\n        controls: player.supported.ui && config.customControls ? 0 : 1,\\n        // Disable keyboard as we handle it\\n        disablekb: 1,\\n        // Allow iOS inline playback\\n        playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n        // Captions are flaky on YouTube\\n        cc_load_policy: player.captions.active ? 1 : 0,\\n        cc_lang_pref: player.config.captions.language,\\n        // Tracking for stats\\n        widget_referrer: window ? window.location.href : null\\n      }, config),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message = {\\n              2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n              5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n              100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n              101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              150: 'The owner of the requested video does not allow it to be played in embedded players.'\\n            }[code] || 'An unknown error occurred';\\n            player.media.error = {\\n              code,\\n              message\\n            };\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            }\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            }\\n          });\\n\\n          // Volume\\n          let {\\n            volume\\n          } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Muted\\n          let {\\n            muted\\n          } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            }\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            }\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            }\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter(s => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n              break;\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n              break;\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n              break;\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n              break;\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n              break;\\n          }\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data\\n          });\\n        }\\n      }\\n    });\\n  }\\n};\\n\\n// ==========================================================================\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster\\n      });\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  }\\n};\\n\\nconst destroy = instance => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n  instance.elements.container.remove();\\n};\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    /**\\n     * Load the IMA SDK\\n     */\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Check if the Google IMA3 SDK is loaded or load it ourselves\\n      if (!is.object(window.google) || !is.object(window.google.ima)) {\\n        loadScript(this.player.config.urls.googleIMA.sdk).then(() => {\\n          this.ready();\\n        }).catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n      } else {\\n        this.ready();\\n      }\\n    });\\n    /**\\n     * Get the ads instance ready\\n     */\\n    _defineProperty$1(this, \\\"ready\\\", () => {\\n      // Double check we're enabled\\n      if (!this.enabled) {\\n        destroy(this);\\n      }\\n\\n      // Start ticking our safety timer. If the whole advertisement\\n      // thing doesn't resolve within our set time; we bail\\n      this.startSafetyTimer(12000, 'ready()');\\n\\n      // Clear the safety timer\\n      this.managerPromise.then(() => {\\n        this.clearSafetyTimer('onAdsManagerLoaded()');\\n      });\\n\\n      // Set listeners on the Plyr instance\\n      this.listeners();\\n\\n      // Setup the IMA SDK\\n      this.setupIMA();\\n    });\\n    /**\\n     * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n     * so here we define our ad container. This div is set up to render on top of the video player.\\n     * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n     * handle to the content video player - the SDK will poll the current time of our player to\\n     * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n     * mobile devices, this initialization is done as the result of a user action.\\n     */\\n    _defineProperty$1(this, \\\"setupIMA\\\", () => {\\n      // Create the container for our advertisements\\n      this.elements.container = createElement('div', {\\n        class: this.player.config.classNames.ads\\n      });\\n      this.player.elements.container.appendChild(this.elements.container);\\n\\n      // So we can run VPAID2\\n      google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n      // Set language\\n      google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n      // Set playback for iOS10+\\n      google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n      // We assume the adContainer is the video container of the plyr element that will house the ads\\n      this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n      // Create ads loader\\n      this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n      // Listen and respond to ads loaded and error events\\n      this.loader.addEventListener(google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, event => this.onAdsManagerLoaded(event), false);\\n      this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error), false);\\n\\n      // Request video ads to be pre-loaded\\n      this.requestAds();\\n    });\\n    /**\\n     * Request advertisements\\n     */\\n    _defineProperty$1(this, \\\"requestAds\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      try {\\n        // Request video ads\\n        const request = new google.ima.AdsRequest();\\n        request.adTagUrl = this.tagUrl;\\n\\n        // Specify the linear and nonlinear slot sizes. This helps the SDK\\n        // to select the correct creative if multiple are returned\\n        request.linearAdSlotWidth = container.offsetWidth;\\n        request.linearAdSlotHeight = container.offsetHeight;\\n        request.nonLinearAdSlotWidth = container.offsetWidth;\\n        request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n        // We only overlay ads as we only support video.\\n        request.forceNonLinearFullSlot = false;\\n\\n        // Mute based on current state\\n        request.setAdWillPlayMuted(!this.player.muted);\\n        this.loader.requestAds(request);\\n      } catch (error) {\\n        this.onAdError(error);\\n      }\\n    });\\n    /**\\n     * Update the ad countdown\\n     * @param {Boolean} start\\n     */\\n    _defineProperty$1(this, \\\"pollCountdown\\\", (start = false) => {\\n      if (!start) {\\n        clearInterval(this.countdownTimer);\\n        this.elements.container.removeAttribute('data-badge-text');\\n        return;\\n      }\\n      const update = () => {\\n        const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n        const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n        this.elements.container.setAttribute('data-badge-text', label);\\n      };\\n      this.countdownTimer = setInterval(update, 100);\\n    });\\n    /**\\n     * This method is called whenever the ads are ready inside the AdDisplayContainer\\n     * @param {Event} event - adsManagerLoadedEvent\\n     */\\n    _defineProperty$1(this, \\\"onAdsManagerLoaded\\\", event => {\\n      // Load could occur after a source change (race condition)\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Get the ads manager\\n      const settings = new google.ima.AdsRenderingSettings();\\n\\n      // Tell the SDK to save and restore content video state on our behalf\\n      settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n      settings.enablePreloading = true;\\n\\n      // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n      // so it can determine when to start the mid- and post-roll\\n      this.manager = event.getAdsManager(this.player, settings);\\n\\n      // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n      this.cuePoints = this.manager.getCuePoints();\\n\\n      // Add listeners to the required events\\n      // Advertisement error events\\n      this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, error => this.onAdError(error));\\n\\n      // Advertisement regular events\\n      Object.keys(google.ima.AdEvent.Type).forEach(type => {\\n        this.manager.addEventListener(google.ima.AdEvent.Type[type], e => this.onAdEvent(e));\\n      });\\n\\n      // Resolve our adsManager\\n      this.trigger('loaded');\\n    });\\n    _defineProperty$1(this, \\\"addCuePoints\\\", () => {\\n      // Add advertisement cue's within the time line if available\\n      if (!is.empty(this.cuePoints)) {\\n        this.cuePoints.forEach(cuePoint => {\\n          if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n            const seekElement = this.player.elements.progress;\\n            if (is.element(seekElement)) {\\n              const cuePercentage = 100 / this.player.duration * cuePoint;\\n              const cue = createElement('span', {\\n                class: this.player.config.classNames.cues\\n              });\\n              cue.style.left = `${cuePercentage.toString()}%`;\\n              seekElement.appendChild(cue);\\n            }\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n     * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n     * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdEvent\\\", event => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n      // don't have ad object associated\\n      const ad = event.getAd();\\n      const adData = event.getAdData();\\n\\n      // Proxy event\\n      const dispatchEvent = type => {\\n        triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n      };\\n\\n      // Bubble the event\\n      dispatchEvent(event.type);\\n      switch (event.type) {\\n        case google.ima.AdEvent.Type.LOADED:\\n          // This is the first event sent for an ad - it is possible to determine whether the\\n          // ad is a video ad or an overlay\\n          this.trigger('loaded');\\n\\n          // Start countdown\\n          this.pollCountdown(true);\\n          if (!ad.isLinear()) {\\n            // Position AdDisplayContainer correctly for overlay\\n            ad.width = container.offsetWidth;\\n            ad.height = container.offsetHeight;\\n          }\\n\\n          // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n          // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n          break;\\n        case google.ima.AdEvent.Type.STARTED:\\n          // Set volume to match player\\n          this.manager.setVolume(this.player.volume);\\n          break;\\n        case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n          // All ads for the current videos are done. We can now request new advertisements\\n          // in case the video is re-played\\n\\n          // TODO: Example for what happens when a next video in a playlist would be loaded.\\n          // So here we load a new video when all ads are done.\\n          // Then we load new ads within a new adsManager. When the video\\n          // Is started - after - the ads are loaded, then we get ads.\\n          // You can also easily test cancelling and reloading by running\\n          // player.ads.cancel() and player.ads.play from the console I guess.\\n          // this.player.source = {\\n          //     type: 'video',\\n          //     title: 'View From A Blue Moon',\\n          //     sources: [{\\n          //         src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n          // 'video/mp4', }], poster:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n          // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n          // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n          // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n          // };\\n\\n          // TODO: So there is still this thing where a video should only be allowed to start\\n          // playing when the IMA SDK is ready or has failed\\n\\n          if (this.player.ended) {\\n            this.loadAds();\\n          } else {\\n            // The SDK won't allow new ads to be called without receiving a contentComplete()\\n            this.loader.contentComplete();\\n          }\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n          // This event indicates the ad has started - the video player can adjust the UI,\\n          // for example display a pause button and remaining time. Fired when content should\\n          // be paused. This usually happens right before an ad is about to cover the content\\n\\n          this.pauseContent();\\n          break;\\n        case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n          // This event indicates the ad has finished - the video player can perform\\n          // appropriate UI actions, such as removing the timer for remaining time detection.\\n          // Fired when content should be resumed. This usually happens when an ad finishes\\n          // or collapses\\n\\n          this.pollCountdown();\\n          this.resumeContent();\\n          break;\\n        case google.ima.AdEvent.Type.LOG:\\n          if (adData.adError) {\\n            this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n          }\\n          break;\\n      }\\n    });\\n    /**\\n     * Any ad error handling comes through here\\n     * @param {Event} event\\n     */\\n    _defineProperty$1(this, \\\"onAdError\\\", event => {\\n      this.cancel();\\n      this.player.debug.warn('Ads error', event);\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events. This ensures\\n     * the mid- and post-roll launch at the correct time. And\\n     * resize the advertisement when the player resizes\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      let time;\\n      this.player.on('canplay', () => {\\n        this.addCuePoints();\\n      });\\n      this.player.on('ended', () => {\\n        this.loader.contentComplete();\\n      });\\n      this.player.on('timeupdate', () => {\\n        time = this.player.currentTime;\\n      });\\n      this.player.on('seeked', () => {\\n        const seekedTime = this.player.currentTime;\\n        if (is.empty(this.cuePoints)) {\\n          return;\\n        }\\n        this.cuePoints.forEach((cuePoint, index) => {\\n          if (time < cuePoint && cuePoint < seekedTime) {\\n            this.manager.discardAdBreak();\\n            this.cuePoints.splice(index, 1);\\n          }\\n        });\\n      });\\n\\n      // Listen to the resizing of the window. And resize ad accordingly\\n      // TODO: eventually implement ResizeObserver\\n      window.addEventListener('resize', () => {\\n        if (this.manager) {\\n          this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n        }\\n      });\\n    });\\n    /**\\n     * Initialize the adsManager and start playing advertisements\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      const {\\n        container\\n      } = this.player.elements;\\n      if (!this.managerPromise) {\\n        this.resumeContent();\\n      }\\n\\n      // Play the requested advertisement whenever the adsManager is ready\\n      this.managerPromise.then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Resume our video\\n     */\\n    _defineProperty$1(this, \\\"resumeContent\\\", () => {\\n      // Hide the advertisement container\\n      this.elements.container.style.zIndex = '';\\n\\n      // Ad is stopped\\n      this.playing = false;\\n\\n      // Play video\\n      silencePromise(this.player.media.play());\\n    });\\n    /**\\n     * Pause our video\\n     */\\n    _defineProperty$1(this, \\\"pauseContent\\\", () => {\\n      // Show the advertisement container\\n      this.elements.container.style.zIndex = 3;\\n\\n      // Ad is playing\\n      this.playing = true;\\n\\n      // Pause our video.\\n      this.player.media.pause();\\n    });\\n    /**\\n     * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n     * allowed to call new ads based on google policies, as they interpret this as an accidental\\n     * video requests. https://developers.google.com/interactive-\\n     * media-ads/docs/sdks/android/faq#8\\n     */\\n    _defineProperty$1(this, \\\"cancel\\\", () => {\\n      // Pause our video\\n      if (this.initialized) {\\n        this.resumeContent();\\n      }\\n\\n      // Tell our instance that we're done for now\\n      this.trigger('error');\\n\\n      // Re-create our adsManager\\n      this.loadAds();\\n    });\\n    /**\\n     * Re-create our adsManager\\n     */\\n    _defineProperty$1(this, \\\"loadAds\\\", () => {\\n      // Tell our adsManager to go bye bye\\n      this.managerPromise.then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise(resolve => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      }).catch(() => {});\\n    });\\n    /**\\n     * Handles callbacks after an ad event was invoked\\n     * @param {String} event - Event type\\n     * @param args\\n     */\\n    _defineProperty$1(this, \\\"trigger\\\", (event, ...args) => {\\n      const handlers = this.events[event];\\n      if (is.array(handlers)) {\\n        handlers.forEach(handler => {\\n          if (is.function(handler)) {\\n            handler.apply(this, args);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     * @return {Ads}\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      if (!is.array(this.events[event])) {\\n        this.events[event] = [];\\n      }\\n      this.events[event].push(callback);\\n      return this;\\n    });\\n    /**\\n     * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n     * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n     * advertisement is playing, or when a user action is required to start, then we clear the\\n     * timer on ad ready\\n     * @param {Number} time\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"startSafetyTimer\\\", (time, from) => {\\n      this.player.debug.log(`Safety timer invoked from: ${from}`);\\n      this.safetyTimer = setTimeout(() => {\\n        this.cancel();\\n        this.clearSafetyTimer('startSafetyTimer()');\\n      }, time);\\n    });\\n    /**\\n     * Clear our safety timer(s)\\n     * @param {String} from\\n     */\\n    _defineProperty$1(this, \\\"clearSafetyTimer\\\", from => {\\n      if (!is.nullOrUndefined(this.safetyTimer)) {\\n        this.player.debug.log(`Safety timer cleared from: ${from}`);\\n        clearTimeout(this.safetyTimer);\\n        this.safetyTimer = null;\\n      }\\n    });\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n    this.load();\\n  }\\n  get enabled() {\\n    const {\\n      config\\n    } = this;\\n    return this.player.isHTML5 && this.player.isVideo && config.enabled && (!is.empty(config.publisherId) || is.url(config.tagUrl));\\n  }\\n  // Build the tag URL\\n  get tagUrl() {\\n    const {\\n      config\\n    } = this;\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId\\n    };\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n}\\n\\n/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nfunction clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = vttDataString => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n  frames.forEach(frame => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n    lines.forEach(line => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(/([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime = Number(matchTimes[1] || 0) * 60 * 60 + Number(matchTimes[2]) * 60 + Number(matchTimes[3]) + Number(`0.${matchTimes[4]}`);\\n          result.endTime = Number(matchTimes[6] || 0) * 60 * 60 + Number(matchTimes[7]) * 60 + Number(matchTimes[8]) + Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = 1 / ratio * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n  return result;\\n};\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    _defineProperty$1(this, \\\"load\\\", () => {\\n      // Toggle the regular seek tooltip\\n      if (this.player.elements.display.seekTooltip) {\\n        this.player.elements.display.seekTooltip.hidden = this.enabled;\\n      }\\n      if (!this.enabled) return;\\n      this.getThumbnails().then(() => {\\n        if (!this.enabled) {\\n          return;\\n        }\\n\\n        // Render DOM elements\\n        this.render();\\n\\n        // Check to see if thumb container size was specified manually in CSS\\n        this.determineContainerAutoSizing();\\n\\n        // Set up listeners\\n        this.listeners();\\n        this.loaded = true;\\n      });\\n    });\\n    // Download VTT files and parse them\\n    _defineProperty$1(this, \\\"getThumbnails\\\", () => {\\n      return new Promise(resolve => {\\n        const {\\n          src\\n        } = this.player.config.previewThumbnails;\\n        if (is.empty(src)) {\\n          throw new Error('Missing previewThumbnails.src config attribute');\\n        }\\n\\n        // Resolve promise\\n        const sortAndResolve = () => {\\n          // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n          this.thumbnails.sort((x, y) => x.height - y.height);\\n          this.player.debug.log('Preview thumbnails', this.thumbnails);\\n          resolve();\\n        };\\n\\n        // Via callback()\\n        if (is.function(src)) {\\n          src(thumbnails => {\\n            this.thumbnails = thumbnails;\\n            sortAndResolve();\\n          });\\n        }\\n        // VTT urls\\n        else {\\n          // If string, convert into single-element list\\n          const urls = is.string(src) ? [src] : src;\\n          // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n          const promises = urls.map(u => this.getThumbnail(u));\\n          // Resolve\\n          Promise.all(promises).then(sortAndResolve);\\n        }\\n      });\\n    });\\n    // Process individual VTT file\\n    _defineProperty$1(this, \\\"getThumbnail\\\", url => {\\n      return new Promise(resolve => {\\n        fetch(url).then(response => {\\n          const thumbnail = {\\n            frames: parseVtt(response),\\n            height: null,\\n            urlPrefix: ''\\n          };\\n\\n          // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n          // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n          // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n          if (!thumbnail.frames[0].text.startsWith('/') && !thumbnail.frames[0].text.startsWith('http://') && !thumbnail.frames[0].text.startsWith('https://')) {\\n            thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n          }\\n\\n          // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n          const tempImage = new Image();\\n          tempImage.onload = () => {\\n            thumbnail.height = tempImage.naturalHeight;\\n            thumbnail.width = tempImage.naturalWidth;\\n            this.thumbnails.push(thumbnail);\\n            resolve();\\n          };\\n          tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n        });\\n      });\\n    });\\n    _defineProperty$1(this, \\\"startMove\\\", event => {\\n      if (!this.loaded) return;\\n      if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n      // Wait until media has a duration\\n      if (!this.player.media.duration) return;\\n      if (event.type === 'touchmove') {\\n        // Calculate seek hover position as approx video seconds\\n        this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n      } else {\\n        var _this$player$config$m, _this$player$config$m2;\\n        // Calculate seek hover position as approx video seconds\\n        const clientRect = this.player.elements.progress.getBoundingClientRect();\\n        const percentage = 100 / clientRect.width * (event.pageX - clientRect.left);\\n        this.seekTime = this.player.media.duration * (percentage / 100);\\n        if (this.seekTime < 0) {\\n          // The mousemove fires for 10+px out to the left\\n          this.seekTime = 0;\\n        }\\n        if (this.seekTime > this.player.media.duration - 1) {\\n          // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n          this.seekTime = this.player.media.duration - 1;\\n        }\\n        this.mousePosX = event.pageX;\\n\\n        // Set time text inside image container\\n        this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n        // Get marker point for time\\n        const point = (_this$player$config$m = this.player.config.markers) === null || _this$player$config$m === void 0 ? void 0 : (_this$player$config$m2 = _this$player$config$m.points) === null || _this$player$config$m2 === void 0 ? void 0 : _this$player$config$m2.find(({\\n          time: t\\n        }) => t === Math.round(this.seekTime));\\n\\n        // Append the point label to the tooltip\\n        if (point) {\\n          // this.elements.thumb.time.innerText.concat('\\\\n');\\n          this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n        }\\n      }\\n\\n      // Download and show image\\n      this.showImageAtCurrentTime();\\n    });\\n    _defineProperty$1(this, \\\"endMove\\\", () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n    _defineProperty$1(this, \\\"startScrubbing\\\", event => {\\n      // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n      if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n        this.mouseDown = true;\\n\\n        // Wait until media has a duration\\n        if (this.player.media.duration) {\\n          this.toggleScrubbingContainer(true);\\n          this.toggleThumbContainer(false, true);\\n\\n          // Download and show image\\n          this.showImageAtCurrentTime();\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"endScrubbing\\\", () => {\\n      this.mouseDown = false;\\n\\n      // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n      if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n        // The video was already seeked/loaded at the chosen time - hide immediately\\n        this.toggleScrubbingContainer(false);\\n      } else {\\n        // The video hasn't seeked yet. Wait for that\\n        once.call(this.player, this.player.media, 'timeupdate', () => {\\n          // Re-check mousedown - we might have already started scrubbing again\\n          if (!this.mouseDown) {\\n            this.toggleScrubbingContainer(false);\\n          }\\n        });\\n      }\\n    });\\n    /**\\n     * Setup hooks for Plyr and window events\\n     */\\n    _defineProperty$1(this, \\\"listeners\\\", () => {\\n      // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n      this.player.on('play', () => {\\n        this.toggleThumbContainer(false, true);\\n      });\\n      this.player.on('seeked', () => {\\n        this.toggleThumbContainer(false);\\n      });\\n      this.player.on('timeupdate', () => {\\n        this.lastTime = this.player.media.currentTime;\\n      });\\n    });\\n    /**\\n     * Create HTML elements for image containers\\n     */\\n    _defineProperty$1(this, \\\"render\\\", () => {\\n      // Create HTML element: plyr__preview-thumbnail-container\\n      this.elements.thumb.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.thumbContainer\\n      });\\n\\n      // Wrapper for the image for styling\\n      this.elements.thumb.imageContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.imageContainer\\n      });\\n      this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n      // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n      const timeContainer = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.timeContainer\\n      });\\n      this.elements.thumb.time = createElement('span', {}, '00:00');\\n      timeContainer.appendChild(this.elements.thumb.time);\\n      this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n      // Inject the whole thumb\\n      if (is.element(this.player.elements.progress)) {\\n        this.player.elements.progress.appendChild(this.elements.thumb.container);\\n      }\\n\\n      // Create HTML element: plyr__preview-scrubbing-container\\n      this.elements.scrubbing.container = createElement('div', {\\n        class: this.player.config.classNames.previewThumbnails.scrubbingContainer\\n      });\\n      this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n    });\\n    _defineProperty$1(this, \\\"destroy\\\", () => {\\n      if (this.elements.thumb.container) {\\n        this.elements.thumb.container.remove();\\n      }\\n      if (this.elements.scrubbing.container) {\\n        this.elements.scrubbing.container.remove();\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImageAtCurrentTime\\\", () => {\\n      if (this.mouseDown) {\\n        this.setScrubbingContainerSize();\\n      } else {\\n        this.setThumbContainerSizeAndPos();\\n      }\\n\\n      // Find the desired thumbnail index\\n      // TODO: Handle a video longer than the thumbs where thumbNum is null\\n      const thumbNum = this.thumbnails[0].frames.findIndex(frame => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime);\\n      const hasThumb = thumbNum >= 0;\\n      let qualityIndex = 0;\\n\\n      // Show the thumb container if we're not scrubbing\\n      if (!this.mouseDown) {\\n        this.toggleThumbContainer(hasThumb);\\n      }\\n\\n      // No matching thumb found\\n      if (!hasThumb) {\\n        return;\\n      }\\n\\n      // Check to see if we've already downloaded higher quality versions of this image\\n      this.thumbnails.forEach((thumbnail, index) => {\\n        if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n          qualityIndex = index;\\n        }\\n      });\\n\\n      // Only proceed if either thumb num or thumbfilename has changed\\n      if (thumbNum !== this.showingThumb) {\\n        this.showingThumb = thumbNum;\\n        this.loadImage(qualityIndex);\\n      }\\n    });\\n    // Show the image that's currently specified in this.showingThumb\\n    _defineProperty$1(this, \\\"loadImage\\\", (qualityIndex = 0) => {\\n      const thumbNum = this.showingThumb;\\n      const thumbnail = this.thumbnails[qualityIndex];\\n      const {\\n        urlPrefix\\n      } = thumbnail;\\n      const frame = thumbnail.frames[thumbNum];\\n      const thumbFilename = thumbnail.frames[thumbNum].text;\\n      const thumbUrl = urlPrefix + thumbFilename;\\n      if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n        // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n        // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n        if (this.loadingImage && this.usingSprites) {\\n          this.loadingImage.onload = null;\\n        }\\n\\n        // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n        // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n        // images causes a flicker. Putting a new image over the top does not\\n        const previewImage = new Image();\\n        previewImage.src = thumbUrl;\\n        previewImage.dataset.index = thumbNum;\\n        previewImage.dataset.filename = thumbFilename;\\n        this.showingThumbFilename = thumbFilename;\\n        this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n        // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n        previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n        this.loadingImage = previewImage;\\n        this.removeOldImages(previewImage);\\n      } else {\\n        // Update the existing image\\n        this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n        this.currentImageElement.dataset.index = thumbNum;\\n        this.removeOldImages(this.currentImageElement);\\n      }\\n    });\\n    _defineProperty$1(this, \\\"showImage\\\", (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n      this.player.debug.log(`Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`);\\n      this.setImageSizeAndOffset(previewImage, frame);\\n      if (newImage) {\\n        this.currentImageContainer.appendChild(previewImage);\\n        this.currentImageElement = previewImage;\\n        if (!this.loadedImages.includes(thumbFilename)) {\\n          this.loadedImages.push(thumbFilename);\\n        }\\n      }\\n\\n      // Preload images before and after the current one\\n      // Show higher quality of the same frame\\n      // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n      this.preloadNearby(thumbNum, true).then(this.preloadNearby(thumbNum, false)).then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n    });\\n    // Remove all preview images that aren't the designated current image\\n    _defineProperty$1(this, \\\"removeOldImages\\\", currentImage => {\\n      // Get a list of all images, convert it from a DOM list to an array\\n      Array.from(this.currentImageContainer.children).forEach(image => {\\n        if (image.tagName.toLowerCase() !== 'img') {\\n          return;\\n        }\\n        const removeDelay = this.usingSprites ? 500 : 1000;\\n        if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n          // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n          // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n          // eslint-disable-next-line no-param-reassign\\n          image.dataset.deleting = true;\\n\\n          // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n          const {\\n            currentImageContainer\\n          } = this;\\n          setTimeout(() => {\\n            currentImageContainer.removeChild(image);\\n            this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n          }, removeDelay);\\n        }\\n      });\\n    });\\n    // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n    // This will only preload the lowest quality\\n    _defineProperty$1(this, \\\"preloadNearby\\\", (thumbNum, forward = true) => {\\n      return new Promise(resolve => {\\n        setTimeout(() => {\\n          const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n          if (this.showingThumbFilename === oldThumbFilename) {\\n            // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n            let thumbnailsClone;\\n            if (forward) {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n            } else {\\n              thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n            }\\n            let foundOne = false;\\n            thumbnailsClone.forEach(frame => {\\n              const newThumbFilename = frame.text;\\n              if (newThumbFilename !== oldThumbFilename) {\\n                // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n                if (!this.loadedImages.includes(newThumbFilename)) {\\n                  foundOne = true;\\n                  this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n                  const {\\n                    urlPrefix\\n                  } = this.thumbnails[0];\\n                  const thumbURL = urlPrefix + newThumbFilename;\\n                  const previewImage = new Image();\\n                  previewImage.src = thumbURL;\\n                  previewImage.onload = () => {\\n                    this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                    if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                    // We don't resolve until the thumb is loaded\\n                    resolve();\\n                  };\\n                }\\n              }\\n            });\\n\\n            // If there are none to preload then we want to resolve immediately\\n            if (!foundOne) {\\n              resolve();\\n            }\\n          }\\n        }, 300);\\n      });\\n    });\\n    // If user has been hovering current image for half a second, look for a higher quality one\\n    _defineProperty$1(this, \\\"getHigherQuality\\\", (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n      if (currentQualityIndex < this.thumbnails.length - 1) {\\n        // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n        let previewImageHeight = previewImage.naturalHeight;\\n        if (this.usingSprites) {\\n          previewImageHeight = frame.h;\\n        }\\n        if (previewImageHeight < this.thumbContainerHeight) {\\n          // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n          setTimeout(() => {\\n            // Make sure the mouse hasn't already moved on and started hovering at another image\\n            if (this.showingThumbFilename === thumbFilename) {\\n              this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n              this.loadImage(currentQualityIndex + 1);\\n            }\\n          }, 300);\\n        }\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleThumbContainer\\\", (toggle = false, clearShowing = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n      this.elements.thumb.container.classList.toggle(className, toggle);\\n      if (!toggle && clearShowing) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"toggleScrubbingContainer\\\", (toggle = false) => {\\n      const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n      this.elements.scrubbing.container.classList.toggle(className, toggle);\\n      if (!toggle) {\\n        this.showingThumb = null;\\n        this.showingThumbFilename = null;\\n      }\\n    });\\n    _defineProperty$1(this, \\\"determineContainerAutoSizing\\\", () => {\\n      if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n        // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n        this.sizeSpecifiedInCSS = true;\\n      }\\n    });\\n    // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n    _defineProperty$1(this, \\\"setThumbContainerSizeAndPos\\\", () => {\\n      const {\\n        imageContainer\\n      } = this.elements.thumb;\\n      if (!this.sizeSpecifiedInCSS) {\\n        const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n        imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n        const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n        imageContainer.style.width = `${thumbWidth}px`;\\n      } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n        const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n        imageContainer.style.height = `${thumbHeight}px`;\\n      }\\n      this.setThumbContainerPos();\\n    });\\n    _defineProperty$1(this, \\\"setThumbContainerPos\\\", () => {\\n      const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n      const containerRect = this.player.elements.container.getBoundingClientRect();\\n      const {\\n        container\\n      } = this.elements.thumb;\\n      // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n      const min = containerRect.left - scrubberRect.left + 10;\\n      const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n      // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n      const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n      const clamped = clamp(position, min, max);\\n\\n      // Move the popover position\\n      container.style.left = `${clamped}px`;\\n\\n      // The arrow can follow the cursor\\n      container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n    });\\n    // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n    _defineProperty$1(this, \\\"setScrubbingContainerSize\\\", () => {\\n      const {\\n        width,\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      this.elements.scrubbing.container.style.width = `${width}px`;\\n      this.elements.scrubbing.container.style.height = `${height}px`;\\n    });\\n    // Sprites need to be offset to the correct location\\n    _defineProperty$1(this, \\\"setImageSizeAndOffset\\\", (previewImage, frame) => {\\n      if (!this.usingSprites) return;\\n\\n      // Find difference between height and preview container height\\n      const multiplier = this.thumbContainerHeight / frame.h;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.left = `-${frame.x * multiplier}px`;\\n      // eslint-disable-next-line no-param-reassign\\n      previewImage.style.top = `-${frame.y * multiplier}px`;\\n    });\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {}\\n    };\\n    this.load();\\n  }\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const {\\n        height\\n      } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n}\\n\\n// ==========================================================================\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach(attribute => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(this, () => {\\n      // Reset quality options\\n      this.options.quality = [];\\n\\n      // Remove elements\\n      removeElement(this.media);\\n      this.media = null;\\n\\n      // Reset class name\\n      if (is.element(this.elements.container)) {\\n        this.elements.container.removeAttribute('class');\\n      }\\n\\n      // Set the type and provider\\n      const {\\n        sources,\\n        type\\n      } = input;\\n      const [{\\n        provider = providers.html5,\\n        src\\n      }] = sources;\\n      const tagName = provider === 'html5' ? type : 'div';\\n      const attributes = provider === 'html5' ? {} : {\\n        src\\n      };\\n      Object.assign(this, {\\n        provider,\\n        type,\\n        // Check for support\\n        supported: support.check(type, provider, this.config.playsinline),\\n        // Create new element\\n        media: createElement(tagName, attributes)\\n      });\\n\\n      // Inject the new element\\n      this.elements.container.appendChild(this.media);\\n\\n      // Autoplay the new source?\\n      if (is.boolean(input.autoplay)) {\\n        this.config.autoplay = input.autoplay;\\n      }\\n\\n      // Set attributes for audio and video\\n      if (this.isHTML5) {\\n        if (this.config.crossorigin) {\\n          this.media.setAttribute('crossorigin', '');\\n        }\\n        if (this.config.autoplay) {\\n          this.media.setAttribute('autoplay', '');\\n        }\\n        if (!is.empty(input.poster)) {\\n          this.poster = input.poster;\\n        }\\n        if (this.config.loop.active) {\\n          this.media.setAttribute('loop', '');\\n        }\\n        if (this.config.muted) {\\n          this.media.setAttribute('muted', '');\\n        }\\n        if (this.config.playsinline) {\\n          this.media.setAttribute('playsinline', '');\\n        }\\n      }\\n\\n      // Restore class hook\\n      ui.addStyleHook.call(this);\\n\\n      // Set new sources for html5\\n      if (this.isHTML5) {\\n        source.insertElements.call(this, 'source', sources);\\n      }\\n\\n      // Set video title\\n      this.config.title = input.title;\\n\\n      // Set up from scratch\\n      media.setup.call(this);\\n\\n      // HTML5 stuff\\n      if (this.isHTML5) {\\n        // Setup captions\\n        if (Object.keys(input).includes('tracks')) {\\n          source.insertElements.call(this, 'track', input.tracks);\\n        }\\n      }\\n\\n      // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n      if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n        // Setup interface\\n        ui.build.call(this);\\n      }\\n\\n      // Load HTML5 sources\\n      if (this.isHTML5) {\\n        this.media.load();\\n      }\\n\\n      // Update previewThumbnails config & reload plugin\\n      if (!is.empty(input.previewThumbnails)) {\\n        Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n        // Cleanup previewThumbnails plugin if it was loaded\\n        if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n          this.previewThumbnails.destroy();\\n          this.previewThumbnails = null;\\n        }\\n\\n        // Create new instance if it is still enabled\\n        if (this.config.previewThumbnails.enabled) {\\n          this.previewThumbnails = new PreviewThumbnails(this);\\n        }\\n      }\\n\\n      // Update the fullscreen support\\n      this.fullscreen.update();\\n    }, true);\\n  }\\n};\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    /**\\n     * Play the media, or play the advertisement (if they are not blocked)\\n     */\\n    _defineProperty$1(this, \\\"play\\\", () => {\\n      if (!is.function(this.media.play)) {\\n        return null;\\n      }\\n\\n      // Intecept play with ads\\n      if (this.ads && this.ads.enabled) {\\n        this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n      }\\n\\n      // Return the promise (for HTML5)\\n      return this.media.play();\\n    });\\n    /**\\n     * Pause the media\\n     */\\n    _defineProperty$1(this, \\\"pause\\\", () => {\\n      if (!this.playing || !is.function(this.media.pause)) {\\n        return null;\\n      }\\n      return this.media.pause();\\n    });\\n    /**\\n     * Toggle playback based on current status\\n     * @param {Boolean} input\\n     */\\n    _defineProperty$1(this, \\\"togglePlay\\\", input => {\\n      // Toggle based on current state if nothing passed\\n      const toggle = is.boolean(input) ? input : !this.playing;\\n      if (toggle) {\\n        return this.play();\\n      }\\n      return this.pause();\\n    });\\n    /**\\n     * Stop playback\\n     */\\n    _defineProperty$1(this, \\\"stop\\\", () => {\\n      if (this.isHTML5) {\\n        this.pause();\\n        this.restart();\\n      } else if (is.function(this.media.stop)) {\\n        this.media.stop();\\n      }\\n    });\\n    /**\\n     * Restart playback\\n     */\\n    _defineProperty$1(this, \\\"restart\\\", () => {\\n      this.currentTime = 0;\\n    });\\n    /**\\n     * Rewind\\n     * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"rewind\\\", seekTime => {\\n      this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Fast forward\\n     * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n     */\\n    _defineProperty$1(this, \\\"forward\\\", seekTime => {\\n      this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n    });\\n    /**\\n     * Increase volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"increaseVolume\\\", step => {\\n      const volume = this.media.muted ? 0 : this.volume;\\n      this.volume = volume + (is.number(step) ? step : 0);\\n    });\\n    /**\\n     * Decrease volume\\n     * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n     */\\n    _defineProperty$1(this, \\\"decreaseVolume\\\", step => {\\n      this.increaseVolume(-step);\\n    });\\n    /**\\n     * Trigger the airplay dialog\\n     * TODO: update player with state, support, enabled\\n     */\\n    _defineProperty$1(this, \\\"airplay\\\", () => {\\n      // Show dialog if supported\\n      if (support.airplay) {\\n        this.media.webkitShowPlaybackTargetPicker();\\n      }\\n    });\\n    /**\\n     * Toggle the player controls\\n     * @param {Boolean} [toggle] - Whether to show the controls\\n     */\\n    _defineProperty$1(this, \\\"toggleControls\\\", toggle => {\\n      // Don't toggle if missing UI support or if it's audio\\n      if (this.supported.ui && !this.isAudio) {\\n        // Get state before change\\n        const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n        // Negate the argument if not undefined since adding the class to hides the controls\\n        const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n        // Apply and get updated state\\n        const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n        // Close menu\\n        if (hiding && is.array(this.config.controls) && this.config.controls.includes('settings') && !is.empty(this.config.settings)) {\\n          controls.toggleMenu.call(this, false);\\n        }\\n\\n        // Trigger event on change\\n        if (hiding !== isHidden) {\\n          const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n          triggerEvent.call(this, this.media, eventName);\\n        }\\n        return !hiding;\\n      }\\n      return false;\\n    });\\n    /**\\n     * Add event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"on\\\", (event, callback) => {\\n      on.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Add event listeners once\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"once\\\", (event, callback) => {\\n      once.call(this, this.elements.container, event, callback);\\n    });\\n    /**\\n     * Remove event listeners\\n     * @param {String} event - Event type\\n     * @param {Function} callback - Callback for when event occurs\\n     */\\n    _defineProperty$1(this, \\\"off\\\", (event, callback) => {\\n      off(this.elements.container, event, callback);\\n    });\\n    /**\\n     * Destroy an instance\\n     * Event listeners are removed when elements are removed\\n     * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n     * @param {Function} callback - Callback for when destroy is complete\\n     * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n     */\\n    _defineProperty$1(this, \\\"destroy\\\", (callback, soft = false) => {\\n      if (!this.ready) {\\n        return;\\n      }\\n      const done = () => {\\n        // Reset overflow (incase destroyed while in fullscreen)\\n        document.body.style.overflow = '';\\n\\n        // GC for embed\\n        this.embed = null;\\n\\n        // If it's a soft destroy, make minimal changes\\n        if (soft) {\\n          if (Object.keys(this.elements).length) {\\n            // Remove elements\\n            removeElement(this.elements.buttons.play);\\n            removeElement(this.elements.captions);\\n            removeElement(this.elements.controls);\\n            removeElement(this.elements.wrapper);\\n\\n            // Clear for GC\\n            this.elements.buttons.play = null;\\n            this.elements.captions = null;\\n            this.elements.controls = null;\\n            this.elements.wrapper = null;\\n          }\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback();\\n          }\\n        } else {\\n          // Unbind listeners\\n          unbindListeners.call(this);\\n\\n          // Cancel current network requests\\n          html5.cancelRequests.call(this);\\n\\n          // Replace the container with the original element provided\\n          replaceElement(this.elements.original, this.elements.container);\\n\\n          // Event\\n          triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n          // Callback\\n          if (is.function(callback)) {\\n            callback.call(this.elements.original);\\n          }\\n\\n          // Reset state\\n          this.ready = false;\\n\\n          // Clear for garbage collection\\n          setTimeout(() => {\\n            this.elements = null;\\n            this.media = null;\\n          }, 200);\\n        }\\n      };\\n\\n      // Stop playback\\n      this.stop();\\n\\n      // Clear timeouts\\n      clearTimeout(this.timers.loading);\\n      clearTimeout(this.timers.controls);\\n      clearTimeout(this.timers.resized);\\n\\n      // Provider specific stuff\\n      if (this.isHTML5) {\\n        // Restore native video controls\\n        ui.toggleNativeControls.call(this, true);\\n\\n        // Clean up\\n        done();\\n      } else if (this.isYouTube) {\\n        // Clear timers\\n        clearInterval(this.timers.buffering);\\n        clearInterval(this.timers.playing);\\n\\n        // Destroy YouTube API\\n        if (this.embed !== null && is.function(this.embed.destroy)) {\\n          this.embed.destroy();\\n        }\\n\\n        // Clean up\\n        done();\\n      } else if (this.isVimeo) {\\n        // Destroy Vimeo API\\n        // then clean up (wait, to prevent postmessage errors)\\n        if (this.embed !== null) {\\n          this.embed.unload().then(done);\\n        }\\n\\n        // Vimeo does not always return\\n        setTimeout(done, 200);\\n      }\\n    });\\n    /**\\n     * Check for support for a mime type (HTML5 only)\\n     * @param {String} type - Mime type\\n     */\\n    _defineProperty$1(this, \\\"supports\\\", type => support.mime.call(this, type));\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if (window.jQuery && this.media instanceof jQuery || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend({}, defaults, Plyr.defaults, options || {}, (() => {\\n      try {\\n        return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n      } catch (_) {\\n        return {};\\n      }\\n    })());\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {}\\n      }\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap()\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: []\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const _type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (_type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n        break;\\n      case 'video':\\n      case 'audio':\\n        this.type = _type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n        break;\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), event => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || this.isEmbed && !this.supported.ui) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const {\\n      buffered\\n    } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({\\n        volume\\n      } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return Boolean(this.media.mozHasAudio) || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length);\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const {\\n      minimumSpeed: min,\\n      maximumSpeed: max\\n    } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n    if (!options.length) {\\n      return;\\n    }\\n    let quality = [!is.empty(input) && Number(input), this.storage.get('quality'), config.selected, config.default].find(is.number);\\n    let updateStorage = true;\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({\\n        quality\\n      });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n         switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n             case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n             case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n             case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n             default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const {\\n      download\\n    } = this.config.urls;\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n    this.config.urls.download = input;\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n    this.config.ratio = reduceAspectRatio(input);\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const {\\n      toggled,\\n      currentTrack\\n    } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n    return targets.map(t => new Plyr(t, options));\\n  }\\n}\\nPlyr.defaults = cloneDeep(defaults);\\n\\n// ==========================================================================\\n\\nexport { Plyr as default };\\n//# sourceMappingURL=plyr.polyfilled.js.map\\n\",\"function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError(\\\"Cannot call a class as a function\\\")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,\\\"value\\\"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}function _defineProperty(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ownKeys(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function _objectSpread2(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?ownKeys(Object(n),!0).forEach((function(t){_defineProperty(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):ownKeys(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}var defaults={addCSS:!0,thumbWidth:15,watch:!0};function matches(e,t){return function(){return Array.from(document.querySelectorAll(t)).includes(this)}.call(e,t)}function trigger(e,t){if(e&&t){var n=new Event(t,{bubbles:!0});e.dispatchEvent(n)}}var getConstructor=function(e){return null!=e?e.constructor:null},instanceOf=function(e,t){return!!(e&&t&&e instanceof t)},isNullOrUndefined=function(e){return null==e},isObject=function(e){return getConstructor(e)===Object},isNumber=function(e){return getConstructor(e)===Number&&!Number.isNaN(e)},isString=function(e){return getConstructor(e)===String},isBoolean=function(e){return getConstructor(e)===Boolean},isFunction=function(e){return getConstructor(e)===Function},isArray=function(e){return Array.isArray(e)},isNodeList=function(e){return instanceOf(e,NodeList)},isElement=function(e){return instanceOf(e,Element)},isEvent=function(e){return instanceOf(e,Event)},isEmpty=function(e){return isNullOrUndefined(e)||(isString(e)||isArray(e)||isNodeList(e))&&!e.length||isObject(e)&&!Object.keys(e).length},is={nullOrUndefined:isNullOrUndefined,object:isObject,number:isNumber,string:isString,boolean:isBoolean,function:isFunction,array:isArray,nodeList:isNodeList,element:isElement,event:isEvent,empty:isEmpty};function getDecimalPlaces(e){var t=\\\"\\\".concat(e).match(/(?:\\\\.(\\\\d+))?(?:[eE]([+-]?\\\\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}function round(e,t){if(1>t){var n=getDecimalPlaces(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}var RangeTouch=function(){function e(t,n){_classCallCheck(this,e),is.element(t)?this.element=t:is.string(t)&&(this.element=document.querySelector(t)),is.element(this.element)&&is.empty(this.element.rangeTouch)&&(this.config=_objectSpread2({},defaults,{},n),this.init())}return _createClass(e,[{key:\\\"init\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"none\\\",this.element.style.webKitUserSelect=\\\"none\\\",this.element.style.touchAction=\\\"manipulation\\\"),this.listeners(!0),this.element.rangeTouch=this)}},{key:\\\"destroy\\\",value:function(){e.enabled&&(this.config.addCSS&&(this.element.style.userSelect=\\\"\\\",this.element.style.webKitUserSelect=\\\"\\\",this.element.style.touchAction=\\\"\\\"),this.listeners(!1),this.element.rangeTouch=null)}},{key:\\\"listeners\\\",value:function(e){var t=this,n=e?\\\"addEventListener\\\":\\\"removeEventListener\\\";[\\\"touchstart\\\",\\\"touchmove\\\",\\\"touchend\\\"].forEach((function(e){t.element[n](e,(function(e){return t.set(e)}),!1)}))}},{key:\\\"get\\\",value:function(t){if(!e.enabled||!is.event(t))return null;var n,r=t.target,i=t.changedTouches[0],o=parseFloat(r.getAttribute(\\\"min\\\"))||0,s=parseFloat(r.getAttribute(\\\"max\\\"))||100,u=parseFloat(r.getAttribute(\\\"step\\\"))||1,c=r.getBoundingClientRect(),a=100/c.width*(this.config.thumbWidth/2)/100;return 0>(n=100/c.width*(i.clientX-c.left))?n=0:100<n&&(n=100),50>n?n-=(100-2*n)*a:50<n&&(n+=2*(n-50)*a),o+round(n/100*(s-o),u)}},{key:\\\"set\\\",value:function(t){e.enabled&&is.event(t)&&!t.target.disabled&&(t.preventDefault(),t.target.value=this.get(t),trigger(t.target,\\\"touchend\\\"===t.type?\\\"change\\\":\\\"input\\\"))}}],[{key:\\\"setup\\\",value:function(t){var n=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},r=null;if(is.empty(t)||is.string(t)?r=Array.from(document.querySelectorAll(is.string(t)?t:'input[type=\\\"range\\\"]')):is.element(t)?r=[t]:is.nodeList(t)?r=Array.from(t):is.array(t)&&(r=t.filter(is.element)),is.empty(r))return null;var i=_objectSpread2({},defaults,{},n);if(is.string(t)&&i.watch){var o=new MutationObserver((function(n){Array.from(n).forEach((function(n){Array.from(n.addedNodes).forEach((function(n){is.element(n)&&matches(n,t)&&new e(n,i)}))}))}));o.observe(document.body,{childList:!0,subtree:!0})}return r.map((function(t){return new e(t,n)}))}},{key:\\\"enabled\\\",get:function(){return\\\"ontouchstart\\\"in document.documentElement}}]),e}();export default RangeTouch;\",\"(function(global) {\\r\\n  /**\\r\\n   * Polyfill URLSearchParams\\r\\n   *\\r\\n   * Inspired from : https://github.com/WebReflection/url-search-params/blob/master/src/url-search-params.js\\r\\n   */\\r\\n\\r\\n  var checkIfIteratorIsSupported = function() {\\r\\n    try {\\r\\n      return !!Symbol.iterator;\\r\\n    } catch (error) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var iteratorSupported = checkIfIteratorIsSupported();\\r\\n\\r\\n  var createIterator = function(items) {\\r\\n    var iterator = {\\r\\n      next: function() {\\r\\n        var value = items.shift();\\r\\n        return { done: value === void 0, value: value };\\r\\n      }\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      iterator[Symbol.iterator] = function() {\\r\\n        return iterator;\\r\\n      };\\r\\n    }\\r\\n\\r\\n    return iterator;\\r\\n  };\\r\\n\\r\\n  /**\\r\\n   * Search param name and values should be encoded according to https://url.spec.whatwg.org/#urlencoded-serializing\\r\\n   * encodeURIComponent() produces the same result except encoding spaces as `%20` instead of `+`.\\r\\n   */\\r\\n  var serializeParam = function(value) {\\r\\n    return encodeURIComponent(value).replace(/%20/g, '+');\\r\\n  };\\r\\n\\r\\n  var deserializeParam = function(value) {\\r\\n    return decodeURIComponent(String(value).replace(/\\\\+/g, ' '));\\r\\n  };\\r\\n\\r\\n  var polyfillURLSearchParams = function() {\\r\\n\\r\\n    var URLSearchParams = function(searchString) {\\r\\n      Object.defineProperty(this, '_entries', { writable: true, value: {} });\\r\\n      var typeofSearchString = typeof searchString;\\r\\n\\r\\n      if (typeofSearchString === 'undefined') {\\r\\n        // do nothing\\r\\n      } else if (typeofSearchString === 'string') {\\r\\n        if (searchString !== '') {\\r\\n          this._fromString(searchString);\\r\\n        }\\r\\n      } else if (searchString instanceof URLSearchParams) {\\r\\n        var _this = this;\\r\\n        searchString.forEach(function(value, name) {\\r\\n          _this.append(name, value);\\r\\n        });\\r\\n      } else if ((searchString !== null) && (typeofSearchString === 'object')) {\\r\\n        if (Object.prototype.toString.call(searchString) === '[object Array]') {\\r\\n          for (var i = 0; i < searchString.length; i++) {\\r\\n            var entry = searchString[i];\\r\\n            if ((Object.prototype.toString.call(entry) === '[object Array]') || (entry.length !== 2)) {\\r\\n              this.append(entry[0], entry[1]);\\r\\n            } else {\\r\\n              throw new TypeError('Expected [string, any] as entry at index ' + i + ' of URLSearchParams\\\\'s input');\\r\\n            }\\r\\n          }\\r\\n        } else {\\r\\n          for (var key in searchString) {\\r\\n            if (searchString.hasOwnProperty(key)) {\\r\\n              this.append(key, searchString[key]);\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      } else {\\r\\n        throw new TypeError('Unsupported input\\\\'s type for URLSearchParams');\\r\\n      }\\r\\n    };\\r\\n\\r\\n    var proto = URLSearchParams.prototype;\\r\\n\\r\\n    proto.append = function(name, value) {\\r\\n      if (name in this._entries) {\\r\\n        this._entries[name].push(String(value));\\r\\n      } else {\\r\\n        this._entries[name] = [String(value)];\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.delete = function(name) {\\r\\n      delete this._entries[name];\\r\\n    };\\r\\n\\r\\n    proto.get = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name][0] : null;\\r\\n    };\\r\\n\\r\\n    proto.getAll = function(name) {\\r\\n      return (name in this._entries) ? this._entries[name].slice(0) : [];\\r\\n    };\\r\\n\\r\\n    proto.has = function(name) {\\r\\n      return (name in this._entries);\\r\\n    };\\r\\n\\r\\n    proto.set = function(name, value) {\\r\\n      this._entries[name] = [String(value)];\\r\\n    };\\r\\n\\r\\n    proto.forEach = function(callback, thisArg) {\\r\\n      var entries;\\r\\n      for (var name in this._entries) {\\r\\n        if (this._entries.hasOwnProperty(name)) {\\r\\n          entries = this._entries[name];\\r\\n          for (var i = 0; i < entries.length; i++) {\\r\\n            callback.call(thisArg, entries[i], name, this);\\r\\n          }\\r\\n        }\\r\\n      }\\r\\n    };\\r\\n\\r\\n    proto.keys = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push(name);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.values = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value) {\\r\\n        items.push(value);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    proto.entries = function() {\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n      });\\r\\n      return createIterator(items);\\r\\n    };\\r\\n\\r\\n    if (iteratorSupported) {\\r\\n      proto[Symbol.iterator] = proto.entries;\\r\\n    }\\r\\n\\r\\n    proto.toString = function() {\\r\\n      var searchArray = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        searchArray.push(serializeParam(name) + '=' + serializeParam(value));\\r\\n      });\\r\\n      return searchArray.join('&');\\r\\n    };\\r\\n\\r\\n\\r\\n    global.URLSearchParams = URLSearchParams;\\r\\n  };\\r\\n\\r\\n  var checkIfURLSearchParamsSupported = function() {\\r\\n    try {\\r\\n      var URLSearchParams = global.URLSearchParams;\\r\\n\\r\\n      return (\\r\\n        (new URLSearchParams('?a=1').toString() === 'a=1') &&\\r\\n        (typeof URLSearchParams.prototype.set === 'function') &&\\r\\n        (typeof URLSearchParams.prototype.entries === 'function')\\r\\n      );\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLSearchParamsSupported()) {\\r\\n    polyfillURLSearchParams();\\r\\n  }\\r\\n\\r\\n  var proto = global.URLSearchParams.prototype;\\r\\n\\r\\n  if (typeof proto.sort !== 'function') {\\r\\n    proto.sort = function() {\\r\\n      var _this = this;\\r\\n      var items = [];\\r\\n      this.forEach(function(value, name) {\\r\\n        items.push([name, value]);\\r\\n        if (!_this._entries) {\\r\\n          _this.delete(name);\\r\\n        }\\r\\n      });\\r\\n      items.sort(function(a, b) {\\r\\n        if (a[0] < b[0]) {\\r\\n          return -1;\\r\\n        } else if (a[0] > b[0]) {\\r\\n          return +1;\\r\\n        } else {\\r\\n          return 0;\\r\\n        }\\r\\n      });\\r\\n      if (_this._entries) { // force reset because IE keeps keys index\\r\\n        _this._entries = {};\\r\\n      }\\r\\n      for (var i = 0; i < items.length; i++) {\\r\\n        this.append(items[i][0], items[i][1]);\\r\\n      }\\r\\n    };\\r\\n  }\\r\\n\\r\\n  if (typeof proto._fromString !== 'function') {\\r\\n    Object.defineProperty(proto, '_fromString', {\\r\\n      enumerable: false,\\r\\n      configurable: false,\\r\\n      writable: false,\\r\\n      value: function(searchString) {\\r\\n        if (this._entries) {\\r\\n          this._entries = {};\\r\\n        } else {\\r\\n          var keys = [];\\r\\n          this.forEach(function(value, name) {\\r\\n            keys.push(name);\\r\\n          });\\r\\n          for (var i = 0; i < keys.length; i++) {\\r\\n            this.delete(keys[i]);\\r\\n          }\\r\\n        }\\r\\n\\r\\n        searchString = searchString.replace(/^\\\\?/, '');\\r\\n        var attributes = searchString.split('&');\\r\\n        var attribute;\\r\\n        for (var i = 0; i < attributes.length; i++) {\\r\\n          attribute = attributes[i].split('=');\\r\\n          this.append(\\r\\n            deserializeParam(attribute[0]),\\r\\n            (attribute.length > 1) ? deserializeParam(attribute[1]) : ''\\r\\n          );\\r\\n        }\\r\\n      }\\r\\n    });\\r\\n  }\\r\\n\\r\\n  // HTMLAnchorElement\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\\r\\n(function(global) {\\r\\n  /**\\r\\n   * Polyfill URL\\r\\n   *\\r\\n   * Inspired from : https://github.com/arv/DOM-URL-Polyfill/blob/master/src/url.js\\r\\n   */\\r\\n\\r\\n  var checkIfURLIsSupported = function() {\\r\\n    try {\\r\\n      var u = new global.URL('b', 'http://a');\\r\\n      u.pathname = 'c d';\\r\\n      return (u.href === 'http://a/c%20d') && u.searchParams;\\r\\n    } catch (e) {\\r\\n      return false;\\r\\n    }\\r\\n  };\\r\\n\\r\\n\\r\\n  var polyfillURL = function() {\\r\\n    var _URL = global.URL;\\r\\n\\r\\n    var URL = function(url, base) {\\r\\n      if (typeof url !== 'string') url = String(url);\\r\\n      if (base && typeof base !== 'string') base = String(base);\\r\\n\\r\\n      // Only create another document if the base is different from current location.\\r\\n      var doc = document, baseElement;\\r\\n      if (base && (global.location === void 0 || base !== global.location.href)) {\\r\\n        base = base.toLowerCase();\\r\\n        doc = document.implementation.createHTMLDocument('');\\r\\n        baseElement = doc.createElement('base');\\r\\n        baseElement.href = base;\\r\\n        doc.head.appendChild(baseElement);\\r\\n        try {\\r\\n          if (baseElement.href.indexOf(base) !== 0) throw new Error(baseElement.href);\\r\\n        } catch (err) {\\r\\n          throw new Error('URL unable to set base ' + base + ' due to ' + err);\\r\\n        }\\r\\n      }\\r\\n\\r\\n      var anchorElement = doc.createElement('a');\\r\\n      anchorElement.href = url;\\r\\n      if (baseElement) {\\r\\n        doc.body.appendChild(anchorElement);\\r\\n        anchorElement.href = anchorElement.href; // force href to refresh\\r\\n      }\\r\\n\\r\\n      var inputElement = doc.createElement('input');\\r\\n      inputElement.type = 'url';\\r\\n      inputElement.value = url;\\r\\n\\r\\n      if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href) || (!inputElement.checkValidity() && !base)) {\\r\\n        throw new TypeError('Invalid URL');\\r\\n      }\\r\\n\\r\\n      Object.defineProperty(this, '_anchorElement', {\\r\\n        value: anchorElement\\r\\n      });\\r\\n\\r\\n\\r\\n      // create a linked searchParams which reflect its changes on URL\\r\\n      var searchParams = new global.URLSearchParams(this.search);\\r\\n      var enableSearchUpdate = true;\\r\\n      var enableSearchParamsUpdate = true;\\r\\n      var _this = this;\\r\\n      ['append', 'delete', 'set'].forEach(function(methodName) {\\r\\n        var method = searchParams[methodName];\\r\\n        searchParams[methodName] = function() {\\r\\n          method.apply(searchParams, arguments);\\r\\n          if (enableSearchUpdate) {\\r\\n            enableSearchParamsUpdate = false;\\r\\n            _this.search = searchParams.toString();\\r\\n            enableSearchParamsUpdate = true;\\r\\n          }\\r\\n        };\\r\\n      });\\r\\n\\r\\n      Object.defineProperty(this, 'searchParams', {\\r\\n        value: searchParams,\\r\\n        enumerable: true\\r\\n      });\\r\\n\\r\\n      var search = void 0;\\r\\n      Object.defineProperty(this, '_updateSearchParams', {\\r\\n        enumerable: false,\\r\\n        configurable: false,\\r\\n        writable: false,\\r\\n        value: function() {\\r\\n          if (this.search !== search) {\\r\\n            search = this.search;\\r\\n            if (enableSearchParamsUpdate) {\\r\\n              enableSearchUpdate = false;\\r\\n              this.searchParams._fromString(this.search);\\r\\n              enableSearchUpdate = true;\\r\\n            }\\r\\n          }\\r\\n        }\\r\\n      });\\r\\n    };\\r\\n\\r\\n    var proto = URL.prototype;\\r\\n\\r\\n    var linkURLWithAnchorAttribute = function(attributeName) {\\r\\n      Object.defineProperty(proto, attributeName, {\\r\\n        get: function() {\\r\\n          return this._anchorElement[attributeName];\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement[attributeName] = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      });\\r\\n    };\\r\\n\\r\\n    ['hash', 'host', 'hostname', 'port', 'protocol']\\r\\n      .forEach(function(attributeName) {\\r\\n        linkURLWithAnchorAttribute(attributeName);\\r\\n      });\\r\\n\\r\\n    Object.defineProperty(proto, 'search', {\\r\\n      get: function() {\\r\\n        return this._anchorElement['search'];\\r\\n      },\\r\\n      set: function(value) {\\r\\n        this._anchorElement['search'] = value;\\r\\n        this._updateSearchParams();\\r\\n      },\\r\\n      enumerable: true\\r\\n    });\\r\\n\\r\\n    Object.defineProperties(proto, {\\r\\n\\r\\n      'toString': {\\r\\n        get: function() {\\r\\n          var _this = this;\\r\\n          return function() {\\r\\n            return _this.href;\\r\\n          };\\r\\n        }\\r\\n      },\\r\\n\\r\\n      'href': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.href.replace(/\\\\?$/, '');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.href = value;\\r\\n          this._updateSearchParams();\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'pathname': {\\r\\n        get: function() {\\r\\n          return this._anchorElement.pathname.replace(/(^\\\\/?)/, '/');\\r\\n        },\\r\\n        set: function(value) {\\r\\n          this._anchorElement.pathname = value;\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'origin': {\\r\\n        get: function() {\\r\\n          // get expected port from protocol\\r\\n          var expectedPort = { 'http:': 80, 'https:': 443, 'ftp:': 21 }[this._anchorElement.protocol];\\r\\n          // add port to origin if, expected port is different than actual port\\r\\n          // and it is not empty f.e http://foo:8080\\r\\n          // 8080 != 80 && 8080 != ''\\r\\n          var addPortToOrigin = this._anchorElement.port != expectedPort &&\\r\\n            this._anchorElement.port !== '';\\r\\n\\r\\n          return this._anchorElement.protocol +\\r\\n            '//' +\\r\\n            this._anchorElement.hostname +\\r\\n            (addPortToOrigin ? (':' + this._anchorElement.port) : '');\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'password': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n\\r\\n      'username': { // TODO\\r\\n        get: function() {\\r\\n          return '';\\r\\n        },\\r\\n        set: function(value) {\\r\\n        },\\r\\n        enumerable: true\\r\\n      },\\r\\n    });\\r\\n\\r\\n    URL.createObjectURL = function(blob) {\\r\\n      return _URL.createObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    URL.revokeObjectURL = function(url) {\\r\\n      return _URL.revokeObjectURL.apply(_URL, arguments);\\r\\n    };\\r\\n\\r\\n    global.URL = URL;\\r\\n\\r\\n  };\\r\\n\\r\\n  if (!checkIfURLIsSupported()) {\\r\\n    polyfillURL();\\r\\n  }\\r\\n\\r\\n  if ((global.location !== void 0) && !('origin' in global.location)) {\\r\\n    var getOrigin = function() {\\r\\n      return global.location.protocol + '//' + global.location.hostname + (global.location.port ? (':' + global.location.port) : '');\\r\\n    };\\r\\n\\r\\n    try {\\r\\n      Object.defineProperty(global.location, 'origin', {\\r\\n        get: getOrigin,\\r\\n        enumerable: true\\r\\n      });\\r\\n    } catch (e) {\\r\\n      setInterval(function() {\\r\\n        global.location.origin = getOrigin();\\r\\n      }, 100);\\r\\n    }\\r\\n  }\\r\\n\\r\\n})(\\r\\n  (typeof global !== 'undefined') ? global\\r\\n    : ((typeof window !== 'undefined') ? window\\r\\n    : ((typeof self !== 'undefined') ? self : this))\\r\\n);\\r\\n\",\"// ==========================================================================\\n// Type checking utils\\n// ==========================================================================\\n\\nconst getConstructor = (input) => (input !== null && typeof input !== 'undefined' ? input.constructor : null);\\nconst instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor);\\nconst isNullOrUndefined = (input) => input === null || typeof input === 'undefined';\\nconst isObject = (input) => getConstructor(input) === Object;\\nconst isNumber = (input) => getConstructor(input) === Number && !Number.isNaN(input);\\nconst isString = (input) => getConstructor(input) === String;\\nconst isBoolean = (input) => getConstructor(input) === Boolean;\\nconst isFunction = (input) => typeof input === 'function';\\nconst isArray = (input) => Array.isArray(input);\\nconst isWeakMap = (input) => instanceOf(input, WeakMap);\\nconst isNodeList = (input) => instanceOf(input, NodeList);\\nconst isTextNode = (input) => getConstructor(input) === Text;\\nconst isEvent = (input) => instanceOf(input, Event);\\nconst isKeyboardEvent = (input) => instanceOf(input, KeyboardEvent);\\nconst isCue = (input) => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue);\\nconst isTrack = (input) => instanceOf(input, TextTrack) || (!isNullOrUndefined(input) && isString(input.kind));\\nconst isPromise = (input) => instanceOf(input, Promise) && isFunction(input.then);\\n\\nconst isElement = (input) =>\\n  input !== null &&\\n  typeof input === 'object' &&\\n  input.nodeType === 1 &&\\n  typeof input.style === 'object' &&\\n  typeof input.ownerDocument === 'object';\\n\\nconst isEmpty = (input) =>\\n  isNullOrUndefined(input) ||\\n  ((isString(input) || isArray(input) || isNodeList(input)) && !input.length) ||\\n  (isObject(input) && !Object.keys(input).length);\\n\\nconst isUrl = (input) => {\\n  // Accept a URL object\\n  if (instanceOf(input, window.URL)) {\\n    return true;\\n  }\\n\\n  // Must be string from here\\n  if (!isString(input)) {\\n    return false;\\n  }\\n\\n  // Add the protocol if required\\n  let string = input;\\n  if (!input.startsWith('http://') || !input.startsWith('https://')) {\\n    string = `http://${input}`;\\n  }\\n\\n  try {\\n    return !isEmpty(new URL(string).hostname);\\n  } catch (_) {\\n    return false;\\n  }\\n};\\n\\nexport default {\\n  nullOrUndefined: isNullOrUndefined,\\n  object: isObject,\\n  number: isNumber,\\n  string: isString,\\n  boolean: isBoolean,\\n  function: isFunction,\\n  array: isArray,\\n  weakMap: isWeakMap,\\n  nodeList: isNodeList,\\n  element: isElement,\\n  textNode: isTextNode,\\n  event: isEvent,\\n  keyboardEvent: isKeyboardEvent,\\n  cue: isCue,\\n  track: isTrack,\\n  promise: isPromise,\\n  url: isUrl,\\n  empty: isEmpty,\\n};\\n\",\"// ==========================================================================\\n// Animation utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\nexport const transitionEndEvent = (() => {\\n  const element = document.createElement('span');\\n\\n  const events = {\\n    WebkitTransition: 'webkitTransitionEnd',\\n    MozTransition: 'transitionend',\\n    OTransition: 'oTransitionEnd otransitionend',\\n    transition: 'transitionend',\\n  };\\n\\n  const type = Object.keys(events).find((event) => element.style[event] !== undefined);\\n\\n  return is.string(type) ? events[type] : false;\\n})();\\n\\n// Force repaint of element\\nexport function repaint(element, delay) {\\n  setTimeout(() => {\\n    try {\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = true;\\n\\n      // eslint-disable-next-line no-unused-expressions\\n      element.offsetHeight;\\n\\n      // eslint-disable-next-line no-param-reassign\\n      element.hidden = false;\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  }, delay);\\n}\\n\",\"// ==========================================================================\\n// Browser sniffing\\n// Unfortunately, due to mixed support, UA sniffing is required\\n// ==========================================================================\\n\\nconst isIE = Boolean(window.document.documentMode);\\nconst isEdge = /Edge/g.test(navigator.userAgent);\\nconst isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/g.test(navigator.userAgent);\\nconst isIPhone = /iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n// navigator.platform may be deprecated but this check is still required\\nconst isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;\\nconst isIos = /iPad|iPhone|iPod/gi.test(navigator.userAgent) && navigator.maxTouchPoints > 1;\\n\\nexport default {\\n  isIE,\\n  isEdge,\\n  isWebKit,\\n  isIPhone,\\n  isIPadOS,\\n  isIos,\\n};\\n\",\"// ==========================================================================\\n// Object utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Clone nested objects\\nexport function cloneDeep(object) {\\n  return JSON.parse(JSON.stringify(object));\\n}\\n\\n// Get a nested value in an object\\nexport function getDeep(object, path) {\\n  return path.split('.').reduce((obj, key) => obj && obj[key], object);\\n}\\n\\n// Deep extend destination object with N more objects\\nexport function extend(target = {}, ...sources) {\\n  if (!sources.length) {\\n    return target;\\n  }\\n\\n  const source = sources.shift();\\n\\n  if (!is.object(source)) {\\n    return target;\\n  }\\n\\n  Object.keys(source).forEach((key) => {\\n    if (is.object(source[key])) {\\n      if (!Object.keys(target).includes(key)) {\\n        Object.assign(target, { [key]: {} });\\n      }\\n\\n      extend(target[key], source[key]);\\n    } else {\\n      Object.assign(target, { [key]: source[key] });\\n    }\\n  });\\n\\n  return extend(target, ...sources);\\n}\\n\",\"// ==========================================================================\\n// Element utils\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { extend } from './objects';\\n\\n// Wrap an element\\nexport function wrap(elements, wrapper) {\\n  // Convert `elements` to an array, if necessary.\\n  const targets = elements.length ? elements : [elements];\\n\\n  // Loops backwards to prevent having to clone the wrapper on the\\n  // first element (see `child` below).\\n  Array.from(targets)\\n    .reverse()\\n    .forEach((element, index) => {\\n      const child = index > 0 ? wrapper.cloneNode(true) : wrapper;\\n      // Cache the current parent and sibling.\\n      const parent = element.parentNode;\\n      const sibling = element.nextSibling;\\n\\n      // Wrap the element (is automatically removed from its current\\n      // parent).\\n      child.appendChild(element);\\n\\n      // If the element had a sibling, insert the wrapper before\\n      // the sibling to maintain the HTML structure; otherwise, just\\n      // append it to the parent.\\n      if (sibling) {\\n        parent.insertBefore(child, sibling);\\n      } else {\\n        parent.appendChild(child);\\n      }\\n    });\\n}\\n\\n// Set attributes\\nexport function setAttributes(element, attributes) {\\n  if (!is.element(element) || is.empty(attributes)) return;\\n\\n  // Assume null and undefined attributes should be left out,\\n  // Setting them would otherwise convert them to \\\"null\\\" and \\\"undefined\\\"\\n  Object.entries(attributes)\\n    .filter(([, value]) => !is.nullOrUndefined(value))\\n    .forEach(([key, value]) => element.setAttribute(key, value));\\n}\\n\\n// Create a DocumentFragment\\nexport function createElement(type, attributes, text) {\\n  // Create a new <element>\\n  const element = document.createElement(type);\\n\\n  // Set all passed attributes\\n  if (is.object(attributes)) {\\n    setAttributes(element, attributes);\\n  }\\n\\n  // Add text node\\n  if (is.string(text)) {\\n    element.innerText = text;\\n  }\\n\\n  // Return built element\\n  return element;\\n}\\n\\n// Insert an element after another\\nexport function insertAfter(element, target) {\\n  if (!is.element(element) || !is.element(target)) return;\\n\\n  target.parentNode.insertBefore(element, target.nextSibling);\\n}\\n\\n// Insert a DocumentFragment\\nexport function insertElement(type, parent, attributes, text) {\\n  if (!is.element(parent)) return;\\n\\n  parent.appendChild(createElement(type, attributes, text));\\n}\\n\\n// Remove element(s)\\nexport function removeElement(element) {\\n  if (is.nodeList(element) || is.array(element)) {\\n    Array.from(element).forEach(removeElement);\\n    return;\\n  }\\n\\n  if (!is.element(element) || !is.element(element.parentNode)) {\\n    return;\\n  }\\n\\n  element.parentNode.removeChild(element);\\n}\\n\\n// Remove all child elements\\nexport function emptyElement(element) {\\n  if (!is.element(element)) return;\\n\\n  let { length } = element.childNodes;\\n\\n  while (length > 0) {\\n    element.removeChild(element.lastChild);\\n    length -= 1;\\n  }\\n}\\n\\n// Replace element\\nexport function replaceElement(newChild, oldChild) {\\n  if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null;\\n\\n  oldChild.parentNode.replaceChild(newChild, oldChild);\\n\\n  return newChild;\\n}\\n\\n// Get an attribute object from a string selector\\nexport function getAttributesFromSelector(sel, existingAttributes) {\\n  // For example:\\n  // '.test' to { class: 'test' }\\n  // '#test' to { id: 'test' }\\n  // '[data-test=\\\"test\\\"]' to { 'data-test': 'test' }\\n\\n  if (!is.string(sel) || is.empty(sel)) return {};\\n\\n  const attributes = {};\\n  const existing = extend({}, existingAttributes);\\n\\n  sel.split(',').forEach((s) => {\\n    // Remove whitespace\\n    const selector = s.trim();\\n    const className = selector.replace('.', '');\\n    const stripped = selector.replace(/[[\\\\]]/g, '');\\n    // Get the parts and value\\n    const parts = stripped.split('=');\\n    const [key] = parts;\\n    const value = parts.length > 1 ? parts[1].replace(/[\\\"']/g, '') : '';\\n    // Get the first character\\n    const start = selector.charAt(0);\\n\\n    switch (start) {\\n      case '.':\\n        // Add to existing classname\\n        if (is.string(existing.class)) {\\n          attributes.class = `${existing.class} ${className}`;\\n        } else {\\n          attributes.class = className;\\n        }\\n        break;\\n\\n      case '#':\\n        // ID selector\\n        attributes.id = selector.replace('#', '');\\n        break;\\n\\n      case '[':\\n        // Attribute selector\\n        attributes[key] = value;\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  });\\n\\n  return extend(existing, attributes);\\n}\\n\\n// Toggle hidden\\nexport function toggleHidden(element, hidden) {\\n  if (!is.element(element)) return;\\n\\n  let hide = hidden;\\n\\n  if (!is.boolean(hide)) {\\n    hide = !element.hidden;\\n  }\\n\\n  // eslint-disable-next-line no-param-reassign\\n  element.hidden = hide;\\n}\\n\\n// Mirror Element.classList.toggle, with IE compatibility for \\\"force\\\" argument\\nexport function toggleClass(element, className, force) {\\n  if (is.nodeList(element)) {\\n    return Array.from(element).map((e) => toggleClass(e, className, force));\\n  }\\n\\n  if (is.element(element)) {\\n    let method = 'toggle';\\n    if (typeof force !== 'undefined') {\\n      method = force ? 'add' : 'remove';\\n    }\\n\\n    element.classList[method](className);\\n    return element.classList.contains(className);\\n  }\\n\\n  return false;\\n}\\n\\n// Has class name\\nexport function hasClass(element, className) {\\n  return is.element(element) && element.classList.contains(className);\\n}\\n\\n// Element matches selector\\nexport function matches(element, selector) {\\n  const { prototype } = Element;\\n\\n  function match() {\\n    return Array.from(document.querySelectorAll(selector)).includes(this);\\n  }\\n\\n  const method =\\n    prototype.matches ||\\n    prototype.webkitMatchesSelector ||\\n    prototype.mozMatchesSelector ||\\n    prototype.msMatchesSelector ||\\n    match;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Closest ancestor element matching selector (also tests element itself)\\nexport function closest(element, selector) {\\n  const { prototype } = Element;\\n\\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill\\n  function closestElement() {\\n    let el = this;\\n\\n    do {\\n      if (matches.matches(el, selector)) return el;\\n      el = el.parentElement || el.parentNode;\\n    } while (el !== null && el.nodeType === 1);\\n    return null;\\n  }\\n\\n  const method = prototype.closest || closestElement;\\n\\n  return method.call(element, selector);\\n}\\n\\n// Find all elements\\nexport function getElements(selector) {\\n  return this.elements.container.querySelectorAll(selector);\\n}\\n\\n// Find a single element\\nexport function getElement(selector) {\\n  return this.elements.container.querySelector(selector);\\n}\\n\\n// Set focus and tab focus class\\nexport function setFocus(element = null, focusVisible = false) {\\n  if (!is.element(element)) return;\\n\\n  // Set regular focus\\n  element.focus({ preventScroll: true, focusVisible });\\n}\\n\",\"// ==========================================================================\\n// Plyr support checks\\n// ==========================================================================\\n\\nimport { transitionEndEvent } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { createElement } from './utils/elements';\\nimport is from './utils/is';\\n\\n// Default codecs for checking mimetype support\\nconst defaultCodecs = {\\n  'audio/ogg': 'vorbis',\\n  'audio/wav': '1',\\n  'video/webm': 'vp8, vorbis',\\n  'video/mp4': 'avc1.42E01E, mp4a.40.2',\\n  'video/ogg': 'theora',\\n};\\n\\n// Check for feature support\\nconst support = {\\n  // Basic support\\n  audio: 'canPlayType' in document.createElement('audio'),\\n  video: 'canPlayType' in document.createElement('video'),\\n\\n  // Check for support\\n  // Basic functionality vs full UI\\n  check(type, provider) {\\n    const api = support[type] || provider !== 'html5';\\n    const ui = api && support.rangeInput;\\n\\n    return {\\n      api,\\n      ui,\\n    };\\n  },\\n\\n  // Picture-in-picture support\\n  // Safari & Chrome only currently\\n  pip: (() => {\\n    // While iPhone's support picture-in-picture for some apps, seemingly Safari isn't one of them\\n    // It will throw the following error when trying to enter picture-in-picture\\n    // `NotSupportedError: The Picture-in-Picture mode is not supported.`\\n    if (browser.isIPhone) {\\n      return false;\\n    }\\n\\n    // Safari\\n    // https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls\\n    if (is.function(createElement('video').webkitSetPresentationMode)) {\\n      return true;\\n    }\\n\\n    // Chrome\\n    // https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture\\n    if (document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture) {\\n      return true;\\n    }\\n\\n    return false;\\n  })(),\\n\\n  // Airplay support\\n  // Safari only currently\\n  airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent),\\n\\n  // Inline playback support\\n  // https://webkit.org/blog/6784/new-video-policies-for-ios/\\n  playsinline: 'playsInline' in document.createElement('video'),\\n\\n  // Check for mime type support against a player instance\\n  // Credits: http://diveintohtml5.info/everything.html\\n  // Related: http://www.leanbackplayer.com/test/h5mt.html\\n  mime(input) {\\n    if (is.empty(input)) {\\n      return false;\\n    }\\n\\n    const [mediaType] = input.split('/');\\n    let type = input;\\n\\n    // Verify we're using HTML5 and there's no media type mismatch\\n    if (!this.isHTML5 || mediaType !== this.type) {\\n      return false;\\n    }\\n\\n    // Add codec if required\\n    if (Object.keys(defaultCodecs).includes(type)) {\\n      type += `; codecs=\\\"${defaultCodecs[input]}\\\"`;\\n    }\\n\\n    try {\\n      return Boolean(type && this.media.canPlayType(type).replace(/no/, ''));\\n    } catch (_) {\\n      return false;\\n    }\\n  },\\n\\n  // Check for textTracks support\\n  textTracks: 'textTracks' in document.createElement('video'),\\n\\n  // <input type=\\\"range\\\"> Sliders\\n  rangeInput: (() => {\\n    const range = document.createElement('input');\\n    range.type = 'range';\\n    return range.type === 'range';\\n  })(),\\n\\n  // Touch\\n  // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event\\n  touch: 'ontouchstart' in document.documentElement,\\n\\n  // Detect transitions support\\n  transitions: transitionEndEvent !== false,\\n\\n  // Reduced motion iOS & MacOS setting\\n  // https://webkit.org/blog/7551/responsive-design-for-motion/\\n  reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches,\\n};\\n\\nexport default support;\\n\",\"// ==========================================================================\\n// Event utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Check for passive event listener support\\n// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md\\n// https://www.youtube.com/watch?v=NPM6172J22g\\nconst supportsPassiveListeners = (() => {\\n  // Test via a getter in the options object to see if the passive property is accessed\\n  let supported = false;\\n  try {\\n    const options = Object.defineProperty({}, 'passive', {\\n      get() {\\n        supported = true;\\n        return null;\\n      },\\n    });\\n    window.addEventListener('test', null, options);\\n    window.removeEventListener('test', null, options);\\n  } catch (_) {\\n    // Do nothing\\n  }\\n\\n  return supported;\\n})();\\n\\n// Toggle event listener\\nexport function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) {\\n  // Bail if no element, event, or callback\\n  if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) {\\n    return;\\n  }\\n\\n  // Allow multiple events\\n  const events = event.split(' ');\\n  // Build options\\n  // Default to just the capture boolean for browsers with no passive listener support\\n  let options = capture;\\n\\n  // If passive events listeners are supported\\n  if (supportsPassiveListeners) {\\n    options = {\\n      // Whether the listener can be passive (i.e. default never prevented)\\n      passive,\\n      // Whether the listener is a capturing listener or not\\n      capture,\\n    };\\n  }\\n\\n  // If a single node is passed, bind the event listener\\n  events.forEach((type) => {\\n    if (this && this.eventListeners && toggle) {\\n      // Cache event listener\\n      this.eventListeners.push({ element, type, callback, options });\\n    }\\n\\n    element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options);\\n  });\\n}\\n\\n// Bind event handler\\nexport function on(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, true, passive, capture);\\n}\\n\\n// Unbind event handler\\nexport function off(element, events = '', callback, passive = true, capture = false) {\\n  toggleListener.call(this, element, events, callback, false, passive, capture);\\n}\\n\\n// Bind once-only event handler\\nexport function once(element, events = '', callback, passive = true, capture = false) {\\n  const onceCallback = (...args) => {\\n    off(element, events, onceCallback, passive, capture);\\n    callback.apply(this, args);\\n  };\\n\\n  toggleListener.call(this, element, events, onceCallback, true, passive, capture);\\n}\\n\\n// Trigger event\\nexport function triggerEvent(element, type = '', bubbles = false, detail = {}) {\\n  // Bail if no element\\n  if (!is.element(element) || is.empty(type)) {\\n    return;\\n  }\\n\\n  // Create and dispatch the event\\n  const event = new CustomEvent(type, {\\n    bubbles,\\n    detail: { ...detail, plyr: this },\\n  });\\n\\n  // Dispatch the event\\n  element.dispatchEvent(event);\\n}\\n\\n// Unbind all cached event listeners\\nexport function unbindListeners() {\\n  if (this && this.eventListeners) {\\n    this.eventListeners.forEach((item) => {\\n      const { element, type, callback, options } = item;\\n      element.removeEventListener(type, callback, options);\\n    });\\n\\n    this.eventListeners = [];\\n  }\\n}\\n\\n// Run method when / if player is ready\\nexport function ready() {\\n  return new Promise((resolve) =>\\n    this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve),\\n  ).then(() => {});\\n}\\n\",\"import is from './is';\\n/**\\n * Silence a Promise-like object.\\n * This is useful for avoiding non-harmful, but potentially confusing \\\"uncaught\\n * play promise\\\" rejection error messages.\\n * @param  {Object} value An object that may or may not be `Promise`-like.\\n */\\nexport function silencePromise(value) {\\n  if (is.promise(value)) {\\n    value.then(null, () => {});\\n  }\\n}\\n\\nexport default { silencePromise };\\n\",\"// ==========================================================================\\n// Array utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Remove duplicates in an array\\nexport function dedupe(array) {\\n  if (!is.array(array)) {\\n    return array;\\n  }\\n\\n  return array.filter((item, index) => array.indexOf(item) === index);\\n}\\n\\n// Get the closest value in an array\\nexport function closest(array, value) {\\n  if (!is.array(array) || !array.length) {\\n    return null;\\n  }\\n\\n  return array.reduce((prev, curr) => (Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev));\\n}\\n\",\"// ==========================================================================\\n// Style utils\\n// ==========================================================================\\n\\nimport { closest } from './arrays';\\nimport is from './is';\\n\\n// Check support for a CSS declaration\\nexport function supportsCSS(declaration) {\\n  if (!window || !window.CSS) {\\n    return false;\\n  }\\n\\n  return window.CSS.supports(declaration);\\n}\\n\\n// Standard/common aspect ratios\\nconst standardRatios = [\\n  [1, 1],\\n  [4, 3],\\n  [3, 4],\\n  [5, 4],\\n  [4, 5],\\n  [3, 2],\\n  [2, 3],\\n  [16, 10],\\n  [10, 16],\\n  [16, 9],\\n  [9, 16],\\n  [21, 9],\\n  [9, 21],\\n  [32, 9],\\n  [9, 32],\\n].reduce((out, [x, y]) => ({ ...out, [x / y]: [x, y] }), {});\\n\\n// Validate an aspect ratio\\nexport function validateAspectRatio(input) {\\n  if (!is.array(input) && (!is.string(input) || !input.includes(':'))) {\\n    return false;\\n  }\\n\\n  const ratio = is.array(input) ? input : input.split(':');\\n\\n  return ratio.map(Number).every(is.number);\\n}\\n\\n// Reduce an aspect ratio to it's lowest form\\nexport function reduceAspectRatio(ratio) {\\n  if (!is.array(ratio) || !ratio.every(is.number)) {\\n    return null;\\n  }\\n\\n  const [width, height] = ratio;\\n  const getDivider = (w, h) => (h === 0 ? w : getDivider(h, w % h));\\n  const divider = getDivider(width, height);\\n\\n  return [width / divider, height / divider];\\n}\\n\\n// Calculate an aspect ratio\\nexport function getAspectRatio(input) {\\n  const parse = (ratio) => (validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null);\\n  // Try provided ratio\\n  let ratio = parse(input);\\n\\n  // Get from config\\n  if (ratio === null) {\\n    ratio = parse(this.config.ratio);\\n  }\\n\\n  // Get from embed\\n  if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) {\\n    ({ ratio } = this.embed);\\n  }\\n\\n  // Get from HTML5 video\\n  if (ratio === null && this.isHTML5) {\\n    const { videoWidth, videoHeight } = this.media;\\n    ratio = [videoWidth, videoHeight];\\n  }\\n\\n  return reduceAspectRatio(ratio);\\n}\\n\\n// Set aspect ratio for responsive container\\nexport function setAspectRatio(input) {\\n  if (!this.isVideo) {\\n    return {};\\n  }\\n\\n  const { wrapper } = this.elements;\\n  const ratio = getAspectRatio.call(this, input);\\n\\n  if (!is.array(ratio)) {\\n    return {};\\n  }\\n\\n  const [x, y] = reduceAspectRatio(ratio);\\n  const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`);\\n  const padding = (100 / x) * y;\\n\\n  if (useNative) {\\n    wrapper.style.aspectRatio = `${x}/${y}`;\\n  } else {\\n    wrapper.style.paddingBottom = `${padding}%`;\\n  }\\n\\n  // For Vimeo we have an extra <div> to hide the standard controls and UI\\n  if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) {\\n    const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);\\n    const offset = (height - padding) / (height / 50);\\n\\n    if (this.fullscreen.active) {\\n      wrapper.style.paddingBottom = null;\\n    } else {\\n      this.media.style.transform = `translateY(-${offset}%)`;\\n    }\\n  } else if (this.isHTML5) {\\n    wrapper.classList.add(this.config.classNames.videoFixedRatio);\\n  }\\n\\n  return { padding, ratio };\\n}\\n\\n// Round an aspect ratio to closest standard ratio\\nexport function roundAspectRatio(x, y, tolerance = 0.05) {\\n  const ratio = x / y;\\n  const closestRatio = closest(Object.keys(standardRatios), ratio);\\n\\n  // Check match is within tolerance\\n  if (Math.abs(closestRatio - ratio) <= tolerance) {\\n    return standardRatios[closestRatio];\\n  }\\n\\n  // No match\\n  return [x, y];\\n}\\n\\n// Get the size of the viewport\\n// https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions\\nexport function getViewportSize() {\\n  const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);\\n  const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);\\n  return [width, height];\\n}\\n\",\"// ==========================================================================\\n// Plyr HTML5 helpers\\n// ==========================================================================\\n\\nimport support from './support';\\nimport { removeElement } from './utils/elements';\\nimport { triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { setAspectRatio } from './utils/style';\\n\\nconst html5 = {\\n  getSources() {\\n    if (!this.isHTML5) {\\n      return [];\\n    }\\n\\n    const sources = Array.from(this.media.querySelectorAll('source'));\\n\\n    // Filter out unsupported sources (if type is specified)\\n    return sources.filter((source) => {\\n      const type = source.getAttribute('type');\\n\\n      if (is.empty(type)) {\\n        return true;\\n      }\\n\\n      return support.mime.call(this, type);\\n    });\\n  },\\n\\n  // Get quality levels\\n  getQualityOptions() {\\n    // Whether we're forcing all options (e.g. for streaming)\\n    if (this.config.quality.forced) {\\n      return this.config.quality.options;\\n    }\\n\\n    // Get sizes from <source> elements\\n    return html5.getSources\\n      .call(this)\\n      .map((source) => Number(source.getAttribute('size')))\\n      .filter(Boolean);\\n  },\\n\\n  setup() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    const player = this;\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set aspect ratio if fixed\\n    if (!is.empty(this.config.ratio)) {\\n      setAspectRatio.call(player);\\n    }\\n\\n    // Quality\\n    Object.defineProperty(player.media, 'quality', {\\n      get() {\\n        // Get sources\\n        const sources = html5.getSources.call(player);\\n        const source = sources.find((s) => s.getAttribute('src') === player.source);\\n\\n        // Return size, if match is found\\n        return source && Number(source.getAttribute('size'));\\n      },\\n      set(input) {\\n        if (player.quality === input) {\\n          return;\\n        }\\n\\n        // If we're using an external handler...\\n        if (player.config.quality.forced && is.function(player.config.quality.onChange)) {\\n          player.config.quality.onChange(input);\\n        } else {\\n          // Get sources\\n          const sources = html5.getSources.call(player);\\n          // Get first match for requested size\\n          const source = sources.find((s) => Number(s.getAttribute('size')) === input);\\n\\n          // No matching source found\\n          if (!source) {\\n            return;\\n          }\\n\\n          // Get current state\\n          const { currentTime, paused, preload, readyState, playbackRate } = player.media;\\n\\n          // Set new source\\n          player.media.src = source.getAttribute('src');\\n\\n          // Prevent loading if preload=\\\"none\\\" and the current source isn't loaded (#1044)\\n          if (preload !== 'none' || readyState) {\\n            // Restore time\\n            player.once('loadedmetadata', () => {\\n              player.speed = playbackRate;\\n              player.currentTime = currentTime;\\n\\n              // Resume playing\\n              if (!paused) {\\n                silencePromise(player.play());\\n              }\\n            });\\n\\n            // Load new source\\n            player.media.load();\\n          }\\n        }\\n\\n        // Trigger change event\\n        triggerEvent.call(player, player.media, 'qualitychange', false, {\\n          quality: input,\\n        });\\n      },\\n    });\\n  },\\n\\n  // Cancel current network requests\\n  // See https://github.com/sampotts/plyr/issues/174\\n  cancelRequests() {\\n    if (!this.isHTML5) {\\n      return;\\n    }\\n\\n    // Remove child sources\\n    removeElement(html5.getSources.call(this));\\n\\n    // Set blank video src attribute\\n    // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error\\n    // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection\\n    this.media.setAttribute('src', this.config.blankVideo);\\n\\n    // Load the new empty source\\n    // This will cancel existing requests\\n    // See https://github.com/sampotts/plyr/issues/174\\n    this.media.load();\\n\\n    // Debugging\\n    this.debug.log('Cancelled network requests');\\n  },\\n};\\n\\nexport default html5;\\n\",\"// ==========================================================================\\n// String utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Generate a random ID\\nexport function generateId(prefix) {\\n  return `${prefix}-${Math.floor(Math.random() * 10000)}`;\\n}\\n\\n// Format string\\nexport function format(input, ...args) {\\n  if (is.empty(input)) return input;\\n\\n  return input.toString().replace(/{(\\\\d+)}/g, (_, i) => args[i].toString());\\n}\\n\\n// Get percentage\\nexport function getPercentage(current, max) {\\n  if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {\\n    return 0;\\n  }\\n\\n  return ((current / max) * 100).toFixed(2);\\n}\\n\\n// Replace all occurrences of a string in a string\\nexport const replaceAll = (input = '', find = '', replace = '') =>\\n  input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\\\\]/\\\\\\\\])/g, '\\\\\\\\$1'), 'g'), replace.toString());\\n\\n// Convert to title case\\nexport const toTitleCase = (input = '') =>\\n  input.toString().replace(/\\\\w\\\\S*/g, (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase());\\n\\n// Convert string to pascalCase\\nexport function toPascalCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert kebab case\\n  string = replaceAll(string, '-', ' ');\\n\\n  // Convert snake case\\n  string = replaceAll(string, '_', ' ');\\n\\n  // Convert to title case\\n  string = toTitleCase(string);\\n\\n  // Convert to pascal case\\n  return replaceAll(string, ' ', '');\\n}\\n\\n// Convert string to pascalCase\\nexport function toCamelCase(input = '') {\\n  let string = input.toString();\\n\\n  // Convert to pascal case\\n  string = toPascalCase(string);\\n\\n  // Convert first character to lowercase\\n  return string.charAt(0).toLowerCase() + string.slice(1);\\n}\\n\\n// Remove HTML from a string\\nexport function stripHTML(source) {\\n  const fragment = document.createDocumentFragment();\\n  const element = document.createElement('div');\\n  fragment.appendChild(element);\\n  element.innerHTML = source;\\n  return fragment.firstChild.innerText;\\n}\\n\\n// Like outerHTML, but also works for DocumentFragment\\nexport function getHTML(element) {\\n  const wrapper = document.createElement('div');\\n  wrapper.appendChild(element);\\n  return wrapper.innerHTML;\\n}\\n\",\"// ==========================================================================\\n// Plyr internationalization\\n// ==========================================================================\\n\\nimport is from './is';\\nimport { getDeep } from './objects';\\nimport { replaceAll } from './strings';\\n\\n// Skip i18n for abbreviations and brand names\\nconst resources = {\\n  pip: 'PIP',\\n  airplay: 'AirPlay',\\n  html5: 'HTML5',\\n  vimeo: 'Vimeo',\\n  youtube: 'YouTube',\\n};\\n\\nconst i18n = {\\n  get(key = '', config = {}) {\\n    if (is.empty(key) || is.empty(config)) {\\n      return '';\\n    }\\n\\n    let string = getDeep(config.i18n, key);\\n\\n    if (is.empty(string)) {\\n      if (Object.keys(resources).includes(key)) {\\n        return resources[key];\\n      }\\n\\n      return '';\\n    }\\n\\n    const replace = {\\n      '{seektime}': config.seekTime,\\n      '{title}': config.title,\\n    };\\n\\n    Object.entries(replace).forEach(([k, v]) => {\\n      string = replaceAll(string, k, v);\\n    });\\n\\n    return string;\\n  },\\n};\\n\\nexport default i18n;\\n\",\"// ==========================================================================\\n// Plyr storage\\n// ==========================================================================\\n\\nimport is from './utils/is';\\nimport { extend } from './utils/objects';\\n\\nclass Storage {\\n  constructor(player) {\\n    this.enabled = player.config.storage.enabled;\\n    this.key = player.config.storage.key;\\n  }\\n\\n  // Check for actual support (see if we can use it)\\n  static get supported() {\\n    try {\\n      if (!('localStorage' in window)) {\\n        return false;\\n      }\\n\\n      const test = '___test';\\n\\n      // Try to use it (it might be disabled, e.g. user is in private mode)\\n      // see: https://github.com/sampotts/plyr/issues/131\\n      window.localStorage.setItem(test, test);\\n      window.localStorage.removeItem(test);\\n\\n      return true;\\n    } catch (_) {\\n      return false;\\n    }\\n  }\\n\\n  get = (key) => {\\n    if (!Storage.supported || !this.enabled) {\\n      return null;\\n    }\\n\\n    const store = window.localStorage.getItem(this.key);\\n\\n    if (is.empty(store)) {\\n      return null;\\n    }\\n\\n    const json = JSON.parse(store);\\n\\n    return is.string(key) && key.length ? json[key] : json;\\n  };\\n\\n  set = (object) => {\\n    // Bail if we don't have localStorage support or it's disabled\\n    if (!Storage.supported || !this.enabled) {\\n      return;\\n    }\\n\\n    // Can only store objectst\\n    if (!is.object(object)) {\\n      return;\\n    }\\n\\n    // Get current storage\\n    let storage = this.get();\\n\\n    // Default to empty object\\n    if (is.empty(storage)) {\\n      storage = {};\\n    }\\n\\n    // Update the working copy of the values\\n    extend(storage, object);\\n\\n    // Update storage\\n    try {\\n      window.localStorage.setItem(this.key, JSON.stringify(storage));\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  };\\n}\\n\\nexport default Storage;\\n\",\"// ==========================================================================\\n// Fetch wrapper\\n// Using XHR to avoid issues with older browsers\\n// ==========================================================================\\n\\nexport default function fetch(url, responseType = 'text') {\\n  return new Promise((resolve, reject) => {\\n    try {\\n      const request = new XMLHttpRequest();\\n\\n      // Check for CORS support\\n      if (!('withCredentials' in request)) {\\n        return;\\n      }\\n\\n      request.addEventListener('load', () => {\\n        if (responseType === 'text') {\\n          try {\\n            resolve(JSON.parse(request.responseText));\\n          } catch (_) {\\n            resolve(request.responseText);\\n          }\\n        } else {\\n          resolve(request.response);\\n        }\\n      });\\n\\n      request.addEventListener('error', () => {\\n        throw new Error(request.status);\\n      });\\n\\n      request.open('GET', url, true);\\n\\n      // Set the required response type\\n      request.responseType = responseType;\\n\\n      request.send();\\n    } catch (error) {\\n      reject(error);\\n    }\\n  });\\n}\\n\",\"// ==========================================================================\\n// Sprite loader\\n// ==========================================================================\\n\\nimport Storage from '../storage';\\nimport fetch from './fetch';\\nimport is from './is';\\n\\n// Load an external SVG sprite\\nexport default function loadSprite(url, id) {\\n  if (!is.string(url)) {\\n    return;\\n  }\\n\\n  const prefix = 'cache';\\n  const hasId = is.string(id);\\n  let isCached = false;\\n  const exists = () => document.getElementById(id) !== null;\\n\\n  const update = (container, data) => {\\n    // eslint-disable-next-line no-param-reassign\\n    container.innerHTML = data;\\n\\n    // Check again incase of race condition\\n    if (hasId && exists()) {\\n      return;\\n    }\\n\\n    // Inject the SVG to the body\\n    document.body.insertAdjacentElement('afterbegin', container);\\n  };\\n\\n  // Only load once if ID set\\n  if (!hasId || !exists()) {\\n    const useStorage = Storage.supported;\\n    // Create container\\n    const container = document.createElement('div');\\n    container.setAttribute('hidden', '');\\n\\n    if (hasId) {\\n      container.setAttribute('id', id);\\n    }\\n\\n    // Check in cache\\n    if (useStorage) {\\n      const cached = window.localStorage.getItem(`${prefix}-${id}`);\\n      isCached = cached !== null;\\n\\n      if (isCached) {\\n        const data = JSON.parse(cached);\\n        update(container, data.content);\\n      }\\n    }\\n\\n    // Get the sprite\\n    fetch(url)\\n      .then((result) => {\\n        if (is.empty(result)) {\\n          return;\\n        }\\n\\n        if (useStorage) {\\n          try {\\n            window.localStorage.setItem(\\n              `${prefix}-${id}`,\\n              JSON.stringify({\\n                content: result,\\n              }),\\n            );\\n          } catch (_) {\\n            // Do nothing\\n          }\\n        }\\n\\n        update(container, result);\\n      })\\n      .catch(() => {});\\n  }\\n}\\n\",\"// ==========================================================================\\n// Time utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n// Time helpers\\nexport const getHours = (value) => Math.trunc((value / 60 / 60) % 60, 10);\\nexport const getMinutes = (value) => Math.trunc((value / 60) % 60, 10);\\nexport const getSeconds = (value) => Math.trunc(value % 60, 10);\\n\\n// Format time to UI friendly string\\nexport function formatTime(time = 0, displayHours = false, inverted = false) {\\n  // Bail if the value isn't a number\\n  if (!is.number(time)) {\\n    return formatTime(undefined, displayHours, inverted);\\n  }\\n\\n  // Format time component to add leading zero\\n  const format = (value) => `0${value}`.slice(-2);\\n  // Breakdown to hours, mins, secs\\n  let hours = getHours(time);\\n  const mins = getMinutes(time);\\n  const secs = getSeconds(time);\\n\\n  // Do we need to display hours?\\n  if (displayHours || hours > 0) {\\n    hours = `${hours}:`;\\n  } else {\\n    hours = '';\\n  }\\n\\n  // Render\\n  return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;\\n}\\n\",\"// ==========================================================================\\n// Plyr controls\\n// TODO: This needs to be split into smaller files and cleaned up\\n// ==========================================================================\\n\\nimport RangeTouch from 'rangetouch';\\n\\nimport captions from './captions';\\nimport html5 from './html5';\\nimport support from './support';\\nimport { repaint, transitionEndEvent } from './utils/animation';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  getElement,\\n  getElements,\\n  hasClass,\\n  matches,\\n  removeElement,\\n  setAttributes,\\n  setFocus,\\n  toggleClass,\\n  toggleHidden,\\n} from './utils/elements';\\nimport { off, on } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { extend } from './utils/objects';\\nimport { getPercentage, replaceAll, toCamelCase, toTitleCase } from './utils/strings';\\nimport { formatTime, getHours } from './utils/time';\\n\\n// TODO: Don't export a massive object - break down and create class\\nconst controls = {\\n  // Get icon URL\\n  getIconUrl() {\\n    const url = new URL(this.config.iconUrl, window.location);\\n    const host = window.location.host ? window.location.host : window.top.location.host;\\n    const cors = url.host !== host || (browser.isIE && !window.svg4everybody);\\n\\n    return {\\n      url: this.config.iconUrl,\\n      cors,\\n    };\\n  },\\n\\n  // Find the UI controls\\n  findElements() {\\n    try {\\n      this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper);\\n\\n      // Buttons\\n      this.elements.buttons = {\\n        play: getElements.call(this, this.config.selectors.buttons.play),\\n        pause: getElement.call(this, this.config.selectors.buttons.pause),\\n        restart: getElement.call(this, this.config.selectors.buttons.restart),\\n        rewind: getElement.call(this, this.config.selectors.buttons.rewind),\\n        fastForward: getElement.call(this, this.config.selectors.buttons.fastForward),\\n        mute: getElement.call(this, this.config.selectors.buttons.mute),\\n        pip: getElement.call(this, this.config.selectors.buttons.pip),\\n        airplay: getElement.call(this, this.config.selectors.buttons.airplay),\\n        settings: getElement.call(this, this.config.selectors.buttons.settings),\\n        captions: getElement.call(this, this.config.selectors.buttons.captions),\\n        fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen),\\n      };\\n\\n      // Progress\\n      this.elements.progress = getElement.call(this, this.config.selectors.progress);\\n\\n      // Inputs\\n      this.elements.inputs = {\\n        seek: getElement.call(this, this.config.selectors.inputs.seek),\\n        volume: getElement.call(this, this.config.selectors.inputs.volume),\\n      };\\n\\n      // Display\\n      this.elements.display = {\\n        buffer: getElement.call(this, this.config.selectors.display.buffer),\\n        currentTime: getElement.call(this, this.config.selectors.display.currentTime),\\n        duration: getElement.call(this, this.config.selectors.display.duration),\\n      };\\n\\n      // Seek tooltip\\n      if (is.element(this.elements.progress)) {\\n        this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`);\\n      }\\n\\n      return true;\\n    } catch (error) {\\n      // Log it\\n      this.debug.warn('It looks like there is a problem with your custom controls HTML', error);\\n\\n      // Restore native video controls\\n      this.toggleNativeControls(true);\\n\\n      return false;\\n    }\\n  },\\n\\n  // Create <svg> icon\\n  createIcon(type, attributes) {\\n    const namespace = 'http://www.w3.org/2000/svg';\\n    const iconUrl = controls.getIconUrl.call(this);\\n    const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`;\\n    // Create <svg>\\n    const icon = document.createElementNS(namespace, 'svg');\\n    setAttributes(\\n      icon,\\n      extend(attributes, {\\n        'aria-hidden': 'true',\\n        focusable: 'false',\\n      }),\\n    );\\n\\n    // Create the <use> to reference sprite\\n    const use = document.createElementNS(namespace, 'use');\\n    const path = `${iconPath}-${type}`;\\n\\n    // Set `href` attributes\\n    // https://github.com/sampotts/plyr/issues/460\\n    // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href\\n    if ('href' in use) {\\n      use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path);\\n    }\\n\\n    // Always set the older attribute even though it's \\\"deprecated\\\" (it'll be around for ages)\\n    use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path);\\n\\n    // Add <use> to <svg>\\n    icon.appendChild(use);\\n\\n    return icon;\\n  },\\n\\n  // Create hidden text label\\n  createLabel(key, attr = {}) {\\n    const text = i18n.get(key, this.config);\\n    const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') };\\n\\n    return createElement('span', attributes, text);\\n  },\\n\\n  // Create a badge\\n  createBadge(text) {\\n    if (is.empty(text)) {\\n      return null;\\n    }\\n\\n    const badge = createElement('span', {\\n      class: this.config.classNames.menu.value,\\n    });\\n\\n    badge.appendChild(\\n      createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.menu.badge,\\n        },\\n        text,\\n      ),\\n    );\\n\\n    return badge;\\n  },\\n\\n  // Create a <button>\\n  createButton(buttonType, attr) {\\n    const attributes = extend({}, attr);\\n    let type = toCamelCase(buttonType);\\n\\n    const props = {\\n      element: 'button',\\n      toggle: false,\\n      label: null,\\n      icon: null,\\n      labelPressed: null,\\n      iconPressed: null,\\n    };\\n\\n    ['element', 'icon', 'label'].forEach((key) => {\\n      if (Object.keys(attributes).includes(key)) {\\n        props[key] = attributes[key];\\n        delete attributes[key];\\n      }\\n    });\\n\\n    // Default to 'button' type to prevent form submission\\n    if (props.element === 'button' && !Object.keys(attributes).includes('type')) {\\n      attributes.type = 'button';\\n    }\\n\\n    // Set class name\\n    if (Object.keys(attributes).includes('class')) {\\n      if (!attributes.class.split(' ').some((c) => c === this.config.classNames.control)) {\\n        extend(attributes, {\\n          class: `${attributes.class} ${this.config.classNames.control}`,\\n        });\\n      }\\n    } else {\\n      attributes.class = this.config.classNames.control;\\n    }\\n\\n    // Large play button\\n    switch (buttonType) {\\n      case 'play':\\n        props.toggle = true;\\n        props.label = 'play';\\n        props.labelPressed = 'pause';\\n        props.icon = 'play';\\n        props.iconPressed = 'pause';\\n        break;\\n\\n      case 'mute':\\n        props.toggle = true;\\n        props.label = 'mute';\\n        props.labelPressed = 'unmute';\\n        props.icon = 'volume';\\n        props.iconPressed = 'muted';\\n        break;\\n\\n      case 'captions':\\n        props.toggle = true;\\n        props.label = 'enableCaptions';\\n        props.labelPressed = 'disableCaptions';\\n        props.icon = 'captions-off';\\n        props.iconPressed = 'captions-on';\\n        break;\\n\\n      case 'fullscreen':\\n        props.toggle = true;\\n        props.label = 'enterFullscreen';\\n        props.labelPressed = 'exitFullscreen';\\n        props.icon = 'enter-fullscreen';\\n        props.iconPressed = 'exit-fullscreen';\\n        break;\\n\\n      case 'play-large':\\n        attributes.class += ` ${this.config.classNames.control}--overlaid`;\\n        type = 'play';\\n        props.label = 'play';\\n        props.icon = 'play';\\n        break;\\n\\n      default:\\n        if (is.empty(props.label)) {\\n          props.label = type;\\n        }\\n        if (is.empty(props.icon)) {\\n          props.icon = buttonType;\\n        }\\n    }\\n\\n    const button = createElement(props.element);\\n\\n    // Setup toggle icon and labels\\n    if (props.toggle) {\\n      // Icon\\n      button.appendChild(\\n        controls.createIcon.call(this, props.iconPressed, {\\n          class: 'icon--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createIcon.call(this, props.icon, {\\n          class: 'icon--not-pressed',\\n        }),\\n      );\\n\\n      // Label/Tooltip\\n      button.appendChild(\\n        controls.createLabel.call(this, props.labelPressed, {\\n          class: 'label--pressed',\\n        }),\\n      );\\n      button.appendChild(\\n        controls.createLabel.call(this, props.label, {\\n          class: 'label--not-pressed',\\n        }),\\n      );\\n    } else {\\n      button.appendChild(controls.createIcon.call(this, props.icon));\\n      button.appendChild(controls.createLabel.call(this, props.label));\\n    }\\n\\n    // Merge and set attributes\\n    extend(attributes, getAttributesFromSelector(this.config.selectors.buttons[type], attributes));\\n    setAttributes(button, attributes);\\n\\n    // We have multiple play buttons\\n    if (type === 'play') {\\n      if (!is.array(this.elements.buttons[type])) {\\n        this.elements.buttons[type] = [];\\n      }\\n\\n      this.elements.buttons[type].push(button);\\n    } else {\\n      this.elements.buttons[type] = button;\\n    }\\n\\n    return button;\\n  },\\n\\n  // Create an <input type='range'>\\n  createRange(type, attributes) {\\n    // Seek input\\n    const input = createElement(\\n      'input',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.inputs[type]),\\n        {\\n          type: 'range',\\n          min: 0,\\n          max: 100,\\n          step: 0.01,\\n          value: 0,\\n          autocomplete: 'off',\\n          // A11y fixes for https://github.com/sampotts/plyr/issues/905\\n          role: 'slider',\\n          'aria-label': i18n.get(type, this.config),\\n          'aria-valuemin': 0,\\n          'aria-valuemax': 100,\\n          'aria-valuenow': 0,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    this.elements.inputs[type] = input;\\n\\n    // Set the fill for webkit now\\n    controls.updateRangeFill.call(this, input);\\n\\n    // Improve support on touch devices\\n    RangeTouch.setup(input);\\n\\n    return input;\\n  },\\n\\n  // Create a <progress>\\n  createProgress(type, attributes) {\\n    const progress = createElement(\\n      'progress',\\n      extend(\\n        getAttributesFromSelector(this.config.selectors.display[type]),\\n        {\\n          min: 0,\\n          max: 100,\\n          value: 0,\\n          role: 'progressbar',\\n          'aria-hidden': true,\\n        },\\n        attributes,\\n      ),\\n    );\\n\\n    // Create the label inside\\n    if (type !== 'volume') {\\n      progress.appendChild(createElement('span', null, '0'));\\n\\n      const suffixKey = {\\n        played: 'played',\\n        buffer: 'buffered',\\n      }[type];\\n      const suffix = suffixKey ? i18n.get(suffixKey, this.config) : '';\\n\\n      progress.innerText = `% ${suffix.toLowerCase()}`;\\n    }\\n\\n    this.elements.display[type] = progress;\\n\\n    return progress;\\n  },\\n\\n  // Create time display\\n  createTime(type, attrs) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.display[type], attrs);\\n\\n    const container = createElement(\\n      'div',\\n      extend(attributes, {\\n        class: `${attributes.class ? attributes.class : ''} ${this.config.classNames.display.time} `.trim(),\\n        'aria-label': i18n.get(type, this.config),\\n        role: 'timer',\\n      }),\\n      '00:00',\\n    );\\n\\n    // Reference for updates\\n    this.elements.display[type] = container;\\n\\n    return container;\\n  },\\n\\n  // Bind keyboard shortcuts for a menu item\\n  // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n  // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n  bindMenuItemShortcuts(menuItem, type) {\\n    // Navigate through menus via arrow keys and space\\n    on.call(\\n      this,\\n      menuItem,\\n      'keydown keyup',\\n      (event) => {\\n        // We only care about space and ⬆️ ⬇️️ ➡️\\n        if (![' ', 'ArrowUp', 'ArrowDown', 'ArrowRight'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Prevent play / seek\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        // We're just here to prevent the keydown bubbling\\n        if (event.type === 'keydown') {\\n          return;\\n        }\\n\\n        const isRadioButton = matches(menuItem, '[role=\\\"menuitemradio\\\"]');\\n\\n        // Show the respective menu\\n        if (!isRadioButton && [' ', 'ArrowRight'].includes(event.key)) {\\n          controls.showMenuPanel.call(this, type, true);\\n        } else {\\n          let target;\\n\\n          if (event.key !== ' ') {\\n            if (event.key === 'ArrowDown' || (isRadioButton && event.key === 'ArrowRight')) {\\n              target = menuItem.nextElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.firstElementChild;\\n              }\\n            } else {\\n              target = menuItem.previousElementSibling;\\n\\n              if (!is.element(target)) {\\n                target = menuItem.parentNode.lastElementChild;\\n              }\\n            }\\n\\n            setFocus.call(this, target, true);\\n          }\\n        }\\n      },\\n      false,\\n    );\\n\\n    // Enter will fire a `click` event but we still need to manage focus\\n    // So we bind to keyup which fires after and set focus here\\n    on.call(this, menuItem, 'keyup', (event) => {\\n      if (event.key !== 'Return') return;\\n\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    });\\n  },\\n\\n  // Create a settings menu item\\n  createMenuItem({ value, list, type, title, badge = null, checked = false }) {\\n    const attributes = getAttributesFromSelector(this.config.selectors.inputs[type]);\\n\\n    const menuItem = createElement(\\n      'button',\\n      extend(attributes, {\\n        type: 'button',\\n        role: 'menuitemradio',\\n        class: `${this.config.classNames.control} ${attributes.class ? attributes.class : ''}`.trim(),\\n        'aria-checked': checked,\\n        value,\\n      }),\\n    );\\n\\n    const flex = createElement('span');\\n\\n    // We have to set as HTML incase of special characters\\n    flex.innerHTML = title;\\n\\n    if (is.element(badge)) {\\n      flex.appendChild(badge);\\n    }\\n\\n    menuItem.appendChild(flex);\\n\\n    // Replicate radio button behavior\\n    Object.defineProperty(menuItem, 'checked', {\\n      enumerable: true,\\n      get() {\\n        return menuItem.getAttribute('aria-checked') === 'true';\\n      },\\n      set(check) {\\n        // Ensure exclusivity\\n        if (check) {\\n          Array.from(menuItem.parentNode.children)\\n            .filter((node) => matches(node, '[role=\\\"menuitemradio\\\"]'))\\n            .forEach((node) => node.setAttribute('aria-checked', 'false'));\\n        }\\n\\n        menuItem.setAttribute('aria-checked', check ? 'true' : 'false');\\n      },\\n    });\\n\\n    this.listeners.bind(\\n      menuItem,\\n      'click keyup',\\n      (event) => {\\n        if (is.keyboardEvent(event) && event.key !== ' ') {\\n          return;\\n        }\\n\\n        event.preventDefault();\\n        event.stopPropagation();\\n\\n        menuItem.checked = true;\\n\\n        switch (type) {\\n          case 'language':\\n            this.currentTrack = Number(value);\\n            break;\\n\\n          case 'quality':\\n            this.quality = value;\\n            break;\\n\\n          case 'speed':\\n            this.speed = parseFloat(value);\\n            break;\\n\\n          default:\\n            break;\\n        }\\n\\n        controls.showMenuPanel.call(this, 'home', is.keyboardEvent(event));\\n      },\\n      type,\\n      false,\\n    );\\n\\n    controls.bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n    list.appendChild(menuItem);\\n  },\\n\\n  // Format a time for display\\n  formatTime(time = 0, inverted = false) {\\n    // Bail if the value isn't a number\\n    if (!is.number(time)) {\\n      return time;\\n    }\\n\\n    // Always display hours if duration is over an hour\\n    const forceHours = getHours(this.duration) > 0;\\n\\n    return formatTime(time, forceHours, inverted);\\n  },\\n\\n  // Update the displayed time\\n  updateTimeDisplay(target = null, time = 0, inverted = false) {\\n    // Bail if there's no element to display or the value isn't a number\\n    if (!is.element(target) || !is.number(time)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line no-param-reassign\\n    target.innerText = controls.formatTime(time, inverted);\\n  },\\n\\n  // Update volume UI and storage\\n  updateVolume() {\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Update range\\n    if (is.element(this.elements.inputs.volume)) {\\n      controls.setRange.call(this, this.elements.inputs.volume, this.muted ? 0 : this.volume);\\n    }\\n\\n    // Update mute state\\n    if (is.element(this.elements.buttons.mute)) {\\n      this.elements.buttons.mute.pressed = this.muted || this.volume === 0;\\n    }\\n  },\\n\\n  // Update seek value and lower fill\\n  setRange(target, value = 0) {\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // eslint-disable-next-line\\n    target.value = value;\\n\\n    // Webkit range fill\\n    controls.updateRangeFill.call(this, target);\\n  },\\n\\n  // Update <progress> elements\\n  updateProgress(event) {\\n    if (!this.supported.ui || !is.event(event)) {\\n      return;\\n    }\\n\\n    let value = 0;\\n\\n    const setProgress = (target, input) => {\\n      const val = is.number(input) ? input : 0;\\n      const progress = is.element(target) ? target : this.elements.display.buffer;\\n\\n      // Update value and label\\n      if (is.element(progress)) {\\n        progress.value = val;\\n\\n        // Update text label inside\\n        const label = progress.getElementsByTagName('span')[0];\\n        if (is.element(label)) {\\n          label.childNodes[0].nodeValue = val;\\n        }\\n      }\\n    };\\n\\n    if (event) {\\n      switch (event.type) {\\n        // Video playing\\n        case 'timeupdate':\\n        case 'seeking':\\n        case 'seeked':\\n          value = getPercentage(this.currentTime, this.duration);\\n\\n          // Set seek range value only if it's a 'natural' time event\\n          if (event.type === 'timeupdate') {\\n            controls.setRange.call(this, this.elements.inputs.seek, value);\\n          }\\n\\n          break;\\n\\n        // Check buffer status\\n        case 'playing':\\n        case 'progress':\\n          setProgress(this.elements.display.buffer, this.buffered * 100);\\n\\n          break;\\n\\n        default:\\n          break;\\n      }\\n    }\\n  },\\n\\n  // Webkit polyfill for lower fill range\\n  updateRangeFill(target) {\\n    // Get range from event if event passed\\n    const range = is.event(target) ? target.target : target;\\n\\n    // Needs to be a valid <input type='range'>\\n    if (!is.element(range) || range.getAttribute('type') !== 'range') {\\n      return;\\n    }\\n\\n    // Set aria values for https://github.com/sampotts/plyr/issues/905\\n    if (matches(range, this.config.selectors.inputs.seek)) {\\n      range.setAttribute('aria-valuenow', this.currentTime);\\n      const currentTime = controls.formatTime(this.currentTime);\\n      const duration = controls.formatTime(this.duration);\\n      const format = i18n.get('seekLabel', this.config);\\n      range.setAttribute(\\n        'aria-valuetext',\\n        format.replace('{currentTime}', currentTime).replace('{duration}', duration),\\n      );\\n    } else if (matches(range, this.config.selectors.inputs.volume)) {\\n      const percent = range.value * 100;\\n      range.setAttribute('aria-valuenow', percent);\\n      range.setAttribute('aria-valuetext', `${percent.toFixed(1)}%`);\\n    } else {\\n      range.setAttribute('aria-valuenow', range.value);\\n    }\\n\\n    // WebKit only\\n    if (!browser.isWebKit && !browser.isIPadOS) {\\n      return;\\n    }\\n\\n    // Set CSS custom property\\n    range.style.setProperty('--value', `${(range.value / range.max) * 100}%`);\\n  },\\n\\n  // Update hover tooltip for seeking\\n  updateSeekTooltip(event) {\\n    // Bail if setting not true\\n    if (\\n      !this.config.tooltips.seek ||\\n      !is.element(this.elements.inputs.seek) ||\\n      !is.element(this.elements.display.seekTooltip) ||\\n      this.duration === 0\\n    ) {\\n      return;\\n    }\\n\\n    const tipElement = this.elements.display.seekTooltip;\\n    const visible = `${this.config.classNames.tooltip}--visible`;\\n    const toggle = (show) => toggleClass(tipElement, visible, show);\\n\\n    // Hide on touch\\n    if (this.touch) {\\n      toggle(false);\\n      return;\\n    }\\n\\n    // Determine percentage, if already visible\\n    let percent = 0;\\n    const clientRect = this.elements.progress.getBoundingClientRect();\\n\\n    if (is.event(event)) {\\n      percent = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n    } else if (hasClass(tipElement, visible)) {\\n      percent = parseFloat(tipElement.style.left, 10);\\n    } else {\\n      return;\\n    }\\n\\n    // Set bounds\\n    if (percent < 0) {\\n      percent = 0;\\n    } else if (percent > 100) {\\n      percent = 100;\\n    }\\n\\n    const time = (this.duration / 100) * percent;\\n\\n    // Display the time a click would seek to\\n    tipElement.innerText = controls.formatTime(time);\\n\\n    // Get marker point for time\\n    const point = this.config.markers?.points?.find(({ time: t }) => t === Math.round(time));\\n\\n    // Append the point label to the tooltip\\n    if (point) {\\n      tipElement.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n    }\\n\\n    // Set position\\n    tipElement.style.left = `${percent}%`;\\n\\n    // Show/hide the tooltip\\n    // If the event is a moues in/out and percentage is inside bounds\\n    if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) {\\n      toggle(event.type === 'mouseenter');\\n    }\\n  },\\n\\n  // Handle time change event\\n  timeUpdate(event) {\\n    // Only invert if only one time element is displayed and used for both duration and currentTime\\n    const invert = !is.element(this.elements.display.duration) && this.config.invertTime;\\n\\n    // Duration\\n    controls.updateTimeDisplay.call(\\n      this,\\n      this.elements.display.currentTime,\\n      invert ? this.duration - this.currentTime : this.currentTime,\\n      invert,\\n    );\\n\\n    // Ignore updates while seeking\\n    if (event && event.type === 'timeupdate' && this.media.seeking) {\\n      return;\\n    }\\n\\n    // Playing progress\\n    controls.updateProgress.call(this, event);\\n  },\\n\\n  // Show the duration on metadataloaded or durationchange events\\n  durationUpdate() {\\n    // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false\\n    if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) {\\n      return;\\n    }\\n\\n    // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar.\\n    // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415\\n    // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062\\n    // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338\\n    if (this.duration >= 2 ** 32) {\\n      toggleHidden(this.elements.display.currentTime, true);\\n      toggleHidden(this.elements.progress, true);\\n      return;\\n    }\\n\\n    // Update ARIA values\\n    if (is.element(this.elements.inputs.seek)) {\\n      this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration);\\n    }\\n\\n    // If there's a spot to display duration\\n    const hasDuration = is.element(this.elements.display.duration);\\n\\n    // If there's only one time display, display duration there\\n    if (!hasDuration && this.config.displayDuration && this.paused) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration);\\n    }\\n\\n    // If there's a duration element, update content\\n    if (hasDuration) {\\n      controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration);\\n    }\\n\\n    if (this.config.markers.enabled) {\\n      controls.setMarkers.call(this);\\n    }\\n\\n    // Update the tooltip (if visible)\\n    controls.updateSeekTooltip.call(this);\\n  },\\n\\n  // Hide/show a tab\\n  toggleMenuButton(setting, toggle) {\\n    toggleHidden(this.elements.settings.buttons[setting], !toggle);\\n  },\\n\\n  // Update the selected setting\\n  updateSetting(setting, container, input) {\\n    const pane = this.elements.settings.panels[setting];\\n    let value = null;\\n    let list = container;\\n\\n    if (setting === 'captions') {\\n      value = this.currentTrack;\\n    } else {\\n      value = !is.empty(input) ? input : this[setting];\\n\\n      // Get default\\n      if (is.empty(value)) {\\n        value = this.config[setting].default;\\n      }\\n\\n      // Unsupported value\\n      if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) {\\n        this.debug.warn(`Unsupported value of '${value}' for ${setting}`);\\n        return;\\n      }\\n\\n      // Disabled value\\n      if (!this.config[setting].options.includes(value)) {\\n        this.debug.warn(`Disabled value of '${value}' for ${setting}`);\\n        return;\\n      }\\n    }\\n\\n    // Get the list if we need to\\n    if (!is.element(list)) {\\n      list = pane && pane.querySelector('[role=\\\"menu\\\"]');\\n    }\\n\\n    // If there's no list it means it's not been rendered...\\n    if (!is.element(list)) {\\n      return;\\n    }\\n\\n    // Update the label\\n    const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`);\\n    label.innerHTML = controls.getLabel.call(this, setting, value);\\n\\n    // Find the radio option and check it\\n    const target = list && list.querySelector(`[value=\\\"${value}\\\"]`);\\n\\n    if (is.element(target)) {\\n      target.checked = true;\\n    }\\n  },\\n\\n  // Translate a value into a nice label\\n  getLabel(setting, value) {\\n    switch (setting) {\\n      case 'speed':\\n        return value === 1 ? i18n.get('normal', this.config) : `${value}&times;`;\\n\\n      case 'quality':\\n        if (is.number(value)) {\\n          const label = i18n.get(`qualityLabel.${value}`, this.config);\\n\\n          if (!label.length) {\\n            return `${value}p`;\\n          }\\n\\n          return label;\\n        }\\n\\n        return toTitleCase(value);\\n\\n      case 'captions':\\n        return captions.getLabel.call(this);\\n\\n      default:\\n        return null;\\n    }\\n  },\\n\\n  // Set the quality menu\\n  setQualityMenu(options) {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.quality)) {\\n      return;\\n    }\\n\\n    const type = 'quality';\\n    const list = this.elements.settings.panels.quality.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Set options if passed and filter based on uniqueness and config\\n    if (is.array(options)) {\\n      this.options.quality = dedupe(options).filter((quality) => this.config.quality.options.includes(quality));\\n    }\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Get the badge HTML for HD, 4K etc\\n    const getBadge = (quality) => {\\n      const label = i18n.get(`qualityBadge.${quality}`, this.config);\\n\\n      if (!label.length) {\\n        return null;\\n      }\\n\\n      return controls.createBadge.call(this, label);\\n    };\\n\\n    // Sort options by the config and then render options\\n    this.options.quality\\n      .sort((a, b) => {\\n        const sorting = this.config.quality.options;\\n        return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;\\n      })\\n      .forEach((quality) => {\\n        controls.createMenuItem.call(this, {\\n          value: quality,\\n          list,\\n          type,\\n          title: controls.getLabel.call(this, 'quality', quality),\\n          badge: getBadge(quality),\\n        });\\n      });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set the looping options\\n  /* setLoopMenu() {\\n        // Menu required\\n        if (!is.element(this.elements.settings.panels.loop)) {\\n            return;\\n        }\\n\\n        const options = ['start', 'end', 'all', 'reset'];\\n        const list = this.elements.settings.panels.loop.querySelector('[role=\\\"menu\\\"]');\\n\\n        // Show the pane and tab\\n        toggleHidden(this.elements.settings.buttons.loop, false);\\n        toggleHidden(this.elements.settings.panels.loop, false);\\n\\n        // Toggle the pane and tab\\n        const toggle = !is.empty(this.loop.options);\\n        controls.toggleMenuButton.call(this, 'loop', toggle);\\n\\n        // Empty the menu\\n        emptyElement(list);\\n\\n        options.forEach(option => {\\n            const item = createElement('li');\\n\\n            const button = createElement(\\n                'button',\\n                extend(getAttributesFromSelector(this.config.selectors.buttons.loop), {\\n                    type: 'button',\\n                    class: this.config.classNames.control,\\n                    'data-plyr-loop-action': option,\\n                }),\\n                i18n.get(option, this.config)\\n            );\\n\\n            if (['start', 'end'].includes(option)) {\\n                const badge = controls.createBadge.call(this, '00:00');\\n                button.appendChild(badge);\\n            }\\n\\n            item.appendChild(button);\\n            list.appendChild(item);\\n        });\\n    }, */\\n\\n  // Get current selected caption language\\n  // TODO: rework this to user the getter in the API?\\n\\n  // Set a list of available captions languages\\n  setCaptionsMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.captions)) {\\n      return;\\n    }\\n\\n    // TODO: Captions or language? Currently it's mixed\\n    const type = 'captions';\\n    const list = this.elements.settings.panels.captions.querySelector('[role=\\\"menu\\\"]');\\n    const tracks = captions.getTracks.call(this);\\n    const toggle = Boolean(tracks.length);\\n\\n    // Toggle the pane and tab\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If there's no captions, bail\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Generate options data\\n    const options = tracks.map((track, value) => ({\\n      value,\\n      checked: this.captions.toggled && this.currentTrack === value,\\n      title: captions.getLabel.call(this, track),\\n      badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()),\\n      list,\\n      type: 'language',\\n    }));\\n\\n    // Add the \\\"Disabled\\\" option to turn off captions\\n    options.unshift({\\n      value: -1,\\n      checked: !this.captions.toggled,\\n      title: i18n.get('disabled', this.config),\\n      list,\\n      type: 'language',\\n    });\\n\\n    // Generate options\\n    options.forEach(controls.createMenuItem.bind(this));\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Set a list of available captions languages\\n  setSpeedMenu() {\\n    // Menu required\\n    if (!is.element(this.elements.settings.panels.speed)) {\\n      return;\\n    }\\n\\n    const type = 'speed';\\n    const list = this.elements.settings.panels.speed.querySelector('[role=\\\"menu\\\"]');\\n\\n    // Filter out invalid speeds\\n    this.options.speed = this.options.speed.filter((o) => o >= this.minimumSpeed && o <= this.maximumSpeed);\\n\\n    // Toggle the pane and tab\\n    const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1;\\n    controls.toggleMenuButton.call(this, type, toggle);\\n\\n    // Empty the menu\\n    emptyElement(list);\\n\\n    // Check if we need to toggle the parent\\n    controls.checkMenu.call(this);\\n\\n    // If we're hiding, nothing more to do\\n    if (!toggle) {\\n      return;\\n    }\\n\\n    // Create items\\n    this.options.speed.forEach((speed) => {\\n      controls.createMenuItem.call(this, {\\n        value: speed,\\n        list,\\n        type,\\n        title: controls.getLabel.call(this, 'speed', speed),\\n      });\\n    });\\n\\n    controls.updateSetting.call(this, type, list);\\n  },\\n\\n  // Check if we need to hide/show the settings menu\\n  checkMenu() {\\n    const { buttons } = this.elements.settings;\\n    const visible = !is.empty(buttons) && Object.values(buttons).some((button) => !button.hidden);\\n\\n    toggleHidden(this.elements.settings.menu, !visible);\\n  },\\n\\n  // Focus the first menu item in a given (or visible) menu\\n  focusFirstMenuItem(pane, focusVisible = false) {\\n    if (this.elements.settings.popup.hidden) {\\n      return;\\n    }\\n\\n    let target = pane;\\n\\n    if (!is.element(target)) {\\n      target = Object.values(this.elements.settings.panels).find((p) => !p.hidden);\\n    }\\n\\n    const firstItem = target.querySelector('[role^=\\\"menuitem\\\"]');\\n\\n    setFocus.call(this, firstItem, focusVisible);\\n  },\\n\\n  // Show/hide menu\\n  toggleMenu(input) {\\n    const { popup } = this.elements.settings;\\n    const button = this.elements.buttons.settings;\\n\\n    // Menu and button are required\\n    if (!is.element(popup) || !is.element(button)) {\\n      return;\\n    }\\n\\n    // True toggle by default\\n    const { hidden } = popup;\\n    let show = hidden;\\n\\n    if (is.boolean(input)) {\\n      show = input;\\n    } else if (is.keyboardEvent(input) && input.key === 'Escape') {\\n      show = false;\\n    } else if (is.event(input)) {\\n      // If Plyr is in a shadowDOM, the event target is set to the component, instead of the\\n      // Element in the shadowDOM. The path, if available, is complete.\\n      const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target;\\n      const isMenuItem = popup.contains(target);\\n\\n      // If the click was inside the menu or if the click\\n      // wasn't the button or menu item and we're trying to\\n      // show the menu (a doc click shouldn't show the menu)\\n      if (isMenuItem || (!isMenuItem && input.target !== button && show)) {\\n        return;\\n      }\\n    }\\n\\n    // Set button attributes\\n    button.setAttribute('aria-expanded', show);\\n\\n    // Show the actual popup\\n    toggleHidden(popup, !show);\\n\\n    // Add class hook\\n    toggleClass(this.elements.container, this.config.classNames.menu.open, show);\\n\\n    // Focus the first item if key interaction\\n    if (show && is.keyboardEvent(input)) {\\n      controls.focusFirstMenuItem.call(this, null, true);\\n    } else if (!show && !hidden) {\\n      // If closing, re-focus the button\\n      setFocus.call(this, button, is.keyboardEvent(input));\\n    }\\n  },\\n\\n  // Get the natural size of a menu panel\\n  getMenuSize(tab) {\\n    const clone = tab.cloneNode(true);\\n    clone.style.position = 'absolute';\\n    clone.style.opacity = 0;\\n    clone.removeAttribute('hidden');\\n\\n    // Append to parent so we get the \\\"real\\\" size\\n    tab.parentNode.appendChild(clone);\\n\\n    // Get the sizes before we remove\\n    const width = clone.scrollWidth;\\n    const height = clone.scrollHeight;\\n\\n    // Remove from the DOM\\n    removeElement(clone);\\n\\n    return {\\n      width,\\n      height,\\n    };\\n  },\\n\\n  // Show a panel in the menu\\n  showMenuPanel(type = '', focusVisible = false) {\\n    const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`);\\n\\n    // Nothing to show, bail\\n    if (!is.element(target)) {\\n      return;\\n    }\\n\\n    // Hide all other panels\\n    const container = target.parentNode;\\n    const current = Array.from(container.children).find((node) => !node.hidden);\\n\\n    // If we can do fancy animations, we'll animate the height/width\\n    if (support.transitions && !support.reducedMotion) {\\n      // Set the current width as a base\\n      container.style.width = `${current.scrollWidth}px`;\\n      container.style.height = `${current.scrollHeight}px`;\\n\\n      // Get potential sizes\\n      const size = controls.getMenuSize.call(this, target);\\n\\n      // Restore auto height/width\\n      const restore = (event) => {\\n        // We're only bothered about height and width on the container\\n        if (event.target !== container || !['width', 'height'].includes(event.propertyName)) {\\n          return;\\n        }\\n\\n        // Revert back to auto\\n        container.style.width = '';\\n        container.style.height = '';\\n\\n        // Only listen once\\n        off.call(this, container, transitionEndEvent, restore);\\n      };\\n\\n      // Listen for the transition finishing and restore auto height/width\\n      on.call(this, container, transitionEndEvent, restore);\\n\\n      // Set dimensions to target\\n      container.style.width = `${size.width}px`;\\n      container.style.height = `${size.height}px`;\\n    }\\n\\n    // Set attributes on current tab\\n    toggleHidden(current, true);\\n\\n    // Set attributes on target\\n    toggleHidden(target, false);\\n\\n    // Focus the first item\\n    controls.focusFirstMenuItem.call(this, target, focusVisible);\\n  },\\n\\n  // Set the download URL\\n  setDownloadUrl() {\\n    const button = this.elements.buttons.download;\\n\\n    // Bail if no button\\n    if (!is.element(button)) {\\n      return;\\n    }\\n\\n    // Set attribute\\n    button.setAttribute('href', this.download);\\n  },\\n\\n  // Build the default HTML\\n  create(data) {\\n    const {\\n      bindMenuItemShortcuts,\\n      createButton,\\n      createProgress,\\n      createRange,\\n      createTime,\\n      setQualityMenu,\\n      setSpeedMenu,\\n      showMenuPanel,\\n    } = controls;\\n    this.elements.controls = null;\\n\\n    // Larger overlaid play button\\n    if (is.array(this.config.controls) && this.config.controls.includes('play-large')) {\\n      this.elements.container.appendChild(createButton.call(this, 'play-large'));\\n    }\\n\\n    // Create the container\\n    const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper));\\n    this.elements.controls = container;\\n\\n    // Default item attributes\\n    const defaultAttributes = { class: 'plyr__controls__item' };\\n\\n    // Loop through controls in order\\n    dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach((control) => {\\n      // Restart button\\n      if (control === 'restart') {\\n        container.appendChild(createButton.call(this, 'restart', defaultAttributes));\\n      }\\n\\n      // Rewind button\\n      if (control === 'rewind') {\\n        container.appendChild(createButton.call(this, 'rewind', defaultAttributes));\\n      }\\n\\n      // Play/Pause button\\n      if (control === 'play') {\\n        container.appendChild(createButton.call(this, 'play', defaultAttributes));\\n      }\\n\\n      // Fast forward button\\n      if (control === 'fast-forward') {\\n        container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes));\\n      }\\n\\n      // Progress\\n      if (control === 'progress') {\\n        const progressContainer = createElement('div', {\\n          class: `${defaultAttributes.class} plyr__progress__container`,\\n        });\\n\\n        const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress));\\n\\n        // Seek range slider\\n        progress.appendChild(\\n          createRange.call(this, 'seek', {\\n            id: `plyr-seek-${data.id}`,\\n          }),\\n        );\\n\\n        // Buffer progress\\n        progress.appendChild(createProgress.call(this, 'buffer'));\\n\\n        // TODO: Add loop display indicator\\n\\n        // Seek tooltip\\n        if (this.config.tooltips.seek) {\\n          const tooltip = createElement(\\n            'span',\\n            {\\n              class: this.config.classNames.tooltip,\\n            },\\n            '00:00',\\n          );\\n\\n          progress.appendChild(tooltip);\\n          this.elements.display.seekTooltip = tooltip;\\n        }\\n\\n        this.elements.progress = progress;\\n        progressContainer.appendChild(this.elements.progress);\\n        container.appendChild(progressContainer);\\n      }\\n\\n      // Media current time display\\n      if (control === 'current-time') {\\n        container.appendChild(createTime.call(this, 'currentTime', defaultAttributes));\\n      }\\n\\n      // Media duration display\\n      if (control === 'duration') {\\n        container.appendChild(createTime.call(this, 'duration', defaultAttributes));\\n      }\\n\\n      // Volume controls\\n      if (control === 'mute' || control === 'volume') {\\n        let { volume } = this.elements;\\n\\n        // Create the volume container if needed\\n        if (!is.element(volume) || !container.contains(volume)) {\\n          volume = createElement(\\n            'div',\\n            extend({}, defaultAttributes, {\\n              class: `${defaultAttributes.class} plyr__volume`.trim(),\\n            }),\\n          );\\n\\n          this.elements.volume = volume;\\n\\n          container.appendChild(volume);\\n        }\\n\\n        // Toggle mute button\\n        if (control === 'mute') {\\n          volume.appendChild(createButton.call(this, 'mute'));\\n        }\\n\\n        // Volume range control\\n        // Ignored on iOS as it's handled globally\\n        // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html\\n        if (control === 'volume' && !browser.isIos && !browser.isIPadOS) {\\n          // Set the attributes\\n          const attributes = {\\n            max: 1,\\n            step: 0.05,\\n            value: this.config.volume,\\n          };\\n\\n          // Create the volume range slider\\n          volume.appendChild(\\n            createRange.call(\\n              this,\\n              'volume',\\n              extend(attributes, {\\n                id: `plyr-volume-${data.id}`,\\n              }),\\n            ),\\n          );\\n        }\\n      }\\n\\n      // Toggle captions button\\n      if (control === 'captions') {\\n        container.appendChild(createButton.call(this, 'captions', defaultAttributes));\\n      }\\n\\n      // Settings button / menu\\n      if (control === 'settings' && !is.empty(this.config.settings)) {\\n        const wrapper = createElement(\\n          'div',\\n          extend({}, defaultAttributes, {\\n            class: `${defaultAttributes.class} plyr__menu`.trim(),\\n            hidden: '',\\n          }),\\n        );\\n\\n        wrapper.appendChild(\\n          createButton.call(this, 'settings', {\\n            'aria-haspopup': true,\\n            'aria-controls': `plyr-settings-${data.id}`,\\n            'aria-expanded': false,\\n          }),\\n        );\\n\\n        const popup = createElement('div', {\\n          class: 'plyr__menu__container',\\n          id: `plyr-settings-${data.id}`,\\n          hidden: '',\\n        });\\n\\n        const inner = createElement('div');\\n\\n        const home = createElement('div', {\\n          id: `plyr-settings-${data.id}-home`,\\n        });\\n\\n        // Create the menu\\n        const menu = createElement('div', {\\n          role: 'menu',\\n        });\\n\\n        home.appendChild(menu);\\n        inner.appendChild(home);\\n        this.elements.settings.panels.home = home;\\n\\n        // Build the menu items\\n        this.config.settings.forEach((type) => {\\n          // TODO: bundle this with the createMenuItem helper and bindings\\n          const menuItem = createElement(\\n            'button',\\n            extend(getAttributesFromSelector(this.config.selectors.buttons.settings), {\\n              type: 'button',\\n              class: `${this.config.classNames.control} ${this.config.classNames.control}--forward`,\\n              role: 'menuitem',\\n              'aria-haspopup': true,\\n              hidden: '',\\n            }),\\n          );\\n\\n          // Bind menu shortcuts for keyboard users\\n          bindMenuItemShortcuts.call(this, menuItem, type);\\n\\n          // Show menu on click\\n          on.call(this, menuItem, 'click', () => {\\n            showMenuPanel.call(this, type, false);\\n          });\\n\\n          const flex = createElement('span', null, i18n.get(type, this.config));\\n\\n          const value = createElement('span', {\\n            class: this.config.classNames.menu.value,\\n          });\\n\\n          // Speed contains HTML entities\\n          value.innerHTML = data[type];\\n\\n          flex.appendChild(value);\\n          menuItem.appendChild(flex);\\n          menu.appendChild(menuItem);\\n\\n          // Build the panes\\n          const pane = createElement('div', {\\n            id: `plyr-settings-${data.id}-${type}`,\\n            hidden: '',\\n          });\\n\\n          // Back button\\n          const backButton = createElement('button', {\\n            type: 'button',\\n            class: `${this.config.classNames.control} ${this.config.classNames.control}--back`,\\n          });\\n\\n          // Visible label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                'aria-hidden': true,\\n              },\\n              i18n.get(type, this.config),\\n            ),\\n          );\\n\\n          // Screen reader label\\n          backButton.appendChild(\\n            createElement(\\n              'span',\\n              {\\n                class: this.config.classNames.hidden,\\n              },\\n              i18n.get('menuBack', this.config),\\n            ),\\n          );\\n\\n          // Go back via keyboard\\n          on.call(\\n            this,\\n            pane,\\n            'keydown',\\n            (event) => {\\n              if (event.key !== 'ArrowLeft') return;\\n\\n              // Prevent seek\\n              event.preventDefault();\\n              event.stopPropagation();\\n\\n              // Show the respective menu\\n              showMenuPanel.call(this, 'home', true);\\n            },\\n            false,\\n          );\\n\\n          // Go back via button click\\n          on.call(this, backButton, 'click', () => {\\n            showMenuPanel.call(this, 'home', false);\\n          });\\n\\n          // Add to pane\\n          pane.appendChild(backButton);\\n\\n          // Menu\\n          pane.appendChild(\\n            createElement('div', {\\n              role: 'menu',\\n            }),\\n          );\\n\\n          inner.appendChild(pane);\\n\\n          this.elements.settings.buttons[type] = menuItem;\\n          this.elements.settings.panels[type] = pane;\\n        });\\n\\n        popup.appendChild(inner);\\n        wrapper.appendChild(popup);\\n        container.appendChild(wrapper);\\n\\n        this.elements.settings.popup = popup;\\n        this.elements.settings.menu = wrapper;\\n      }\\n\\n      // Picture in picture button\\n      if (control === 'pip' && support.pip) {\\n        container.appendChild(createButton.call(this, 'pip', defaultAttributes));\\n      }\\n\\n      // Airplay button\\n      if (control === 'airplay' && support.airplay) {\\n        container.appendChild(createButton.call(this, 'airplay', defaultAttributes));\\n      }\\n\\n      // Download button\\n      if (control === 'download') {\\n        const attributes = extend({}, defaultAttributes, {\\n          element: 'a',\\n          href: this.download,\\n          target: '_blank',\\n        });\\n\\n        // Set download attribute for HTML5 only\\n        if (this.isHTML5) {\\n          attributes.download = '';\\n        }\\n\\n        const { download } = this.config.urls;\\n\\n        if (!is.url(download) && this.isEmbed) {\\n          extend(attributes, {\\n            icon: `logo-${this.provider}`,\\n            label: this.provider,\\n          });\\n        }\\n\\n        container.appendChild(createButton.call(this, 'download', attributes));\\n      }\\n\\n      // Toggle fullscreen button\\n      if (control === 'fullscreen') {\\n        container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes));\\n      }\\n    });\\n\\n    // Set available quality levels\\n    if (this.isHTML5) {\\n      setQualityMenu.call(this, html5.getQualityOptions.call(this));\\n    }\\n\\n    setSpeedMenu.call(this);\\n\\n    return container;\\n  },\\n\\n  // Insert controls\\n  inject() {\\n    // Sprite\\n    if (this.config.loadSprite) {\\n      const icon = controls.getIconUrl.call(this);\\n\\n      // Only load external sprite using AJAX\\n      if (icon.cors) {\\n        loadSprite(icon.url, 'sprite-plyr');\\n      }\\n    }\\n\\n    // Create a unique ID\\n    this.id = Math.floor(Math.random() * 10000);\\n\\n    // Null by default\\n    let container = null;\\n    this.elements.controls = null;\\n\\n    // Set template properties\\n    const props = {\\n      id: this.id,\\n      seektime: this.config.seekTime,\\n      title: this.config.title,\\n    };\\n    let update = true;\\n\\n    // If function, run it and use output\\n    if (is.function(this.config.controls)) {\\n      this.config.controls = this.config.controls.call(this, props);\\n    }\\n\\n    // Convert falsy controls to empty array (primarily for empty strings)\\n    if (!this.config.controls) {\\n      this.config.controls = [];\\n    }\\n\\n    if (is.element(this.config.controls) || is.string(this.config.controls)) {\\n      // HTMLElement or Non-empty string passed as the option\\n      container = this.config.controls;\\n    } else {\\n      // Create controls\\n      container = controls.create.call(this, {\\n        id: this.id,\\n        seektime: this.config.seekTime,\\n        speed: this.speed,\\n        quality: this.quality,\\n        captions: captions.getLabel.call(this),\\n        // TODO: Looping\\n        // loop: 'None',\\n      });\\n      update = false;\\n    }\\n\\n    // Replace props with their value\\n    const replace = (input) => {\\n      let result = input;\\n\\n      Object.entries(props).forEach(([key, value]) => {\\n        result = replaceAll(result, `{${key}}`, value);\\n      });\\n\\n      return result;\\n    };\\n\\n    // Update markup\\n    if (update) {\\n      if (is.string(this.config.controls)) {\\n        container = replace(container);\\n      }\\n    }\\n\\n    // Controls container\\n    let target;\\n\\n    // Inject to custom location\\n    if (is.string(this.config.selectors.controls.container)) {\\n      target = document.querySelector(this.config.selectors.controls.container);\\n    }\\n\\n    // Inject into the container by default\\n    if (!is.element(target)) {\\n      target = this.elements.container;\\n    }\\n\\n    // Inject controls HTML (needs to be before captions, hence \\\"afterbegin\\\")\\n    const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML';\\n    target[insertMethod]('afterbegin', container);\\n\\n    // Find the elements if need be\\n    if (!is.element(this.elements.controls)) {\\n      controls.findElements.call(this);\\n    }\\n\\n    // Add pressed property to buttons\\n    if (!is.empty(this.elements.buttons)) {\\n      const addProperty = (button) => {\\n        const className = this.config.classNames.controlPressed;\\n        button.setAttribute('aria-pressed', 'false');\\n\\n        Object.defineProperty(button, 'pressed', {\\n          configurable: true,\\n          enumerable: true,\\n          get() {\\n            return hasClass(button, className);\\n          },\\n          set(pressed = false) {\\n            toggleClass(button, className, pressed);\\n            button.setAttribute('aria-pressed', pressed ? 'true' : 'false');\\n          },\\n        });\\n      };\\n\\n      // Toggle classname when pressed property is set\\n      Object.values(this.elements.buttons)\\n        .filter(Boolean)\\n        .forEach((button) => {\\n          if (is.array(button) || is.nodeList(button)) {\\n            Array.from(button).filter(Boolean).forEach(addProperty);\\n          } else {\\n            addProperty(button);\\n          }\\n        });\\n    }\\n\\n    // Edge sometimes doesn't finish the paint so force a repaint\\n    if (browser.isEdge) {\\n      repaint(target);\\n    }\\n\\n    // Setup tooltips\\n    if (this.config.tooltips.controls) {\\n      const { classNames, selectors } = this.config;\\n      const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`;\\n      const labels = getElements.call(this, selector);\\n\\n      Array.from(labels).forEach((label) => {\\n        toggleClass(label, this.config.classNames.hidden, false);\\n        toggleClass(label, this.config.classNames.tooltip, true);\\n      });\\n    }\\n  },\\n\\n  // Set media metadata\\n  setMediaMetadata() {\\n    try {\\n      if ('mediaSession' in navigator) {\\n        navigator.mediaSession.metadata = new window.MediaMetadata({\\n          title: this.config.mediaMetadata.title,\\n          artist: this.config.mediaMetadata.artist,\\n          album: this.config.mediaMetadata.album,\\n          artwork: this.config.mediaMetadata.artwork,\\n        });\\n      }\\n    } catch (_) {\\n      // Do nothing\\n    }\\n  },\\n\\n  // Add markers\\n  setMarkers() {\\n    if (!this.duration || this.elements.markers) return;\\n\\n    // Get valid points\\n    const points = this.config.markers?.points?.filter(({ time }) => time > 0 && time < this.duration);\\n    if (!points?.length) return;\\n\\n    const containerFragment = document.createDocumentFragment();\\n    const pointsFragment = document.createDocumentFragment();\\n    let tipElement = null;\\n    const tipVisible = `${this.config.classNames.tooltip}--visible`;\\n    const toggleTip = (show) => toggleClass(tipElement, tipVisible, show);\\n\\n    // Inject markers to progress container\\n    points.forEach((point) => {\\n      const markerElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.marker,\\n        },\\n        '',\\n      );\\n\\n      const left = `${(point.time / this.duration) * 100}%`;\\n\\n      if (tipElement) {\\n        // Show on hover\\n        markerElement.addEventListener('mouseenter', () => {\\n          if (point.label) return;\\n          tipElement.style.left = left;\\n          tipElement.innerHTML = point.label;\\n          toggleTip(true);\\n        });\\n\\n        // Hide on leave\\n        markerElement.addEventListener('mouseleave', () => {\\n          toggleTip(false);\\n        });\\n      }\\n\\n      markerElement.addEventListener('click', () => {\\n        this.currentTime = point.time;\\n      });\\n\\n      markerElement.style.left = left;\\n      pointsFragment.appendChild(markerElement);\\n    });\\n\\n    containerFragment.appendChild(pointsFragment);\\n\\n    // Inject a tooltip if needed\\n    if (!this.config.tooltips.seek) {\\n      tipElement = createElement(\\n        'span',\\n        {\\n          class: this.config.classNames.tooltip,\\n        },\\n        '',\\n      );\\n\\n      containerFragment.appendChild(tipElement);\\n    }\\n\\n    this.elements.markers = {\\n      points: pointsFragment,\\n      tip: tipElement,\\n    };\\n\\n    this.elements.progress.appendChild(containerFragment);\\n  },\\n};\\n\\nexport default controls;\\n\",\"// ==========================================================================\\n// URL utils\\n// ==========================================================================\\n\\nimport is from './is';\\n\\n/**\\n * Parse a string to a URL object\\n * @param {String} input - the URL to be parsed\\n * @param {Boolean} safe - failsafe parsing\\n */\\nexport function parseUrl(input, safe = true) {\\n  let url = input;\\n\\n  if (safe) {\\n    const parser = document.createElement('a');\\n    parser.href = url;\\n    url = parser.href;\\n  }\\n\\n  try {\\n    return new URL(url);\\n  } catch (_) {\\n    return null;\\n  }\\n}\\n\\n// Convert object to URLSearchParams\\nexport function buildUrlParams(input) {\\n  const params = new URLSearchParams();\\n\\n  if (is.object(input)) {\\n    Object.entries(input).forEach(([key, value]) => {\\n      params.set(key, value);\\n    });\\n  }\\n\\n  return params;\\n}\\n\",\"// ==========================================================================\\n// Plyr Captions\\n// TODO: Create as class\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport support from './support';\\nimport { dedupe } from './utils/arrays';\\nimport browser from './utils/browser';\\nimport {\\n  createElement,\\n  emptyElement,\\n  getAttributesFromSelector,\\n  insertAfter,\\n  removeElement,\\n  toggleClass,\\n} from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport fetch from './utils/fetch';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport { getHTML } from './utils/strings';\\nimport { parseUrl } from './utils/urls';\\n\\nconst captions = {\\n  // Setup captions\\n  setup() {\\n    // Requires UI support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    // Only Vimeo and HTML5 video supported at this point\\n    if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) {\\n      // Clear menu and hide\\n      if (\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        this.config.settings.includes('captions')\\n      ) {\\n        controls.setCaptionsMenu.call(this);\\n      }\\n\\n      return;\\n    }\\n\\n    // Inject the container\\n    if (!is.element(this.elements.captions)) {\\n      this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions));\\n      this.elements.captions.setAttribute('dir', 'auto');\\n\\n      insertAfter(this.elements.captions, this.elements.wrapper);\\n    }\\n\\n    // Fix IE captions if CORS is used\\n    // Fetch captions and inject as blobs instead (data URIs not supported!)\\n    if (browser.isIE && window.URL) {\\n      const elements = this.media.querySelectorAll('track');\\n\\n      Array.from(elements).forEach((track) => {\\n        const src = track.getAttribute('src');\\n        const url = parseUrl(src);\\n\\n        if (\\n          url !== null &&\\n          url.hostname !== window.location.href.hostname &&\\n          ['http:', 'https:'].includes(url.protocol)\\n        ) {\\n          fetch(src, 'blob')\\n            .then((blob) => {\\n              track.setAttribute('src', window.URL.createObjectURL(blob));\\n            })\\n            .catch(() => {\\n              removeElement(track);\\n            });\\n        }\\n      });\\n    }\\n\\n    // Get and set initial data\\n    // The \\\"preferred\\\" options are not realized unless / until the wanted language has a match\\n    // * languages: Array of user's browser languages.\\n    // * language:  The language preferred by user settings or config\\n    // * active:    The state preferred by user settings or config\\n    // * toggled:   The real captions state\\n\\n    const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en'];\\n    const languages = dedupe(browserLanguages.map((language) => language.split('-')[0]));\\n    let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase();\\n\\n    // Use first browser language when language is 'auto'\\n    if (language === 'auto') {\\n      [language] = languages;\\n    }\\n\\n    let active = this.storage.get('captions');\\n    if (!is.boolean(active)) {\\n      ({ active } = this.config.captions);\\n    }\\n\\n    Object.assign(this.captions, {\\n      toggled: false,\\n      active,\\n      language,\\n      languages,\\n    });\\n\\n    // Watch changes to textTracks and update captions menu\\n    if (this.isHTML5) {\\n      const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack';\\n      on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this));\\n    }\\n\\n    // Update available languages in list next tick (the event must not be triggered before the listeners)\\n    setTimeout(captions.update.bind(this), 0);\\n  },\\n\\n  // Update available language options in settings based on tracks\\n  update() {\\n    const tracks = captions.getTracks.call(this, true);\\n    // Get the wanted language\\n    const { active, language, meta, currentTrackNode } = this.captions;\\n    const languageExists = Boolean(tracks.find((track) => track.language === language));\\n\\n    // Handle tracks (add event listener and \\\"pseudo\\\"-default)\\n    if (this.isHTML5 && this.isVideo) {\\n      tracks\\n        .filter((track) => !meta.get(track))\\n        .forEach((track) => {\\n          this.debug.log('Track added', track);\\n\\n          // Attempt to store if the original dom element was \\\"default\\\"\\n          meta.set(track, {\\n            default: track.mode === 'showing',\\n          });\\n\\n          // Turn off native caption rendering to avoid double captions\\n          // Note: mode='hidden' forces a track to download. To ensure every track\\n          // isn't downloaded at once, only 'showing' tracks should be reassigned\\n          // eslint-disable-next-line no-param-reassign\\n          if (track.mode === 'showing') {\\n            // eslint-disable-next-line no-param-reassign\\n            track.mode = 'hidden';\\n          }\\n\\n          // Add event listener for cue changes\\n          on.call(this, track, 'cuechange', () => captions.updateCues.call(this));\\n        });\\n    }\\n\\n    // Update language first time it matches, or if the previous matching track was removed\\n    if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) {\\n      captions.setLanguage.call(this, language);\\n      captions.toggle.call(this, active && languageExists);\\n    }\\n\\n    // Enable or disable captions based on track length\\n    if (this.elements) {\\n      toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks));\\n    }\\n\\n    // Update available languages in list\\n    if (\\n      is.array(this.config.controls) &&\\n      this.config.controls.includes('settings') &&\\n      this.config.settings.includes('captions')\\n    ) {\\n      controls.setCaptionsMenu.call(this);\\n    }\\n  },\\n\\n  // Toggle captions display\\n  // Used internally for the toggleCaptions method, with the passive option forced to false\\n  toggle(input, passive = true) {\\n    // If there's no full support\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    const { toggled } = this.captions; // Current state\\n    const activeClass = this.config.classNames.captions.active;\\n    // Get the next state\\n    // If the method is called without parameter, toggle based on current value\\n    const active = is.nullOrUndefined(input) ? !toggled : input;\\n\\n    // Update state and trigger event\\n    if (active !== toggled) {\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.active = active;\\n        this.storage.set({ captions: active });\\n      }\\n\\n      // Force language if the call isn't passive and there is no matching language to toggle to\\n      if (!this.language && active && !passive) {\\n        const tracks = captions.getTracks.call(this);\\n        const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true);\\n\\n        // Override user preferences to avoid switching languages if a matching track is added\\n        this.captions.language = track.language;\\n\\n        // Set caption, but don't store in localStorage as user preference\\n        captions.set.call(this, tracks.indexOf(track));\\n        return;\\n      }\\n\\n      // Toggle button if it's enabled\\n      if (this.elements.buttons.captions) {\\n        this.elements.buttons.captions.pressed = active;\\n      }\\n\\n      // Add class hook\\n      toggleClass(this.elements.container, activeClass, active);\\n\\n      this.captions.toggled = active;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // Trigger event (not used internally)\\n      triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled');\\n    }\\n\\n    // Wait for the call stack to clear before setting mode='hidden'\\n    // on the active track - forcing the browser to download it\\n    setTimeout(() => {\\n      if (active && this.captions.toggled) {\\n        this.captions.currentTrackNode.mode = 'hidden';\\n      }\\n    });\\n  },\\n\\n  // Set captions by track index\\n  // Used internally for the currentTrack setter with the passive option forced to false\\n  set(index, passive = true) {\\n    const tracks = captions.getTracks.call(this);\\n\\n    // Disable captions if setting to -1\\n    if (index === -1) {\\n      captions.toggle.call(this, false, passive);\\n      return;\\n    }\\n\\n    if (!is.number(index)) {\\n      this.debug.warn('Invalid caption argument', index);\\n      return;\\n    }\\n\\n    if (!(index in tracks)) {\\n      this.debug.warn('Track not found', index);\\n      return;\\n    }\\n\\n    if (this.captions.currentTrack !== index) {\\n      this.captions.currentTrack = index;\\n      const track = tracks[index];\\n      const { language } = track || {};\\n\\n      // Store reference to node for invalidation on remove\\n      this.captions.currentTrackNode = track;\\n\\n      // Update settings menu\\n      controls.updateSetting.call(this, 'captions');\\n\\n      // When passive, don't override user preferences\\n      if (!passive) {\\n        this.captions.language = language;\\n        this.storage.set({ language });\\n      }\\n\\n      // Handle Vimeo captions\\n      if (this.isVimeo) {\\n        this.embed.enableTextTrack(language);\\n      }\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'languagechange');\\n    }\\n\\n    // Show captions\\n    captions.toggle.call(this, true, passive);\\n\\n    if (this.isHTML5 && this.isVideo) {\\n      // If we change the active track while a cue is already displayed we need to update it\\n      captions.updateCues.call(this);\\n    }\\n  },\\n\\n  // Set captions by language\\n  // Used internally for the language setter with the passive option forced to false\\n  setLanguage(input, passive = true) {\\n    if (!is.string(input)) {\\n      this.debug.warn('Invalid language argument', input);\\n      return;\\n    }\\n    // Normalize\\n    const language = input.toLowerCase();\\n    this.captions.language = language;\\n\\n    // Set currentTrack\\n    const tracks = captions.getTracks.call(this);\\n    const track = captions.findTrack.call(this, [language]);\\n    captions.set.call(this, tracks.indexOf(track), passive);\\n  },\\n\\n  // Get current valid caption tracks\\n  // If update is false it will also ignore tracks without metadata\\n  // This is used to \\\"freeze\\\" the language options when captions.update is false\\n  getTracks(update = false) {\\n    // Handle media or textTracks missing or null\\n    const tracks = Array.from((this.media || {}).textTracks || []);\\n    // For HTML5, use cache instead of current tracks when it exists (if captions.update is false)\\n    // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata)\\n    return tracks\\n      .filter((track) => !this.isHTML5 || update || this.captions.meta.has(track))\\n      .filter((track) => ['captions', 'subtitles'].includes(track.kind));\\n  },\\n\\n  // Match tracks based on languages and get the first\\n  findTrack(languages, force = false) {\\n    const tracks = captions.getTracks.call(this);\\n    const sortIsDefault = (track) => Number((this.captions.meta.get(track) || {}).default);\\n    const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a));\\n    let track;\\n\\n    languages.every((language) => {\\n      track = sorted.find((t) => t.language === language);\\n      return !track; // Break iteration if there is a match\\n    });\\n\\n    // If no match is found but is required, get first\\n    return track || (force ? sorted[0] : undefined);\\n  },\\n\\n  // Get the current track\\n  getCurrentTrack() {\\n    return captions.getTracks.call(this)[this.currentTrack];\\n  },\\n\\n  // Get UI label for track\\n  getLabel(track) {\\n    let currentTrack = track;\\n\\n    if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) {\\n      currentTrack = captions.getCurrentTrack.call(this);\\n    }\\n\\n    if (is.track(currentTrack)) {\\n      if (!is.empty(currentTrack.label)) {\\n        return currentTrack.label;\\n      }\\n\\n      if (!is.empty(currentTrack.language)) {\\n        return track.language.toUpperCase();\\n      }\\n\\n      return i18n.get('enabled', this.config);\\n    }\\n\\n    return i18n.get('disabled', this.config);\\n  },\\n\\n  // Update captions using current track's active cues\\n  // Also optional array argument in case there isn't any track (ex: vimeo)\\n  updateCues(input) {\\n    // Requires UI\\n    if (!this.supported.ui) {\\n      return;\\n    }\\n\\n    if (!is.element(this.elements.captions)) {\\n      this.debug.warn('No captions element to render to');\\n      return;\\n    }\\n\\n    // Only accept array or empty input\\n    if (!is.nullOrUndefined(input) && !Array.isArray(input)) {\\n      this.debug.warn('updateCues: Invalid input', input);\\n      return;\\n    }\\n\\n    let cues = input;\\n\\n    // Get cues from track\\n    if (!cues) {\\n      const track = captions.getCurrentTrack.call(this);\\n\\n      cues = Array.from((track || {}).activeCues || [])\\n        .map((cue) => cue.getCueAsHTML())\\n        .map(getHTML);\\n    }\\n\\n    // Set new caption text\\n    const content = cues.map((cueText) => cueText.trim()).join('\\\\n');\\n    const changed = content !== this.elements.captions.innerHTML;\\n\\n    if (changed) {\\n      // Empty the container and create a new child element\\n      emptyElement(this.elements.captions);\\n      const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption));\\n      caption.innerHTML = content;\\n      this.elements.captions.appendChild(caption);\\n\\n      // Trigger event\\n      triggerEvent.call(this, this.media, 'cuechange');\\n    }\\n  },\\n};\\n\\nexport default captions;\\n\",\"// ==========================================================================\\n// Plyr default config\\n// ==========================================================================\\n\\nconst defaults = {\\n  // Disable\\n  enabled: true,\\n\\n  // Custom media title\\n  title: '',\\n\\n  // Logging to console\\n  debug: false,\\n\\n  // Auto play (if supported)\\n  autoplay: false,\\n\\n  // Only allow one media playing at once (vimeo only)\\n  autopause: true,\\n\\n  // Allow inline playback on iOS\\n  playsinline: true,\\n\\n  // Default time to skip when rewind/fast forward\\n  seekTime: 10,\\n\\n  // Default volume\\n  volume: 1,\\n  muted: false,\\n\\n  // Pass a custom duration\\n  duration: null,\\n\\n  // Display the media duration on load in the current time position\\n  // If you have opted to display both duration and currentTime, this is ignored\\n  displayDuration: true,\\n\\n  // Invert the current time to be a countdown\\n  invertTime: true,\\n\\n  // Clicking the currentTime inverts it's value to show time left rather than elapsed\\n  toggleInvert: true,\\n\\n  // Force an aspect ratio\\n  // The format must be `'w:h'` (e.g. `'16:9'`)\\n  ratio: null,\\n\\n  // Click video container to play/pause\\n  clickToPlay: true,\\n\\n  // Auto hide the controls\\n  hideControls: true,\\n\\n  // Reset to start when playback ended\\n  resetOnEnd: false,\\n\\n  // Disable the standard context menu\\n  disableContextMenu: true,\\n\\n  // Sprite (for icons)\\n  loadSprite: true,\\n  iconPrefix: 'plyr',\\n  iconUrl: 'https://cdn.plyr.io/3.7.8/plyr.svg',\\n\\n  // Blank video (used to prevent errors on source change)\\n  blankVideo: 'https://cdn.plyr.io/static/blank.mp4',\\n\\n  // Quality default\\n  quality: {\\n    default: 576,\\n    // The options to display in the UI, if available for the source media\\n    options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240],\\n    forced: false,\\n    onChange: null,\\n  },\\n\\n  // Set loops\\n  loop: {\\n    active: false,\\n    // start: null,\\n    // end: null,\\n  },\\n\\n  // Speed default and options to display\\n  speed: {\\n    selected: 1,\\n    // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x)\\n    options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4],\\n  },\\n\\n  // Keyboard shortcut settings\\n  keyboard: {\\n    focused: true,\\n    global: false,\\n  },\\n\\n  // Display tooltips\\n  tooltips: {\\n    controls: false,\\n    seek: true,\\n  },\\n\\n  // Captions settings\\n  captions: {\\n    active: false,\\n    language: 'auto',\\n    // Listen to new tracks added after Plyr is initialized.\\n    // This is needed for streaming captions, but may result in unselectable options\\n    update: false,\\n  },\\n\\n  // Fullscreen settings\\n  fullscreen: {\\n    enabled: true, // Allow fullscreen?\\n    fallback: true, // Fallback using full viewport/window\\n    iosNative: false, // Use the native fullscreen in iOS (disables custom controls)\\n    // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode\\n    // Non-ancestors of the player element will be ignored\\n    // container: null, // defaults to the player element\\n  },\\n\\n  // Local storage\\n  storage: {\\n    enabled: true,\\n    key: 'plyr',\\n  },\\n\\n  // Default controls\\n  controls: [\\n    'play-large',\\n    // 'restart',\\n    // 'rewind',\\n    'play',\\n    // 'fast-forward',\\n    'progress',\\n    'current-time',\\n    // 'duration',\\n    'mute',\\n    'volume',\\n    'captions',\\n    'settings',\\n    'pip',\\n    'airplay',\\n    // 'download',\\n    'fullscreen',\\n  ],\\n  settings: ['captions', 'quality', 'speed'],\\n\\n  // Localisation\\n  i18n: {\\n    restart: 'Restart',\\n    rewind: 'Rewind {seektime}s',\\n    play: 'Play',\\n    pause: 'Pause',\\n    fastForward: 'Forward {seektime}s',\\n    seek: 'Seek',\\n    seekLabel: '{currentTime} of {duration}',\\n    played: 'Played',\\n    buffered: 'Buffered',\\n    currentTime: 'Current time',\\n    duration: 'Duration',\\n    volume: 'Volume',\\n    mute: 'Mute',\\n    unmute: 'Unmute',\\n    enableCaptions: 'Enable captions',\\n    disableCaptions: 'Disable captions',\\n    download: 'Download',\\n    enterFullscreen: 'Enter fullscreen',\\n    exitFullscreen: 'Exit fullscreen',\\n    frameTitle: 'Player for {title}',\\n    captions: 'Captions',\\n    settings: 'Settings',\\n    pip: 'PIP',\\n    menuBack: 'Go back to previous menu',\\n    speed: 'Speed',\\n    normal: 'Normal',\\n    quality: 'Quality',\\n    loop: 'Loop',\\n    start: 'Start',\\n    end: 'End',\\n    all: 'All',\\n    reset: 'Reset',\\n    disabled: 'Disabled',\\n    enabled: 'Enabled',\\n    advertisement: 'Ad',\\n    qualityBadge: {\\n      2160: '4K',\\n      1440: 'HD',\\n      1080: 'HD',\\n      720: 'HD',\\n      576: 'SD',\\n      480: 'SD',\\n    },\\n  },\\n\\n  // URLs\\n  urls: null,\\n\\n  // Custom control listeners\\n  listeners: {\\n    seek: null,\\n    play: null,\\n    pause: null,\\n    restart: null,\\n    rewind: null,\\n    fastForward: null,\\n    mute: null,\\n    volume: null,\\n    captions: null,\\n    download: null,\\n    fullscreen: null,\\n    pip: null,\\n    airplay: null,\\n    speed: null,\\n    quality: null,\\n    loop: null,\\n    language: null,\\n  },\\n\\n  // Events to watch and bubble\\n  events: [\\n    // Events to watch on HTML5 media elements and bubble\\n    // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events\\n    'ended',\\n    'progress',\\n    'stalled',\\n    'playing',\\n    'waiting',\\n    'canplay',\\n    'canplaythrough',\\n    'loadstart',\\n    'loadeddata',\\n    'loadedmetadata',\\n    'timeupdate',\\n    'volumechange',\\n    'play',\\n    'pause',\\n    'error',\\n    'seeking',\\n    'seeked',\\n    'emptied',\\n    'ratechange',\\n    'cuechange',\\n\\n    // Custom events\\n    'download',\\n    'enterfullscreen',\\n    'exitfullscreen',\\n    'captionsenabled',\\n    'captionsdisabled',\\n    'languagechange',\\n    'controlshidden',\\n    'controlsshown',\\n    'ready',\\n\\n    // YouTube\\n    'statechange',\\n\\n    // Quality\\n    'qualitychange',\\n\\n    // Ads\\n    'adsloaded',\\n    'adscontentpause',\\n    'adscontentresume',\\n    'adstarted',\\n    'adsmidpoint',\\n    'adscomplete',\\n    'adsallcomplete',\\n    'adsimpression',\\n    'adsclick',\\n  ],\\n\\n  // Selectors\\n  // Change these to match your template if using custom HTML\\n  selectors: {\\n    editable: 'input, textarea, select, [contenteditable]',\\n    container: '.plyr',\\n    controls: {\\n      container: null,\\n      wrapper: '.plyr__controls',\\n    },\\n    labels: '[data-plyr]',\\n    buttons: {\\n      play: '[data-plyr=\\\"play\\\"]',\\n      pause: '[data-plyr=\\\"pause\\\"]',\\n      restart: '[data-plyr=\\\"restart\\\"]',\\n      rewind: '[data-plyr=\\\"rewind\\\"]',\\n      fastForward: '[data-plyr=\\\"fast-forward\\\"]',\\n      mute: '[data-plyr=\\\"mute\\\"]',\\n      captions: '[data-plyr=\\\"captions\\\"]',\\n      download: '[data-plyr=\\\"download\\\"]',\\n      fullscreen: '[data-plyr=\\\"fullscreen\\\"]',\\n      pip: '[data-plyr=\\\"pip\\\"]',\\n      airplay: '[data-plyr=\\\"airplay\\\"]',\\n      settings: '[data-plyr=\\\"settings\\\"]',\\n      loop: '[data-plyr=\\\"loop\\\"]',\\n    },\\n    inputs: {\\n      seek: '[data-plyr=\\\"seek\\\"]',\\n      volume: '[data-plyr=\\\"volume\\\"]',\\n      speed: '[data-plyr=\\\"speed\\\"]',\\n      language: '[data-plyr=\\\"language\\\"]',\\n      quality: '[data-plyr=\\\"quality\\\"]',\\n    },\\n    display: {\\n      currentTime: '.plyr__time--current',\\n      duration: '.plyr__time--duration',\\n      buffer: '.plyr__progress__buffer',\\n      loop: '.plyr__progress__loop', // Used later\\n      volume: '.plyr__volume--display',\\n    },\\n    progress: '.plyr__progress',\\n    captions: '.plyr__captions',\\n    caption: '.plyr__caption',\\n  },\\n\\n  // Class hooks added to the player in different states\\n  classNames: {\\n    type: 'plyr--{0}',\\n    provider: 'plyr--{0}',\\n    video: 'plyr__video-wrapper',\\n    embed: 'plyr__video-embed',\\n    videoFixedRatio: 'plyr__video-wrapper--fixed-ratio',\\n    embedContainer: 'plyr__video-embed__container',\\n    poster: 'plyr__poster',\\n    posterEnabled: 'plyr__poster-enabled',\\n    ads: 'plyr__ads',\\n    control: 'plyr__control',\\n    controlPressed: 'plyr__control--pressed',\\n    playing: 'plyr--playing',\\n    paused: 'plyr--paused',\\n    stopped: 'plyr--stopped',\\n    loading: 'plyr--loading',\\n    hover: 'plyr--hover',\\n    tooltip: 'plyr__tooltip',\\n    cues: 'plyr__cues',\\n    marker: 'plyr__progress__marker',\\n    hidden: 'plyr__sr-only',\\n    hideControls: 'plyr--hide-controls',\\n    isTouch: 'plyr--is-touch',\\n    uiSupported: 'plyr--full-ui',\\n    noTransition: 'plyr--no-transition',\\n    display: {\\n      time: 'plyr__time',\\n    },\\n    menu: {\\n      value: 'plyr__menu__value',\\n      badge: 'plyr__badge',\\n      open: 'plyr--menu-open',\\n    },\\n    captions: {\\n      enabled: 'plyr--captions-enabled',\\n      active: 'plyr--captions-active',\\n    },\\n    fullscreen: {\\n      enabled: 'plyr--fullscreen-enabled',\\n      fallback: 'plyr--fullscreen-fallback',\\n    },\\n    pip: {\\n      supported: 'plyr--pip-supported',\\n      active: 'plyr--pip-active',\\n    },\\n    airplay: {\\n      supported: 'plyr--airplay-supported',\\n      active: 'plyr--airplay-active',\\n    },\\n    previewThumbnails: {\\n      // Tooltip thumbs\\n      thumbContainer: 'plyr__preview-thumb',\\n      thumbContainerShown: 'plyr__preview-thumb--is-shown',\\n      imageContainer: 'plyr__preview-thumb__image-container',\\n      timeContainer: 'plyr__preview-thumb__time-container',\\n      // Scrubbing\\n      scrubbingContainer: 'plyr__preview-scrubbing',\\n      scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown',\\n    },\\n  },\\n\\n  // Embed attributes\\n  attributes: {\\n    embed: {\\n      provider: 'data-plyr-provider',\\n      id: 'data-plyr-embed-id',\\n      hash: 'data-plyr-embed-hash',\\n    },\\n  },\\n\\n  // Advertisements plugin\\n  // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio\\n  ads: {\\n    enabled: false,\\n    publisherId: '',\\n    tagUrl: '',\\n  },\\n\\n  // Preview Thumbnails plugin\\n  previewThumbnails: {\\n    enabled: false,\\n    src: '',\\n  },\\n\\n  // Vimeo plugin\\n  vimeo: {\\n    byline: false,\\n    portrait: false,\\n    title: false,\\n    speed: true,\\n    transparent: false,\\n    // Custom settings from Plyr\\n    customControls: true,\\n    referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy\\n    // Whether the owner of the video has a Pro or Business account\\n    // (which allows us to properly hide controls without CSS hacks, etc)\\n    premium: false,\\n  },\\n\\n  // YouTube plugin\\n  youtube: {\\n    rel: 0, // No related vids\\n    showinfo: 0, // Hide info\\n    iv_load_policy: 3, // Hide annotations\\n    modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)\\n    // Custom settings from Plyr\\n    customControls: true,\\n    noCookie: false, // Whether to use an alternative version of YouTube without cookies\\n  },\\n\\n  // Media Metadata\\n  mediaMetadata: {\\n    title: '',\\n    artist: '',\\n    album: '',\\n    artwork: [],\\n  },\\n\\n  // Markers\\n  markers: {\\n    enabled: false,\\n    points: [],\\n  },\\n};\\n\\nexport default defaults;\\n\",\"// ==========================================================================\\n// Plyr states\\n// ==========================================================================\\n\\nexport const pip = {\\n  active: 'picture-in-picture',\\n  inactive: 'inline',\\n};\\n\\nexport default { pip };\\n\",\"// ==========================================================================\\n// Plyr supported types and providers\\n// ==========================================================================\\n\\nexport const providers = {\\n  html5: 'html5',\\n  youtube: 'youtube',\\n  vimeo: 'vimeo',\\n};\\n\\nexport const types = {\\n  audio: 'audio',\\n  video: 'video',\\n};\\n\\n/**\\n * Get provider by URL\\n * @param {String} url\\n */\\nexport function getProviderByUrl(url) {\\n  // YouTube\\n  if (/^(https?:\\\\/\\\\/)?(www\\\\.)?(youtube\\\\.com|youtube-nocookie\\\\.com|youtu\\\\.?be)\\\\/.+$/.test(url)) {\\n    return providers.youtube;\\n  }\\n\\n  // Vimeo\\n  if (/^https?:\\\\/\\\\/player.vimeo.com\\\\/video\\\\/\\\\d{0,9}(?=\\\\b|\\\\/)/.test(url)) {\\n    return providers.vimeo;\\n  }\\n\\n  return null;\\n}\\n\\nexport default { providers, types };\\n\",\"// ==========================================================================\\n// Console wrapper\\n// ==========================================================================\\n\\nconst noop = () => {};\\n\\nexport default class Console {\\n  constructor(enabled = false) {\\n    this.enabled = window.console && enabled;\\n\\n    if (this.enabled) {\\n      this.log('Debugging enabled');\\n    }\\n  }\\n\\n  get log() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.log, console) : noop;\\n  }\\n\\n  get warn() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop;\\n  }\\n\\n  get error() {\\n    // eslint-disable-next-line no-console\\n    return this.enabled ? Function.prototype.bind.call(console.error, console) : noop;\\n  }\\n}\\n\",\"// ==========================================================================\\n// Fullscreen wrapper\\n// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing\\n// https://webkit.org/blog/7929/designing-websites-for-iphone-x/\\n// ==========================================================================\\n\\nimport browser from './utils/browser';\\nimport { closest, getElements, hasClass, toggleClass } from './utils/elements';\\nimport { on, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\n\\nclass Fullscreen {\\n  constructor(player) {\\n    // Keep reference to parent\\n    this.player = player;\\n\\n    // Get prefix\\n    this.prefix = Fullscreen.prefix;\\n    this.property = Fullscreen.property;\\n\\n    // Scroll position\\n    this.scrollPosition = { x: 0, y: 0 };\\n\\n    // Force the use of 'full window/browser' rather than fullscreen\\n    this.forceFallback = player.config.fullscreen.fallback === 'force';\\n\\n    // Get the fullscreen element\\n    // Checks container is an ancestor, defaults to null\\n    this.player.elements.fullscreen =\\n      player.config.fullscreen.container && closest(this.player.elements.container, player.config.fullscreen.container);\\n\\n    // Register event listeners\\n    // Handle event (incase user presses escape etc)\\n    on.call(\\n      this.player,\\n      document,\\n      this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`,\\n      () => {\\n        // TODO: Filter for target??\\n        this.onChange();\\n      },\\n    );\\n\\n    // Fullscreen toggle on double click\\n    on.call(this.player, this.player.elements.container, 'dblclick', (event) => {\\n      // Ignore double click in controls\\n      if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) {\\n        return;\\n      }\\n\\n      this.player.listeners.proxy(event, this.toggle, 'fullscreen');\\n    });\\n\\n    // Tap focus when in fullscreen\\n    on.call(this, this.player.elements.container, 'keydown', (event) => this.trapFocus(event));\\n\\n    // Update the UI\\n    this.update();\\n  }\\n\\n  // Determine if native supported\\n  static get nativeSupported() {\\n    return !!(\\n      document.fullscreenEnabled ||\\n      document.webkitFullscreenEnabled ||\\n      document.mozFullScreenEnabled ||\\n      document.msFullscreenEnabled\\n    );\\n  }\\n\\n  // If we're actually using native\\n  get useNative() {\\n    return Fullscreen.nativeSupported && !this.forceFallback;\\n  }\\n\\n  // Get the prefix for handlers\\n  static get prefix() {\\n    // No prefix\\n    if (is.function(document.exitFullscreen)) return '';\\n\\n    // Check for fullscreen support by vendor prefix\\n    let value = '';\\n    const prefixes = ['webkit', 'moz', 'ms'];\\n\\n    prefixes.some((pre) => {\\n      if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) {\\n        value = pre;\\n        return true;\\n      }\\n\\n      return false;\\n    });\\n\\n    return value;\\n  }\\n\\n  static get property() {\\n    return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';\\n  }\\n\\n  // Determine if fullscreen is supported\\n  get supported() {\\n    return [\\n      // Fullscreen is enabled in config\\n      this.player.config.fullscreen.enabled,\\n      // Must be a video\\n      this.player.isVideo,\\n      // Either native is supported or fallback enabled\\n      Fullscreen.nativeSupported || this.player.config.fullscreen.fallback,\\n      // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline\\n      // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback\\n      !this.player.isYouTube ||\\n        Fullscreen.nativeSupported ||\\n        !browser.isIos ||\\n        (this.player.config.playsinline && !this.player.config.fullscreen.iosNative),\\n    ].every(Boolean);\\n  }\\n\\n  // Get active state\\n  get active() {\\n    if (!this.supported) return false;\\n\\n    // Fallback using classname\\n    if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      return hasClass(this.target, this.player.config.classNames.fullscreen.fallback);\\n    }\\n\\n    const element = !this.prefix\\n      ? this.target.getRootNode().fullscreenElement\\n      : this.target.getRootNode()[`${this.prefix}${this.property}Element`];\\n\\n    return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target;\\n  }\\n\\n  // Get target element\\n  get target() {\\n    return browser.isIos && this.player.config.fullscreen.iosNative\\n      ? this.player.media\\n      : this.player.elements.fullscreen ?? this.player.elements.container;\\n  }\\n\\n  onChange = () => {\\n    if (!this.supported) return;\\n\\n    // Update toggle button\\n    const button = this.player.elements.buttons.fullscreen;\\n    if (is.element(button)) {\\n      button.pressed = this.active;\\n    }\\n\\n    // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up\\n    const target = this.target === this.player.media ? this.target : this.player.elements.container;\\n    // Trigger an event\\n    triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);\\n  };\\n\\n  toggleFallback = (toggle = false) => {\\n    // Store or restore scroll position\\n    if (toggle) {\\n      this.scrollPosition = {\\n        x: window.scrollX ?? 0,\\n        y: window.scrollY ?? 0,\\n      };\\n    } else {\\n      window.scrollTo(this.scrollPosition.x, this.scrollPosition.y);\\n    }\\n\\n    // Toggle scroll\\n    document.body.style.overflow = toggle ? 'hidden' : '';\\n\\n    // Toggle class hook\\n    toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle);\\n\\n    // Force full viewport on iPhone X+\\n    if (browser.isIos) {\\n      let viewport = document.head.querySelector('meta[name=\\\"viewport\\\"]');\\n      const property = 'viewport-fit=cover';\\n\\n      // Inject the viewport meta if required\\n      if (!viewport) {\\n        viewport = document.createElement('meta');\\n        viewport.setAttribute('name', 'viewport');\\n      }\\n\\n      // Check if the property already exists\\n      const hasProperty = is.string(viewport.content) && viewport.content.includes(property);\\n\\n      if (toggle) {\\n        this.cleanupViewport = !hasProperty;\\n        if (!hasProperty) viewport.content += `,${property}`;\\n      } else if (this.cleanupViewport) {\\n        viewport.content = viewport.content\\n          .split(',')\\n          .filter((part) => part.trim() !== property)\\n          .join(',');\\n      }\\n    }\\n\\n    // Toggle button and fire events\\n    this.onChange();\\n  };\\n\\n  // Trap focus inside container\\n  trapFocus = (event) => {\\n    // Bail if iOS/iPadOS, not active, not the tab key\\n    if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return;\\n\\n    // Get the current focused element\\n    const focused = document.activeElement;\\n    const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]');\\n    const [first] = focusable;\\n    const last = focusable[focusable.length - 1];\\n\\n    if (focused === last && !event.shiftKey) {\\n      // Move focus to first element that can be tabbed if Shift isn't used\\n      first.focus();\\n      event.preventDefault();\\n    } else if (focused === first && event.shiftKey) {\\n      // Move focus to last element that can be tabbed if Shift is used\\n      last.focus();\\n      event.preventDefault();\\n    }\\n  };\\n\\n  // Update UI\\n  update = () => {\\n    if (this.supported) {\\n      let mode;\\n\\n      if (this.forceFallback) mode = 'Fallback (forced)';\\n      else if (Fullscreen.nativeSupported) mode = 'Native';\\n      else mode = 'Fallback';\\n\\n      this.player.debug.log(`${mode} fullscreen enabled`);\\n    } else {\\n      this.player.debug.log('Fullscreen not supported and fallback disabled');\\n    }\\n\\n    // Add styling hook to show button\\n    toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported);\\n  };\\n\\n  // Make an element fullscreen\\n  enter = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen doesn't need the request step\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.requestFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(true);\\n    } else if (!this.prefix) {\\n      this.target.requestFullscreen({ navigationUI: 'hide' });\\n    } else if (!is.empty(this.prefix)) {\\n      this.target[`${this.prefix}Request${this.property}`]();\\n    }\\n  };\\n\\n  // Bail from fullscreen\\n  exit = () => {\\n    if (!this.supported) return;\\n\\n    // iOS native fullscreen\\n    if (browser.isIos && this.player.config.fullscreen.iosNative) {\\n      if (this.player.isVimeo) {\\n        this.player.embed.exitFullscreen();\\n      } else {\\n        this.target.webkitEnterFullscreen();\\n      }\\n      silencePromise(this.player.play());\\n    } else if (!Fullscreen.nativeSupported || this.forceFallback) {\\n      this.toggleFallback(false);\\n    } else if (!this.prefix) {\\n      (document.cancelFullScreen || document.exitFullscreen).call(document);\\n    } else if (!is.empty(this.prefix)) {\\n      const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';\\n      document[`${this.prefix}${action}${this.property}`]();\\n    }\\n  };\\n\\n  // Toggle state\\n  toggle = () => {\\n    if (!this.active) this.enter();\\n    else this.exit();\\n  };\\n}\\n\\nexport default Fullscreen;\\n\",\"// ==========================================================================\\n// Load image avoiding xhr/fetch CORS issues\\n// Server status can't be obtained this way unfortunately, so this uses \\\"naturalWidth\\\" to determine if the image has loaded\\n// By default it checks if it is at least 1px, but you can add a second argument to change this\\n// ==========================================================================\\n\\nexport default function loadImage(src, minWidth = 1) {\\n  return new Promise((resolve, reject) => {\\n    const image = new Image();\\n\\n    const handler = () => {\\n      delete image.onload;\\n      delete image.onerror;\\n      (image.naturalWidth >= minWidth ? resolve : reject)(image);\\n    };\\n\\n    Object.assign(image, { onload: handler, onerror: handler, src });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Plyr UI\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport controls from './controls';\\nimport support from './support';\\nimport { getElement, toggleClass } from './utils/elements';\\nimport { ready, triggerEvent } from './utils/events';\\nimport i18n from './utils/i18n';\\nimport is from './utils/is';\\nimport loadImage from './utils/load-image';\\n\\nconst ui = {\\n  addStyleHook() {\\n    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);\\n    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);\\n  },\\n\\n  // Toggle native HTML5 media controls\\n  toggleNativeControls(toggle = false) {\\n    if (toggle && this.isHTML5) {\\n      this.media.setAttribute('controls', '');\\n    } else {\\n      this.media.removeAttribute('controls');\\n    }\\n  },\\n\\n  // Setup the UI\\n  build() {\\n    // Re-attach media element listeners\\n    // TODO: Use event bubbling?\\n    this.listeners.media();\\n\\n    // Don't setup interface if no support\\n    if (!this.supported.ui) {\\n      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);\\n\\n      // Restore native controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Bail\\n      return;\\n    }\\n\\n    // Inject custom controls if not present\\n    if (!is.element(this.elements.controls)) {\\n      // Inject custom controls\\n      controls.inject.call(this);\\n\\n      // Re-attach control listeners\\n      this.listeners.controls();\\n    }\\n\\n    // Remove native controls\\n    ui.toggleNativeControls.call(this);\\n\\n    // Setup captions for HTML5\\n    if (this.isHTML5) {\\n      captions.setup.call(this);\\n    }\\n\\n    // Reset volume\\n    this.volume = null;\\n\\n    // Reset mute state\\n    this.muted = null;\\n\\n    // Reset loop state\\n    this.loop = null;\\n\\n    // Reset quality setting\\n    this.quality = null;\\n\\n    // Reset speed\\n    this.speed = null;\\n\\n    // Reset volume display\\n    controls.updateVolume.call(this);\\n\\n    // Reset time display\\n    controls.timeUpdate.call(this);\\n\\n    // Reset duration display\\n    controls.durationUpdate.call(this);\\n\\n    // Update the UI\\n    ui.checkPlaying.call(this);\\n\\n    // Check for picture-in-picture support\\n    toggleClass(\\n      this.elements.container,\\n      this.config.classNames.pip.supported,\\n      support.pip && this.isHTML5 && this.isVideo,\\n    );\\n\\n    // Check for airplay support\\n    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);\\n\\n    // Add touch class\\n    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);\\n\\n    // Ready for API calls\\n    this.ready = true;\\n\\n    // Ready event at end of execution stack\\n    setTimeout(() => {\\n      triggerEvent.call(this, this.media, 'ready');\\n    }, 0);\\n\\n    // Set the title\\n    ui.setTitle.call(this);\\n\\n    // Assure the poster image is set, if the property was added before the element was created\\n    if (this.poster) {\\n      ui.setPoster.call(this, this.poster, false).catch(() => {});\\n    }\\n\\n    // Manually set the duration if user has overridden it.\\n    // The event listeners for it doesn't get called if preload is disabled (#701)\\n    if (this.config.duration) {\\n      controls.durationUpdate.call(this);\\n    }\\n\\n    // Media metadata\\n    if (this.config.mediaMetadata) {\\n      controls.setMediaMetadata.call(this);\\n    }\\n  },\\n\\n  // Setup aria attribute for play and iframe title\\n  setTitle() {\\n    // Find the current text\\n    let label = i18n.get('play', this.config);\\n\\n    // If there's a media title set, use that for the label\\n    if (is.string(this.config.title) && !is.empty(this.config.title)) {\\n      label += `, ${this.config.title}`;\\n    }\\n\\n    // If there's a play button, set label\\n    Array.from(this.elements.buttons.play || []).forEach((button) => {\\n      button.setAttribute('aria-label', label);\\n    });\\n\\n    // Set iframe title\\n    // https://github.com/sampotts/plyr/issues/124\\n    if (this.isEmbed) {\\n      const iframe = getElement.call(this, 'iframe');\\n\\n      if (!is.element(iframe)) {\\n        return;\\n      }\\n\\n      // Default to media type\\n      const title = !is.empty(this.config.title) ? this.config.title : 'video';\\n      const format = i18n.get('frameTitle', this.config);\\n\\n      iframe.setAttribute('title', format.replace('{title}', title));\\n    }\\n  },\\n\\n  // Toggle poster\\n  togglePoster(enable) {\\n    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);\\n  },\\n\\n  // Set the poster image (async)\\n  // Used internally for the poster setter, with the passive option forced to false\\n  setPoster(poster, passive = true) {\\n    // Don't override if call is passive\\n    if (passive && this.poster) {\\n      return Promise.reject(new Error('Poster already set'));\\n    }\\n\\n    // Set property synchronously to respect the call order\\n    this.media.setAttribute('data-poster', poster);\\n\\n    // Show the poster\\n    this.elements.poster.removeAttribute('hidden');\\n\\n    // Wait until ui is ready\\n    return (\\n      ready\\n        .call(this)\\n        // Load image\\n        .then(() => loadImage(poster))\\n        .catch((error) => {\\n          // Hide poster on error unless it's been set by another call\\n          if (poster === this.poster) {\\n            ui.togglePoster.call(this, false);\\n          }\\n          // Rethrow\\n          throw error;\\n        })\\n        .then(() => {\\n          // Prevent race conditions\\n          if (poster !== this.poster) {\\n            throw new Error('setPoster cancelled by later call to setPoster');\\n          }\\n        })\\n        .then(() => {\\n          Object.assign(this.elements.poster.style, {\\n            backgroundImage: `url('${poster}')`,\\n            // Reset backgroundSize as well (since it can be set to \\\"cover\\\" for padded thumbnails for youtube)\\n            backgroundSize: '',\\n          });\\n\\n          ui.togglePoster.call(this, true);\\n\\n          return poster;\\n        })\\n    );\\n  },\\n\\n  // Check playing state\\n  checkPlaying(event) {\\n    // Class hooks\\n    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);\\n    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);\\n    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);\\n\\n    // Set state\\n    Array.from(this.elements.buttons.play || []).forEach((target) => {\\n      Object.assign(target, { pressed: this.playing });\\n      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));\\n    });\\n\\n    // Only update controls on non timeupdate events\\n    if (is.event(event) && event.type === 'timeupdate') {\\n      return;\\n    }\\n\\n    // Toggle controls\\n    ui.toggleControls.call(this);\\n  },\\n\\n  // Check if media is loading\\n  checkLoading(event) {\\n    this.loading = ['stalled', 'waiting'].includes(event.type);\\n\\n    // Clear timer\\n    clearTimeout(this.timers.loading);\\n\\n    // Timer to prevent flicker when seeking\\n    this.timers.loading = setTimeout(\\n      () => {\\n        // Update progress bar loading class state\\n        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);\\n\\n        // Update controls visibility\\n        ui.toggleControls.call(this);\\n      },\\n      this.loading ? 250 : 0,\\n    );\\n  },\\n\\n  // Toggle controls based on state and `force` argument\\n  toggleControls(force) {\\n    const { controls: controlsElement } = this.elements;\\n\\n    if (controlsElement && this.config.hideControls) {\\n      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)\\n      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();\\n\\n      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide\\n      this.toggleControls(\\n        Boolean(\\n          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,\\n        ),\\n      );\\n    }\\n  },\\n\\n  // Migrate any custom properties from the media to the parent\\n  migrateStyles() {\\n    // Loop through values (as they are the keys when the object is spread 🤔)\\n    Object.values({ ...this.media.style })\\n      // We're only fussed about Plyr specific properties\\n      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))\\n      .forEach((key) => {\\n        // Set on the container\\n        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));\\n\\n        // Clean up from media element\\n        this.media.style.removeProperty(key);\\n      });\\n\\n    // Remove attribute if empty\\n    if (is.empty(this.media.style)) {\\n      this.media.removeAttribute('style');\\n    }\\n  },\\n};\\n\\nexport default ui;\\n\",\"// ==========================================================================\\n// Plyr Event Listeners\\n// ==========================================================================\\n\\nimport controls from './controls';\\nimport ui from './ui';\\nimport { repaint } from './utils/animation';\\nimport browser from './utils/browser';\\nimport { getElement, getElements, matches, toggleClass } from './utils/elements';\\nimport { off, on, once, toggleListener, triggerEvent } from './utils/events';\\nimport is from './utils/is';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, getViewportSize, supportsCSS } from './utils/style';\\n\\nclass Listeners {\\n  constructor(player) {\\n    this.player = player;\\n    this.lastKey = null;\\n    this.focusTimer = null;\\n    this.lastKeyDown = null;\\n\\n    this.handleKey = this.handleKey.bind(this);\\n    this.toggleMenu = this.toggleMenu.bind(this);\\n    this.firstTouch = this.firstTouch.bind(this);\\n  }\\n\\n  // Handle key presses\\n  handleKey(event) {\\n    const { player } = this;\\n    const { elements } = player;\\n    const { key, type, altKey, ctrlKey, metaKey, shiftKey } = event;\\n    const pressed = type === 'keydown';\\n    const repeat = pressed && key === this.lastKey;\\n\\n    // Bail if a modifier key is set\\n    if (altKey || ctrlKey || metaKey || shiftKey) {\\n      return;\\n    }\\n\\n    // If the event is bubbled from the media element\\n    // Firefox doesn't get the key for whatever reason\\n    if (!key) {\\n      return;\\n    }\\n\\n    // Seek by increment\\n    const seekByIncrement = (increment) => {\\n      // Divide the max duration into 10th's and times by the number value\\n      player.currentTime = (player.duration / 10) * increment;\\n    };\\n\\n    // Handle the key on keydown\\n    // Reset on keyup\\n    if (pressed) {\\n      // Check focused element\\n      // and if the focused element is not editable (e.g. text input)\\n      // and any that accept key input http://webaim.org/techniques/keyboard/\\n      const focused = document.activeElement;\\n      if (is.element(focused)) {\\n        const { editable } = player.config.selectors;\\n        const { seek } = elements.inputs;\\n\\n        if (focused !== seek && matches(focused, editable)) {\\n          return;\\n        }\\n\\n        if (event.key === ' ' && matches(focused, 'button, [role^=\\\"menuitem\\\"]')) {\\n          return;\\n        }\\n      }\\n\\n      // Which keys should we prevent default\\n      const preventDefault = [\\n        ' ',\\n        'ArrowLeft',\\n        'ArrowUp',\\n        'ArrowRight',\\n        'ArrowDown',\\n\\n        '0',\\n        '1',\\n        '2',\\n        '3',\\n        '4',\\n        '5',\\n        '6',\\n        '7',\\n        '8',\\n        '9',\\n\\n        'c',\\n        'f',\\n        'k',\\n        'l',\\n        'm',\\n      ];\\n\\n      // If the key is found prevent default (e.g. prevent scrolling for arrows)\\n      if (preventDefault.includes(key)) {\\n        event.preventDefault();\\n        event.stopPropagation();\\n      }\\n\\n      switch (key) {\\n        case '0':\\n        case '1':\\n        case '2':\\n        case '3':\\n        case '4':\\n        case '5':\\n        case '6':\\n        case '7':\\n        case '8':\\n        case '9':\\n          if (!repeat) {\\n            seekByIncrement(parseInt(key, 10));\\n          }\\n          break;\\n\\n        case ' ':\\n        case 'k':\\n          if (!repeat) {\\n            silencePromise(player.togglePlay());\\n          }\\n          break;\\n\\n        case 'ArrowUp':\\n          player.increaseVolume(0.1);\\n          break;\\n\\n        case 'ArrowDown':\\n          player.decreaseVolume(0.1);\\n          break;\\n\\n        case 'm':\\n          if (!repeat) {\\n            player.muted = !player.muted;\\n          }\\n          break;\\n\\n        case 'ArrowRight':\\n          player.forward();\\n          break;\\n\\n        case 'ArrowLeft':\\n          player.rewind();\\n          break;\\n\\n        case 'f':\\n          player.fullscreen.toggle();\\n          break;\\n\\n        case 'c':\\n          if (!repeat) {\\n            player.toggleCaptions();\\n          }\\n          break;\\n\\n        case 'l':\\n          player.loop = !player.loop;\\n          break;\\n\\n        default:\\n          break;\\n      }\\n\\n      // Escape is handle natively when in full screen\\n      // So we only need to worry about non native\\n      if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) {\\n        player.fullscreen.toggle();\\n      }\\n\\n      // Store last key for next cycle\\n      this.lastKey = key;\\n    } else {\\n      this.lastKey = null;\\n    }\\n  }\\n\\n  // Toggle menu\\n  toggleMenu(event) {\\n    controls.toggleMenu.call(this.player, event);\\n  }\\n\\n  // Device is touch enabled\\n  firstTouch = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    player.touch = true;\\n\\n    // Add touch class\\n    toggleClass(elements.container, player.config.classNames.isTouch, true);\\n  };\\n\\n  // Global window & document listeners\\n  global = (toggle = true) => {\\n    const { player } = this;\\n\\n    // Keyboard shortcuts\\n    if (player.config.keyboard.global) {\\n      toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false);\\n    }\\n\\n    // Click anywhere closes menu\\n    toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle);\\n\\n    // Detect touch by events\\n    once.call(player, document.body, 'touchstart', this.firstTouch);\\n  };\\n\\n  // Container listeners\\n  container = () => {\\n    const { player } = this;\\n    const { config, elements, timers } = player;\\n\\n    // Keyboard shortcuts\\n    if (!config.keyboard.global && config.keyboard.focused) {\\n      on.call(player, elements.container, 'keydown keyup', this.handleKey, false);\\n    }\\n\\n    // Toggle controls on mouse events and entering fullscreen\\n    on.call(\\n      player,\\n      elements.container,\\n      'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen',\\n      (event) => {\\n        const { controls: controlsElement } = elements;\\n\\n        // Remove button states for fullscreen\\n        if (controlsElement && event.type === 'enterfullscreen') {\\n          controlsElement.pressed = false;\\n          controlsElement.hover = false;\\n        }\\n\\n        // Show, then hide after a timeout unless another control event occurs\\n        const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type);\\n        let delay = 0;\\n\\n        if (show) {\\n          ui.toggleControls.call(player, true);\\n          // Use longer timeout for touch devices\\n          delay = player.touch ? 3000 : 2000;\\n        }\\n\\n        // Clear timer\\n        clearTimeout(timers.controls);\\n\\n        // Set new timer to prevent flicker when seeking\\n        timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n      },\\n    );\\n\\n    // Set a gutter for Vimeo\\n    const setGutter = () => {\\n      if (!player.isVimeo || player.config.vimeo.premium) {\\n        return;\\n      }\\n\\n      const target = elements.wrapper;\\n      const { active } = player.fullscreen;\\n      const [videoWidth, videoHeight] = getAspectRatio.call(player);\\n      const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`);\\n\\n      // If not active, remove styles\\n      if (!active) {\\n        if (useNativeAspectRatio) {\\n          target.style.width = null;\\n          target.style.height = null;\\n        } else {\\n          target.style.maxWidth = null;\\n          target.style.margin = null;\\n        }\\n        return;\\n      }\\n\\n      // Determine which dimension will overflow and constrain view\\n      const [viewportWidth, viewportHeight] = getViewportSize();\\n      const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight;\\n\\n      if (useNativeAspectRatio) {\\n        target.style.width = overflow ? 'auto' : '100%';\\n        target.style.height = overflow ? '100%' : 'auto';\\n      } else {\\n        target.style.maxWidth = overflow ? `${(viewportHeight / videoHeight) * videoWidth}px` : null;\\n        target.style.margin = overflow ? '0 auto' : null;\\n      }\\n    };\\n\\n    // Handle resizing\\n    const resized = () => {\\n      clearTimeout(timers.resized);\\n      timers.resized = setTimeout(setGutter, 50);\\n    };\\n\\n    on.call(player, elements.container, 'enterfullscreen exitfullscreen', (event) => {\\n      const { target } = player.fullscreen;\\n\\n      // Ignore events not from target\\n      if (target !== elements.container) {\\n        return;\\n      }\\n\\n      // If it's not an embed and no ratio specified\\n      if (!player.isEmbed && is.empty(player.config.ratio)) {\\n        return;\\n      }\\n\\n      // Set Vimeo gutter\\n      setGutter();\\n\\n      // Watch for resizes\\n      const method = event.type === 'enterfullscreen' ? on : off;\\n      method.call(player, window, 'resize', resized);\\n    });\\n  };\\n\\n  // Listen for media events\\n  media = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n\\n    // Time change on media\\n    on.call(player, player.media, 'timeupdate seeking seeked', (event) => controls.timeUpdate.call(player, event));\\n\\n    // Display duration\\n    on.call(player, player.media, 'durationchange loadeddata loadedmetadata', (event) =>\\n      controls.durationUpdate.call(player, event),\\n    );\\n\\n    // Handle the media finishing\\n    on.call(player, player.media, 'ended', () => {\\n      // Show poster on end\\n      if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) {\\n        // Restart\\n        player.restart();\\n\\n        // Call pause otherwise IE11 will start playing the video again\\n        player.pause();\\n      }\\n    });\\n\\n    // Check for buffer progress\\n    on.call(player, player.media, 'progress playing seeking seeked', (event) =>\\n      controls.updateProgress.call(player, event),\\n    );\\n\\n    // Handle volume changes\\n    on.call(player, player.media, 'volumechange', (event) => controls.updateVolume.call(player, event));\\n\\n    // Handle play/pause\\n    on.call(player, player.media, 'playing play pause ended emptied timeupdate', (event) =>\\n      ui.checkPlaying.call(player, event),\\n    );\\n\\n    // Loading state\\n    on.call(player, player.media, 'waiting canplay seeked playing', (event) => ui.checkLoading.call(player, event));\\n\\n    // Click video\\n    if (player.supported.ui && player.config.clickToPlay && !player.isAudio) {\\n      // Re-fetch the wrapper\\n      const wrapper = getElement.call(player, `.${player.config.classNames.video}`);\\n\\n      // Bail if there's no wrapper (this should never happen)\\n      if (!is.element(wrapper)) {\\n        return;\\n      }\\n\\n      // On click play, pause or restart\\n      on.call(player, elements.container, 'click', (event) => {\\n        const targets = [elements.container, wrapper];\\n\\n        // Ignore if click if not container or in video wrapper\\n        if (!targets.includes(event.target) && !wrapper.contains(event.target)) {\\n          return;\\n        }\\n\\n        // Touch devices will just show controls (if hidden)\\n        if (player.touch && player.config.hideControls) {\\n          return;\\n        }\\n\\n        if (player.ended) {\\n          this.proxy(event, player.restart, 'restart');\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.play());\\n            },\\n            'play',\\n          );\\n        } else {\\n          this.proxy(\\n            event,\\n            () => {\\n              silencePromise(player.togglePlay());\\n            },\\n            'play',\\n          );\\n        }\\n      });\\n    }\\n\\n    // Disable right click\\n    if (player.supported.ui && player.config.disableContextMenu) {\\n      on.call(\\n        player,\\n        elements.wrapper,\\n        'contextmenu',\\n        (event) => {\\n          event.preventDefault();\\n        },\\n        false,\\n      );\\n    }\\n\\n    // Volume change\\n    on.call(player, player.media, 'volumechange', () => {\\n      // Save to storage\\n      player.storage.set({\\n        volume: player.volume,\\n        muted: player.muted,\\n      });\\n    });\\n\\n    // Speed change\\n    on.call(player, player.media, 'ratechange', () => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'speed');\\n\\n      // Save to storage\\n      player.storage.set({ speed: player.speed });\\n    });\\n\\n    // Quality change\\n    on.call(player, player.media, 'qualitychange', (event) => {\\n      // Update UI\\n      controls.updateSetting.call(player, 'quality', null, event.detail.quality);\\n    });\\n\\n    // Update download link when ready and if quality changes\\n    on.call(player, player.media, 'ready qualitychange', () => {\\n      controls.setDownloadUrl.call(player);\\n    });\\n\\n    // Proxy events to container\\n    // Bubble up key events for Edge\\n    const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' ');\\n\\n    on.call(player, player.media, proxyEvents, (event) => {\\n      let { detail = {} } = event;\\n\\n      // Get error details from media\\n      if (event.type === 'error') {\\n        detail = player.media.error;\\n      }\\n\\n      triggerEvent.call(player, elements.container, event.type, true, detail);\\n    });\\n  };\\n\\n  // Run default and custom handlers\\n  proxy = (event, defaultHandler, customHandlerKey) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n    let returned = true;\\n\\n    // Execute custom handler\\n    if (hasCustomHandler) {\\n      returned = customHandler.call(player, event);\\n    }\\n\\n    // Only call default handler if not prevented in custom handler\\n    if (returned !== false && is.function(defaultHandler)) {\\n      defaultHandler.call(player, event);\\n    }\\n  };\\n\\n  // Trigger custom and default handlers\\n  bind = (element, type, defaultHandler, customHandlerKey, passive = true) => {\\n    const { player } = this;\\n    const customHandler = player.config.listeners[customHandlerKey];\\n    const hasCustomHandler = is.function(customHandler);\\n\\n    on.call(\\n      player,\\n      element,\\n      type,\\n      (event) => this.proxy(event, defaultHandler, customHandlerKey),\\n      passive && !hasCustomHandler,\\n    );\\n  };\\n\\n  // Listen for control events\\n  controls = () => {\\n    const { player } = this;\\n    const { elements } = player;\\n    // IE doesn't support input event, so we fallback to change\\n    const inputEvent = browser.isIE ? 'change' : 'input';\\n\\n    // Play/pause toggle\\n    if (elements.buttons.play) {\\n      Array.from(elements.buttons.play).forEach((button) => {\\n        this.bind(\\n          button,\\n          'click',\\n          () => {\\n            silencePromise(player.togglePlay());\\n          },\\n          'play',\\n        );\\n      });\\n    }\\n\\n    // Pause\\n    this.bind(elements.buttons.restart, 'click', player.restart, 'restart');\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.rewind,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after rewind\\n        player.lastSeekTime = Date.now();\\n        player.rewind();\\n      },\\n      'rewind',\\n    );\\n\\n    // Rewind\\n    this.bind(\\n      elements.buttons.fastForward,\\n      'click',\\n      () => {\\n        // Record seek time so we can prevent hiding controls for a few seconds after fast forward\\n        player.lastSeekTime = Date.now();\\n        player.forward();\\n      },\\n      'fastForward',\\n    );\\n\\n    // Mute toggle\\n    this.bind(\\n      elements.buttons.mute,\\n      'click',\\n      () => {\\n        player.muted = !player.muted;\\n      },\\n      'mute',\\n    );\\n\\n    // Captions toggle\\n    this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions());\\n\\n    // Download\\n    this.bind(\\n      elements.buttons.download,\\n      'click',\\n      () => {\\n        triggerEvent.call(player, player.media, 'download');\\n      },\\n      'download',\\n    );\\n\\n    // Fullscreen toggle\\n    this.bind(\\n      elements.buttons.fullscreen,\\n      'click',\\n      () => {\\n        player.fullscreen.toggle();\\n      },\\n      'fullscreen',\\n    );\\n\\n    // Picture-in-Picture\\n    this.bind(\\n      elements.buttons.pip,\\n      'click',\\n      () => {\\n        player.pip = 'toggle';\\n      },\\n      'pip',\\n    );\\n\\n    // Airplay\\n    this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay');\\n\\n    // Settings menu - click toggle\\n    this.bind(\\n      elements.buttons.settings,\\n      'click',\\n      (event) => {\\n        // Prevent the document click listener closing the menu\\n        event.stopPropagation();\\n        event.preventDefault();\\n\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false,\\n    ); // Can't be passive as we're preventing default\\n\\n    // Settings menu - keyboard toggle\\n    // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus\\n    // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143\\n    this.bind(\\n      elements.buttons.settings,\\n      'keyup',\\n      (event) => {\\n        if (![' ', 'Enter'].includes(event.key)) {\\n          return;\\n        }\\n\\n        // Because return triggers a click anyway, all we need to do is set focus\\n        if (event.key === 'Enter') {\\n          controls.focusFirstMenuItem.call(player, null, true);\\n          return;\\n        }\\n\\n        // Prevent scroll\\n        event.preventDefault();\\n\\n        // Prevent playing video (Firefox)\\n        event.stopPropagation();\\n\\n        // Toggle menu\\n        controls.toggleMenu.call(player, event);\\n      },\\n      null,\\n      false, // Can't be passive as we're preventing default\\n    );\\n\\n    // Escape closes menu\\n    this.bind(elements.settings.menu, 'keydown', (event) => {\\n      if (event.key === 'Escape') {\\n        controls.toggleMenu.call(player, event);\\n      }\\n    });\\n\\n    // Set range input alternative \\\"value\\\", which matches the tooltip time (#954)\\n    this.bind(elements.inputs.seek, 'mousedown mousemove', (event) => {\\n      const rect = elements.progress.getBoundingClientRect();\\n      const percent = (100 / rect.width) * (event.pageX - rect.left);\\n      event.currentTarget.setAttribute('seek-value', percent);\\n    });\\n\\n    // Pause while seeking\\n    this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', (event) => {\\n      const seek = event.currentTarget;\\n      const attribute = 'play-on-seeked';\\n\\n      if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) {\\n        return;\\n      }\\n\\n      // Record seek time so we can prevent hiding controls for a few seconds after seek\\n      player.lastSeekTime = Date.now();\\n\\n      // Was playing before?\\n      const play = seek.hasAttribute(attribute);\\n      // Done seeking\\n      const done = ['mouseup', 'touchend', 'keyup'].includes(event.type);\\n\\n      // If we're done seeking and it was playing, resume playback\\n      if (play && done) {\\n        seek.removeAttribute(attribute);\\n        silencePromise(player.play());\\n      } else if (!done && player.playing) {\\n        seek.setAttribute(attribute, '');\\n        player.pause();\\n      }\\n    });\\n\\n    // Fix range inputs on iOS\\n    // Super weird iOS bug where after you interact with an <input type=\\\"range\\\">,\\n    // it takes over further interactions on the page. This is a hack\\n    if (browser.isIos) {\\n      const inputs = getElements.call(player, 'input[type=\\\"range\\\"]');\\n      Array.from(inputs).forEach((input) => this.bind(input, inputEvent, (event) => repaint(event.target)));\\n    }\\n\\n    // Seek\\n    this.bind(\\n      elements.inputs.seek,\\n      inputEvent,\\n      (event) => {\\n        const seek = event.currentTarget;\\n        // If it exists, use seek-value instead of \\\"value\\\" for consistency with tooltip time (#954)\\n        let seekTo = seek.getAttribute('seek-value');\\n\\n        if (is.empty(seekTo)) {\\n          seekTo = seek.value;\\n        }\\n\\n        seek.removeAttribute('seek-value');\\n\\n        player.currentTime = (seekTo / seek.max) * player.duration;\\n      },\\n      'seek',\\n    );\\n\\n    // Seek tooltip\\n    this.bind(elements.progress, 'mouseenter mouseleave mousemove', (event) =>\\n      controls.updateSeekTooltip.call(player, event),\\n    );\\n\\n    // Preview thumbnails plugin\\n    // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this\\n    this.bind(elements.progress, 'mousemove touchmove', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startMove(event);\\n      }\\n    });\\n\\n    // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering\\n    this.bind(elements.progress, 'mouseleave touchend click', () => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endMove(false, true);\\n      }\\n    });\\n\\n    // Show scrubbing preview\\n    this.bind(elements.progress, 'mousedown touchstart', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.startScrubbing(event);\\n      }\\n    });\\n\\n    this.bind(elements.progress, 'mouseup touchend', (event) => {\\n      const { previewThumbnails } = player;\\n\\n      if (previewThumbnails && previewThumbnails.loaded) {\\n        previewThumbnails.endScrubbing(event);\\n      }\\n    });\\n\\n    // Polyfill for lower fill in <input type=\\\"range\\\"> for webkit\\n    if (browser.isWebKit) {\\n      Array.from(getElements.call(player, 'input[type=\\\"range\\\"]')).forEach((element) => {\\n        this.bind(element, 'input', (event) => controls.updateRangeFill.call(player, event.target));\\n      });\\n    }\\n\\n    // Current time invert\\n    // Only if one time element is used for both currentTime and duration\\n    if (player.config.toggleInvert && !is.element(elements.display.duration)) {\\n      this.bind(elements.display.currentTime, 'click', () => {\\n        // Do nothing if we're at the start\\n        if (player.currentTime === 0) {\\n          return;\\n        }\\n\\n        player.config.invertTime = !player.config.invertTime;\\n\\n        controls.timeUpdate.call(player);\\n      });\\n    }\\n\\n    // Volume\\n    this.bind(\\n      elements.inputs.volume,\\n      inputEvent,\\n      (event) => {\\n        player.volume = event.target.value;\\n      },\\n      'volume',\\n    );\\n\\n    // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mouseenter mouseleave', (event) => {\\n      elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n    });\\n\\n    // Also update controls.hover state for any non-player children of fullscreen element (as above)\\n    if (elements.fullscreen) {\\n      Array.from(elements.fullscreen.children)\\n        .filter((c) => !c.contains(elements.container))\\n        .forEach((child) => {\\n          this.bind(child, 'mouseenter mouseleave', (event) => {\\n            if (elements.controls) {\\n              elements.controls.hover = !player.touch && event.type === 'mouseenter';\\n            }\\n          });\\n        });\\n    }\\n\\n    // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting)\\n    this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', (event) => {\\n      elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type);\\n    });\\n\\n    // Show controls when they receive focus (e.g., when using keyboard tab key)\\n    this.bind(elements.controls, 'focusin', () => {\\n      const { config, timers } = player;\\n\\n      // Skip transition to prevent focus from scrolling the parent element\\n      toggleClass(elements.controls, config.classNames.noTransition, true);\\n\\n      // Toggle\\n      ui.toggleControls.call(player, true);\\n\\n      // Restore transition\\n      setTimeout(() => {\\n        toggleClass(elements.controls, config.classNames.noTransition, false);\\n      }, 0);\\n\\n      // Delay a little more for mouse users\\n      const delay = this.touch ? 3000 : 4000;\\n\\n      // Clear timer\\n      clearTimeout(timers.controls);\\n\\n      // Hide again after delay\\n      timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay);\\n    });\\n\\n    // Mouse wheel for volume\\n    this.bind(\\n      elements.inputs.volume,\\n      'wheel',\\n      (event) => {\\n        // Detect \\\"natural\\\" scroll - supported on OS X Safari only\\n        // Other browsers on OS X will be inverted until support improves\\n        const inverted = event.webkitDirectionInvertedFromDevice;\\n        // Get delta from event. Invert if `inverted` is true\\n        const [x, y] = [event.deltaX, -event.deltaY].map((value) => (inverted ? -value : value));\\n        // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta)\\n        const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y);\\n\\n        // Change the volume by 2%\\n        player.increaseVolume(direction / 50);\\n\\n        // Don't break page scrolling at max and min\\n        const { volume } = player.media;\\n        if ((direction === 1 && volume < 1) || (direction === -1 && volume > 0)) {\\n          event.preventDefault();\\n        }\\n      },\\n      'volume',\\n      false,\\n    );\\n  };\\n}\\n\\nexport default Listeners;\\n\",\"(function(root, factory) {\\n  if (typeof define === 'function' && define.amd) {\\n    define([], factory);\\n  } else if (typeof exports === 'object') {\\n    module.exports = factory();\\n  } else {\\n    root.loadjs = factory();\\n  }\\n}(this, function() {\\n/**\\n * Global dependencies.\\n * @global {Object} document - DOM\\n */\\n\\nvar devnull = function() {},\\n    bundleIdCache = {},\\n    bundleResultCache = {},\\n    bundleCallbackQueue = {};\\n\\n\\n/**\\n * Subscribe to bundle load event.\\n * @param {string[]} bundleIds - Bundle ids\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction subscribe(bundleIds, callbackFn) {\\n  // listify\\n  bundleIds = bundleIds.push ? bundleIds : [bundleIds];\\n\\n  var depsNotFound = [],\\n      i = bundleIds.length,\\n      numWaiting = i,\\n      fn,\\n      bundleId,\\n      r,\\n      q;\\n\\n  // define callback function\\n  fn = function (bundleId, pathsNotFound) {\\n    if (pathsNotFound.length) depsNotFound.push(bundleId);\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(depsNotFound);\\n  };\\n\\n  // register callback\\n  while (i--) {\\n    bundleId = bundleIds[i];\\n\\n    // execute callback if in result cache\\n    r = bundleResultCache[bundleId];\\n    if (r) {\\n      fn(bundleId, r);\\n      continue;\\n    }\\n\\n    // add to callback queue\\n    q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || [];\\n    q.push(fn);\\n  }\\n}\\n\\n\\n/**\\n * Publish bundle load event.\\n * @param {string} bundleId - Bundle id\\n * @param {string[]} pathsNotFound - List of files not found\\n */\\nfunction publish(bundleId, pathsNotFound) {\\n  // exit if id isn't defined\\n  if (!bundleId) return;\\n\\n  var q = bundleCallbackQueue[bundleId];\\n\\n  // cache result\\n  bundleResultCache[bundleId] = pathsNotFound;\\n\\n  // exit if queue is empty\\n  if (!q) return;\\n\\n  // empty callback queue\\n  while (q.length) {\\n    q[0](bundleId, pathsNotFound);\\n    q.splice(0, 1);\\n  }\\n}\\n\\n\\n/**\\n * Execute callbacks.\\n * @param {Object or Function} args - The callback args\\n * @param {string[]} depsNotFound - List of dependencies not found\\n */\\nfunction executeCallbacks(args, depsNotFound) {\\n  // accept function as argument\\n  if (args.call) args = {success: args};\\n\\n  // success and error callbacks\\n  if (depsNotFound.length) (args.error || devnull)(depsNotFound);\\n  else (args.success || devnull)(args);\\n}\\n\\n\\n/**\\n * Load individual file.\\n * @param {string} path - The file path\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFile(path, callbackFn, args, numTries) {\\n  var doc = document,\\n      async = args.async,\\n      maxTries = (args.numRetries || 0) + 1,\\n      beforeCallbackFn = args.before || devnull,\\n      pathname = path.replace(/[\\\\?|#].*$/, ''),\\n      pathStripped = path.replace(/^(css|img)!/, ''),\\n      isLegacyIECss,\\n      e;\\n\\n  numTries = numTries || 0;\\n\\n  if (/(^css!|\\\\.css$)/.test(pathname)) {\\n    // css\\n    e = doc.createElement('link');\\n    e.rel = 'stylesheet';\\n    e.href = pathStripped;\\n\\n    // tag IE9+\\n    isLegacyIECss = 'hideFocus' in e;\\n\\n    // use preload in IE Edge (to detect load errors)\\n    if (isLegacyIECss && e.relList) {\\n      isLegacyIECss = 0;\\n      e.rel = 'preload';\\n      e.as = 'style';\\n    }\\n  } else if (/(^img!|\\\\.(png|gif|jpg|svg|webp)$)/.test(pathname)) {\\n    // image\\n    e = doc.createElement('img');\\n    e.src = pathStripped;    \\n  } else {\\n    // javascript\\n    e = doc.createElement('script');\\n    e.src = path;\\n    e.async = async === undefined ? true : async;\\n  }\\n\\n  e.onload = e.onerror = e.onbeforeload = function (ev) {\\n    var result = ev.type[0];\\n\\n    // treat empty stylesheets as failures to get around lack of onerror\\n    // support in IE9-11\\n    if (isLegacyIECss) {\\n      try {\\n        if (!e.sheet.cssText.length) result = 'e';\\n      } catch (x) {\\n        // sheets objects created from load errors don't allow access to\\n        // `cssText` (unless error is Code:18 SecurityError)\\n        if (x.code != 18) result = 'e';\\n      }\\n    }\\n\\n    // handle retries in case of load failure\\n    if (result == 'e') {\\n      // increment counter\\n      numTries += 1;\\n\\n      // exit function and try again\\n      if (numTries < maxTries) {\\n        return loadFile(path, callbackFn, args, numTries);\\n      }\\n    } else if (e.rel == 'preload' && e.as == 'style') {\\n      // activate preloaded stylesheets\\n      return e.rel = 'stylesheet'; // jshint ignore:line\\n    }\\n    \\n    // execute callback\\n    callbackFn(path, result, ev.defaultPrevented);\\n  };\\n\\n  // add to document (unless callback returns `false`)\\n  if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e);\\n}\\n\\n\\n/**\\n * Load multiple files.\\n * @param {string[]} paths - The file paths\\n * @param {Function} callbackFn - The callback function\\n */\\nfunction loadFiles(paths, callbackFn, args) {\\n  // listify paths\\n  paths = paths.push ? paths : [paths];\\n\\n  var numWaiting = paths.length,\\n      x = numWaiting,\\n      pathsNotFound = [],\\n      fn,\\n      i;\\n\\n  // define callback function\\n  fn = function(path, result, defaultPrevented) {\\n    // handle error\\n    if (result == 'e') pathsNotFound.push(path);\\n\\n    // handle beforeload event. If defaultPrevented then that means the load\\n    // will be blocked (ex. Ghostery/ABP on Safari)\\n    if (result == 'b') {\\n      if (defaultPrevented) pathsNotFound.push(path);\\n      else return;\\n    }\\n\\n    numWaiting--;\\n    if (!numWaiting) callbackFn(pathsNotFound);\\n  };\\n\\n  // load scripts\\n  for (i=0; i < x; i++) loadFile(paths[i], fn, args);\\n}\\n\\n\\n/**\\n * Initiate script load and register bundle.\\n * @param {(string|string[])} paths - The file paths\\n * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success\\n *   callback or (3) object literal with success/error arguments, numRetries,\\n *   etc.\\n * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object\\n *   literal with success/error arguments, numRetries, etc.\\n */\\nfunction loadjs(paths, arg1, arg2) {\\n  var bundleId,\\n      args;\\n\\n  // bundleId (if string)\\n  if (arg1 && arg1.trim) bundleId = arg1;\\n\\n  // args (default is {})\\n  args = (bundleId ? arg2 : arg1) || {};\\n\\n  // throw error if bundle is already defined\\n  if (bundleId) {\\n    if (bundleId in bundleIdCache) {\\n      throw \\\"LoadJS\\\";\\n    } else {\\n      bundleIdCache[bundleId] = true;\\n    }\\n  }\\n\\n  function loadFn(resolve, reject) {\\n    loadFiles(paths, function (pathsNotFound) {\\n      // execute callbacks\\n      executeCallbacks(args, pathsNotFound);\\n      \\n      // resolve Promise\\n      if (resolve) {\\n        executeCallbacks({success: resolve, error: reject}, pathsNotFound);\\n      }\\n\\n      // publish bundle load event\\n      publish(bundleId, pathsNotFound);\\n    }, args);\\n  }\\n  \\n  if (args.returnPromise) return new Promise(loadFn);\\n  else loadFn();\\n}\\n\\n\\n/**\\n * Execute callbacks when dependencies have been satisfied.\\n * @param {(string|string[])} deps - List of bundle ids\\n * @param {Object} args - success/error arguments\\n */\\nloadjs.ready = function ready(deps, args) {\\n  // subscribe to bundle load event\\n  subscribe(deps, function (depsNotFound) {\\n    // execute callbacks\\n    executeCallbacks(args, depsNotFound);\\n  });\\n\\n  return loadjs;\\n};\\n\\n\\n/**\\n * Manually satisfy bundle dependencies.\\n * @param {string} bundleId - The bundle id\\n */\\nloadjs.done = function done(bundleId) {\\n  publish(bundleId, []);\\n};\\n\\n\\n/**\\n * Reset loadjs dependencies statuses\\n */\\nloadjs.reset = function reset() {\\n  bundleIdCache = {};\\n  bundleResultCache = {};\\n  bundleCallbackQueue = {};\\n};\\n\\n\\n/**\\n * Determine if bundle has already been defined\\n * @param String} bundleId - The bundle id\\n */\\nloadjs.isDefined = function isDefined(bundleId) {\\n  return bundleId in bundleIdCache;\\n};\\n\\n\\n// export\\nreturn loadjs;\\n\\n}));\\n\",\"// ==========================================================================\\n// Load an external script\\n// ==========================================================================\\n\\nimport loadjs from 'loadjs';\\n\\nexport default function loadScript(url) {\\n  return new Promise((resolve, reject) => {\\n    loadjs(url, {\\n      success: resolve,\\n      error: reject,\\n    });\\n  });\\n}\\n\",\"// ==========================================================================\\n// Vimeo plugin\\n// ==========================================================================\\n\\nimport captions from '../captions';\\nimport controls from '../controls';\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { format, stripHTML } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\nimport { buildUrlParams } from '../utils/urls';\\n\\n// Parse Vimeo ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  if (is.number(Number(url))) {\\n    return url;\\n  }\\n\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Try to extract a hash for private videos from the URL\\nfunction parseHash(url) {\\n  /* This regex matches a hexadecimal hash if given in any of these forms:\\n   *  - [https://player.]vimeo.com/video/{id}/{hash}[?params]\\n   *  - [https://player.]vimeo.com/video/{id}?h={hash}[&params]\\n   *  - [https://player.]vimeo.com/video/{id}?[params]&h={hash}\\n   *  - video/{id}/{hash}\\n   * If matched, the hash is available in capture group 4\\n   */\\n  const regex = /^.*(vimeo.com\\\\/|video\\\\/)(\\\\d+)(\\\\?.*&*h=|\\\\/)+([\\\\d,a-f]+)/;\\n  const found = url.match(regex);\\n\\n  return found && found.length === 5 ? found[4] : null;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nconst vimeo = {\\n  setup() {\\n    const player = this;\\n\\n    // Add embed class for responsive\\n    toggleClass(player.elements.wrapper, player.config.classNames.embed, true);\\n\\n    // Set speed options from config\\n    player.options.speed = player.config.speed.options;\\n\\n    // Set intial ratio\\n    setAspectRatio.call(player);\\n\\n    // Load the SDK if not already\\n    if (!is.object(window.Vimeo)) {\\n      loadScript(player.config.urls.vimeo.sdk)\\n        .then(() => {\\n          vimeo.ready.call(player);\\n        })\\n        .catch((error) => {\\n          player.debug.warn('Vimeo SDK (player.js) failed to load', error);\\n        });\\n    } else {\\n      vimeo.ready.call(player);\\n    }\\n  },\\n\\n  // API Ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.vimeo;\\n    const { premium, referrerPolicy, ...frameParams } = config;\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n    let hash = '';\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(player.config.attributes.embed.id);\\n      // hash can also be set as attribute on the <div>\\n      hash = player.media.getAttribute(player.config.attributes.embed.hash);\\n    } else {\\n      hash = parseHash(source);\\n    }\\n    const hashParam = hash ? { h: hash } : {};\\n\\n    // If the owner has a pro or premium account then we can hide controls etc\\n    if (premium) {\\n      Object.assign(frameParams, {\\n        controls: false,\\n        sidedock: false,\\n      });\\n    }\\n\\n    // Get Vimeo params for the iframe\\n    const params = buildUrlParams({\\n      loop: player.config.loop.active,\\n      autoplay: player.autoplay,\\n      muted: player.muted,\\n      gesture: 'media',\\n      playsinline: player.config.playsinline,\\n      // hash has to be added to iframe-URL\\n      ...hashParam,\\n      ...frameParams,\\n    });\\n\\n    const id = parseId(source);\\n    // Build an iframe\\n    const iframe = createElement('iframe');\\n    const src = format(player.config.urls.vimeo.iframe, id, params);\\n    iframe.setAttribute('src', src);\\n    iframe.setAttribute('allowfullscreen', '');\\n    iframe.setAttribute(\\n      'allow',\\n      ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; '),\\n    );\\n\\n    // Set the referrer policy if required\\n    if (!is.empty(referrerPolicy)) {\\n      iframe.setAttribute('referrerPolicy', referrerPolicy);\\n    }\\n\\n    // Inject the package\\n    if (premium || !config.customControls) {\\n      iframe.setAttribute('data-poster', player.poster);\\n      player.media = replaceElement(iframe, player.media);\\n    } else {\\n      const wrapper = createElement('div', {\\n        class: player.config.classNames.embedContainer,\\n        'data-poster': player.poster,\\n      });\\n      wrapper.appendChild(iframe);\\n      player.media = replaceElement(wrapper, player.media);\\n    }\\n\\n    // Get poster image\\n    if (!config.customControls) {\\n      fetch(format(player.config.urls.vimeo.api, src)).then((response) => {\\n        if (is.empty(response) || !response.thumbnail_url) {\\n          return;\\n        }\\n\\n        // Set and show poster\\n        ui.setPoster.call(player, response.thumbnail_url).catch(() => {});\\n      });\\n    }\\n\\n    // Setup instance\\n    // https://github.com/vimeo/player.js\\n    player.embed = new window.Vimeo.Player(iframe, {\\n      autopause: player.config.autopause,\\n      muted: player.muted,\\n    });\\n\\n    player.media.paused = true;\\n    player.media.currentTime = 0;\\n\\n    // Disable native text track rendering\\n    if (player.supported.ui) {\\n      player.embed.disableTextTrack();\\n    }\\n\\n    // Create a faux HTML5 API using the Vimeo API\\n    player.media.play = () => {\\n      assurePlaybackState.call(player, true);\\n      return player.embed.play();\\n    };\\n\\n    player.media.pause = () => {\\n      assurePlaybackState.call(player, false);\\n      return player.embed.pause();\\n    };\\n\\n    player.media.stop = () => {\\n      player.pause();\\n      player.currentTime = 0;\\n    };\\n\\n    // Seeking\\n    let { currentTime } = player.media;\\n    Object.defineProperty(player.media, 'currentTime', {\\n      get() {\\n        return currentTime;\\n      },\\n      set(time) {\\n        // Vimeo will automatically play on seek if the video hasn't been played before\\n\\n        // Get current paused state and volume etc\\n        const { embed, media, paused, volume } = player;\\n        const restorePause = paused && !embed.hasPlayed;\\n\\n        // Set seeking state and trigger event\\n        media.seeking = true;\\n        triggerEvent.call(player, media, 'seeking');\\n\\n        // If paused, mute until seek is complete\\n        Promise.resolve(restorePause && embed.setVolume(0))\\n          // Seek\\n          .then(() => embed.setCurrentTime(time))\\n          // Restore paused\\n          .then(() => restorePause && embed.pause())\\n          // Restore volume\\n          .then(() => restorePause && embed.setVolume(volume))\\n          .catch(() => {\\n            // Do nothing\\n          });\\n      },\\n    });\\n\\n    // Playback speed\\n    let speed = player.config.speed.selected;\\n    Object.defineProperty(player.media, 'playbackRate', {\\n      get() {\\n        return speed;\\n      },\\n      set(input) {\\n        player.embed\\n          .setPlaybackRate(input)\\n          .then(() => {\\n            speed = input;\\n            triggerEvent.call(player, player.media, 'ratechange');\\n          })\\n          .catch(() => {\\n            // Cannot set Playback Rate, Video is probably not on Pro account\\n            player.options.speed = [1];\\n          });\\n      },\\n    });\\n\\n    // Volume\\n    let { volume } = player.config;\\n    Object.defineProperty(player.media, 'volume', {\\n      get() {\\n        return volume;\\n      },\\n      set(input) {\\n        player.embed.setVolume(input).then(() => {\\n          volume = input;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Muted\\n    let { muted } = player.config;\\n    Object.defineProperty(player.media, 'muted', {\\n      get() {\\n        return muted;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : false;\\n\\n        player.embed.setMuted(toggle ? true : player.config.muted).then(() => {\\n          muted = toggle;\\n          triggerEvent.call(player, player.media, 'volumechange');\\n        });\\n      },\\n    });\\n\\n    // Loop\\n    let { loop } = player.config;\\n    Object.defineProperty(player.media, 'loop', {\\n      get() {\\n        return loop;\\n      },\\n      set(input) {\\n        const toggle = is.boolean(input) ? input : player.config.loop.active;\\n\\n        player.embed.setLoop(toggle).then(() => {\\n          loop = toggle;\\n        });\\n      },\\n    });\\n\\n    // Source\\n    let currentSrc;\\n    player.embed\\n      .getVideoUrl()\\n      .then((value) => {\\n        currentSrc = value;\\n        controls.setDownloadUrl.call(player);\\n      })\\n      .catch((error) => {\\n        this.debug.warn(error);\\n      });\\n\\n    Object.defineProperty(player.media, 'currentSrc', {\\n      get() {\\n        return currentSrc;\\n      },\\n    });\\n\\n    // Ended\\n    Object.defineProperty(player.media, 'ended', {\\n      get() {\\n        return player.currentTime === player.duration;\\n      },\\n    });\\n\\n    // Set aspect ratio based on video size\\n    Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then((dimensions) => {\\n      const [width, height] = dimensions;\\n      player.embed.ratio = roundAspectRatio(width, height);\\n      setAspectRatio.call(this);\\n    });\\n\\n    // Set autopause\\n    player.embed.setAutopause(player.config.autopause).then((state) => {\\n      player.config.autopause = state;\\n    });\\n\\n    // Get title\\n    player.embed.getVideoTitle().then((title) => {\\n      player.config.title = title;\\n      ui.setTitle.call(this);\\n    });\\n\\n    // Get current time\\n    player.embed.getCurrentTime().then((value) => {\\n      currentTime = value;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    // Get duration\\n    player.embed.getDuration().then((value) => {\\n      player.media.duration = value;\\n      triggerEvent.call(player, player.media, 'durationchange');\\n    });\\n\\n    // Get captions\\n    player.embed.getTextTracks().then((tracks) => {\\n      player.media.textTracks = tracks;\\n      captions.setup.call(player);\\n    });\\n\\n    player.embed.on('cuechange', ({ cues = [] }) => {\\n      const strippedCues = cues.map((cue) => stripHTML(cue.text));\\n      captions.updateCues.call(player, strippedCues);\\n    });\\n\\n    player.embed.on('loaded', () => {\\n      // Assure state and events are updated on autoplay\\n      player.embed.getPaused().then((paused) => {\\n        assurePlaybackState.call(player, !paused);\\n        if (!paused) {\\n          triggerEvent.call(player, player.media, 'playing');\\n        }\\n      });\\n\\n      if (is.element(player.embed.element) && player.supported.ui) {\\n        const frame = player.embed.element;\\n\\n        // Fix keyboard focus issues\\n        // https://github.com/sampotts/plyr/issues/317\\n        frame.setAttribute('tabindex', -1);\\n      }\\n    });\\n\\n    player.embed.on('bufferstart', () => {\\n      triggerEvent.call(player, player.media, 'waiting');\\n    });\\n\\n    player.embed.on('bufferend', () => {\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('play', () => {\\n      assurePlaybackState.call(player, true);\\n      triggerEvent.call(player, player.media, 'playing');\\n    });\\n\\n    player.embed.on('pause', () => {\\n      assurePlaybackState.call(player, false);\\n    });\\n\\n    player.embed.on('timeupdate', (data) => {\\n      player.media.seeking = false;\\n      currentTime = data.seconds;\\n      triggerEvent.call(player, player.media, 'timeupdate');\\n    });\\n\\n    player.embed.on('progress', (data) => {\\n      player.media.buffered = data.percent;\\n      triggerEvent.call(player, player.media, 'progress');\\n\\n      // Check all loaded\\n      if (parseInt(data.percent, 10) === 1) {\\n        triggerEvent.call(player, player.media, 'canplaythrough');\\n      }\\n\\n      // Get duration as if we do it before load, it gives an incorrect value\\n      // https://github.com/sampotts/plyr/issues/891\\n      player.embed.getDuration().then((value) => {\\n        if (value !== player.media.duration) {\\n          player.media.duration = value;\\n          triggerEvent.call(player, player.media, 'durationchange');\\n        }\\n      });\\n    });\\n\\n    player.embed.on('seeked', () => {\\n      player.media.seeking = false;\\n      triggerEvent.call(player, player.media, 'seeked');\\n    });\\n\\n    player.embed.on('ended', () => {\\n      player.media.paused = true;\\n      triggerEvent.call(player, player.media, 'ended');\\n    });\\n\\n    player.embed.on('error', (detail) => {\\n      player.media.error = detail;\\n      triggerEvent.call(player, player.media, 'error');\\n    });\\n\\n    // Rebuild UI\\n    if (config.customControls) {\\n      setTimeout(() => ui.build.call(player), 0);\\n    }\\n  },\\n};\\n\\nexport default vimeo;\\n\",\"// ==========================================================================\\n// YouTube plugin\\n// ==========================================================================\\n\\nimport ui from '../ui';\\nimport { createElement, replaceElement, toggleClass } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport loadImage from '../utils/load-image';\\nimport loadScript from '../utils/load-script';\\nimport { extend } from '../utils/objects';\\nimport { format, generateId } from '../utils/strings';\\nimport { roundAspectRatio, setAspectRatio } from '../utils/style';\\n\\n// Parse YouTube ID from URL\\nfunction parseId(url) {\\n  if (is.empty(url)) {\\n    return null;\\n  }\\n\\n  const regex = /^.*(youtu.be\\\\/|v\\\\/|u\\\\/\\\\w\\\\/|embed\\\\/|watch\\\\?v=|&v=)([^#&?]*).*/;\\n  return url.match(regex) ? RegExp.$2 : url;\\n}\\n\\n// Set playback state and trigger change (only on actual change)\\nfunction assurePlaybackState(play) {\\n  if (play && !this.embed.hasPlayed) {\\n    this.embed.hasPlayed = true;\\n  }\\n  if (this.media.paused === play) {\\n    this.media.paused = !play;\\n    triggerEvent.call(this, this.media, play ? 'play' : 'pause');\\n  }\\n}\\n\\nfunction getHost(config) {\\n  if (config.noCookie) {\\n    return 'https://www.youtube-nocookie.com';\\n  }\\n\\n  if (window.location.protocol === 'http:') {\\n    return 'http://www.youtube.com';\\n  }\\n\\n  // Use YouTube's default\\n  return undefined;\\n}\\n\\nconst youtube = {\\n  setup() {\\n    // Add embed class for responsive\\n    toggleClass(this.elements.wrapper, this.config.classNames.embed, true);\\n\\n    // Setup API\\n    if (is.object(window.YT) && is.function(window.YT.Player)) {\\n      youtube.ready.call(this);\\n    } else {\\n      // Reference current global callback\\n      const callback = window.onYouTubeIframeAPIReady;\\n\\n      // Set callback to process queue\\n      window.onYouTubeIframeAPIReady = () => {\\n        // Call global callback if set\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n\\n        youtube.ready.call(this);\\n      };\\n\\n      // Load the SDK\\n      loadScript(this.config.urls.youtube.sdk).catch((error) => {\\n        this.debug.warn('YouTube API failed to load', error);\\n      });\\n    }\\n  },\\n\\n  // Get the media title\\n  getTitle(videoId) {\\n    const url = format(this.config.urls.youtube.api, videoId);\\n\\n    fetch(url)\\n      .then((data) => {\\n        if (is.object(data)) {\\n          const { title, height, width } = data;\\n\\n          // Set title\\n          this.config.title = title;\\n          ui.setTitle.call(this);\\n\\n          // Set aspect ratio\\n          this.embed.ratio = roundAspectRatio(width, height);\\n        }\\n\\n        setAspectRatio.call(this);\\n      })\\n      .catch(() => {\\n        // Set aspect ratio\\n        setAspectRatio.call(this);\\n      });\\n  },\\n\\n  // API ready\\n  ready() {\\n    const player = this;\\n    const config = player.config.youtube;\\n    // Ignore already setup (race condition)\\n    const currentId = player.media && player.media.getAttribute('id');\\n    if (!is.empty(currentId) && currentId.startsWith('youtube-')) {\\n      return;\\n    }\\n\\n    // Get the source URL or ID\\n    let source = player.media.getAttribute('src');\\n\\n    // Get from <div> if needed\\n    if (is.empty(source)) {\\n      source = player.media.getAttribute(this.config.attributes.embed.id);\\n    }\\n\\n    // Replace the <iframe> with a <div> due to YouTube API issues\\n    const videoId = parseId(source);\\n    const id = generateId(player.provider);\\n    // Replace media element\\n    const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });\\n    player.media = replaceElement(container, player.media);\\n\\n    // Only load the poster when using custom controls\\n    if (config.customControls) {\\n      const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;\\n\\n      // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)\\n      loadImage(posterSrc('maxres'), 121) // Highest quality and un-padded\\n        .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3\\n        .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists\\n        .then((image) => ui.setPoster.call(player, image.src))\\n        .then((src) => {\\n          // If the image is padded, use background-size \\\"cover\\\" instead (like youtube does too with their posters)\\n          if (!src.includes('maxres')) {\\n            player.elements.poster.style.backgroundSize = 'cover';\\n          }\\n        })\\n        .catch(() => {});\\n    }\\n\\n    // Setup instance\\n    // https://developers.google.com/youtube/iframe_api_reference\\n    player.embed = new window.YT.Player(player.media, {\\n      videoId,\\n      host: getHost(config),\\n      playerVars: extend(\\n        {},\\n        {\\n          // Autoplay\\n          autoplay: player.config.autoplay ? 1 : 0,\\n          // iframe interface language\\n          hl: player.config.hl,\\n          // Only show controls if not fully supported or opted out\\n          controls: player.supported.ui && config.customControls ? 0 : 1,\\n          // Disable keyboard as we handle it\\n          disablekb: 1,\\n          // Allow iOS inline playback\\n          playsinline: player.config.playsinline && !player.config.fullscreen.iosNative ? 1 : 0,\\n          // Captions are flaky on YouTube\\n          cc_load_policy: player.captions.active ? 1 : 0,\\n          cc_lang_pref: player.config.captions.language,\\n          // Tracking for stats\\n          widget_referrer: window ? window.location.href : null,\\n        },\\n        config,\\n      ),\\n      events: {\\n        onError(event) {\\n          // YouTube may fire onError twice, so only handle it once\\n          if (!player.media.error) {\\n            const code = event.data;\\n            // Messages copied from https://developers.google.com/youtube/iframe_api_reference#onError\\n            const message =\\n              {\\n                2: 'The request contains an invalid parameter value. For example, this error occurs if you specify a video ID that does not have 11 characters, or if the video ID contains invalid characters, such as exclamation points or asterisks.',\\n                5: 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.',\\n                100: 'The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.',\\n                101: 'The owner of the requested video does not allow it to be played in embedded players.',\\n                150: 'The owner of the requested video does not allow it to be played in embedded players.',\\n              }[code] || 'An unknown error occurred';\\n\\n            player.media.error = { code, message };\\n\\n            triggerEvent.call(player, player.media, 'error');\\n          }\\n        },\\n        onPlaybackRateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get current speed\\n          player.media.playbackRate = instance.getPlaybackRate();\\n\\n          triggerEvent.call(player, player.media, 'ratechange');\\n        },\\n        onReady(event) {\\n          // Bail if onReady has already been called. See issue #1108\\n          if (is.function(player.media.play)) {\\n            return;\\n          }\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Get the title\\n          youtube.getTitle.call(player, videoId);\\n\\n          // Create a faux HTML5 API using the YouTube API\\n          player.media.play = () => {\\n            assurePlaybackState.call(player, true);\\n            instance.playVideo();\\n          };\\n\\n          player.media.pause = () => {\\n            assurePlaybackState.call(player, false);\\n            instance.pauseVideo();\\n          };\\n\\n          player.media.stop = () => {\\n            instance.stopVideo();\\n          };\\n\\n          player.media.duration = instance.getDuration();\\n          player.media.paused = true;\\n\\n          // Seeking\\n          player.media.currentTime = 0;\\n          Object.defineProperty(player.media, 'currentTime', {\\n            get() {\\n              return Number(instance.getCurrentTime());\\n            },\\n            set(time) {\\n              // If paused and never played, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet).\\n              if (player.paused && !player.embed.hasPlayed) {\\n                player.embed.mute();\\n              }\\n\\n              // Set seeking state and trigger event\\n              player.media.seeking = true;\\n              triggerEvent.call(player, player.media, 'seeking');\\n\\n              // Seek after events sent\\n              instance.seekTo(time);\\n            },\\n          });\\n\\n          // Playback speed\\n          Object.defineProperty(player.media, 'playbackRate', {\\n            get() {\\n              return instance.getPlaybackRate();\\n            },\\n            set(input) {\\n              instance.setPlaybackRate(input);\\n            },\\n          });\\n\\n          // Volume\\n          let { volume } = player.config;\\n          Object.defineProperty(player.media, 'volume', {\\n            get() {\\n              return volume;\\n            },\\n            set(input) {\\n              volume = input;\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Muted\\n          let { muted } = player.config;\\n          Object.defineProperty(player.media, 'muted', {\\n            get() {\\n              return muted;\\n            },\\n            set(input) {\\n              const toggle = is.boolean(input) ? input : muted;\\n              muted = toggle;\\n              instance[toggle ? 'mute' : 'unMute']();\\n              instance.setVolume(volume * 100);\\n              triggerEvent.call(player, player.media, 'volumechange');\\n            },\\n          });\\n\\n          // Source\\n          Object.defineProperty(player.media, 'currentSrc', {\\n            get() {\\n              return instance.getVideoUrl();\\n            },\\n          });\\n\\n          // Ended\\n          Object.defineProperty(player.media, 'ended', {\\n            get() {\\n              return player.currentTime === player.duration;\\n            },\\n          });\\n\\n          // Get available speeds\\n          const speeds = instance.getAvailablePlaybackRates();\\n          // Filter based on config\\n          player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));\\n\\n          // Set the tabindex to avoid focus entering iframe\\n          if (player.supported.ui && config.customControls) {\\n            player.media.setAttribute('tabindex', -1);\\n          }\\n\\n          triggerEvent.call(player, player.media, 'timeupdate');\\n          triggerEvent.call(player, player.media, 'durationchange');\\n\\n          // Reset timer\\n          clearInterval(player.timers.buffering);\\n\\n          // Setup buffering\\n          player.timers.buffering = setInterval(() => {\\n            // Get loaded % from YouTube\\n            player.media.buffered = instance.getVideoLoadedFraction();\\n\\n            // Trigger progress only when we actually buffer something\\n            if (player.media.lastBuffered === null || player.media.lastBuffered < player.media.buffered) {\\n              triggerEvent.call(player, player.media, 'progress');\\n            }\\n\\n            // Set last buffer point\\n            player.media.lastBuffered = player.media.buffered;\\n\\n            // Bail if we're at 100%\\n            if (player.media.buffered === 1) {\\n              clearInterval(player.timers.buffering);\\n\\n              // Trigger event\\n              triggerEvent.call(player, player.media, 'canplaythrough');\\n            }\\n          }, 200);\\n\\n          // Rebuild UI\\n          if (config.customControls) {\\n            setTimeout(() => ui.build.call(player), 50);\\n          }\\n        },\\n        onStateChange(event) {\\n          // Get the instance\\n          const instance = event.target;\\n\\n          // Reset timer\\n          clearInterval(player.timers.playing);\\n\\n          const seeked = player.media.seeking && [1, 2].includes(event.data);\\n\\n          if (seeked) {\\n            // Unset seeking and fire seeked event\\n            player.media.seeking = false;\\n            triggerEvent.call(player, player.media, 'seeked');\\n          }\\n\\n          // Handle events\\n          // -1   Unstarted\\n          // 0    Ended\\n          // 1    Playing\\n          // 2    Paused\\n          // 3    Buffering\\n          // 5    Video cued\\n          switch (event.data) {\\n            case -1:\\n              // Update scrubber\\n              triggerEvent.call(player, player.media, 'timeupdate');\\n\\n              // Get loaded % from YouTube\\n              player.media.buffered = instance.getVideoLoadedFraction();\\n              triggerEvent.call(player, player.media, 'progress');\\n\\n              break;\\n\\n            case 0:\\n              assurePlaybackState.call(player, false);\\n\\n              // YouTube doesn't support loop for a single video, so mimick it.\\n              if (player.media.loop) {\\n                // YouTube needs a call to `stopVideo` before playing again\\n                instance.stopVideo();\\n                instance.playVideo();\\n              } else {\\n                triggerEvent.call(player, player.media, 'ended');\\n              }\\n\\n              break;\\n\\n            case 1:\\n              // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {\\n                player.media.pause();\\n              } else {\\n                assurePlaybackState.call(player, true);\\n\\n                triggerEvent.call(player, player.media, 'playing');\\n\\n                // Poll to get playback progress\\n                player.timers.playing = setInterval(() => {\\n                  triggerEvent.call(player, player.media, 'timeupdate');\\n                }, 50);\\n\\n                // Check duration again due to YouTube bug\\n                // https://github.com/sampotts/plyr/issues/374\\n                // https://code.google.com/p/gdata-issues/issues/detail?id=8690\\n                if (player.media.duration !== instance.getDuration()) {\\n                  player.media.duration = instance.getDuration();\\n                  triggerEvent.call(player, player.media, 'durationchange');\\n                }\\n              }\\n\\n              break;\\n\\n            case 2:\\n              // Restore audio (YouTube starts playing on seek if the video hasn't been played yet)\\n              if (!player.muted) {\\n                player.embed.unMute();\\n              }\\n              assurePlaybackState.call(player, false);\\n\\n              break;\\n\\n            case 3:\\n              // Trigger waiting event to add loading classes to container as the video buffers.\\n              triggerEvent.call(player, player.media, 'waiting');\\n\\n              break;\\n\\n            default:\\n              break;\\n          }\\n\\n          triggerEvent.call(player, player.elements.container, 'statechange', false, {\\n            code: event.data,\\n          });\\n        },\\n      },\\n    });\\n  },\\n};\\n\\nexport default youtube;\\n\",\"// ==========================================================================\\n// Plyr Media\\n// ==========================================================================\\n\\nimport html5 from './html5';\\nimport vimeo from './plugins/vimeo';\\nimport youtube from './plugins/youtube';\\nimport { createElement, toggleClass, wrap } from './utils/elements';\\n\\nconst media = {\\n  // Setup media\\n  setup() {\\n    // If there's no media, bail\\n    if (!this.media) {\\n      this.debug.warn('No media element found!');\\n      return;\\n    }\\n\\n    // Add type class\\n    toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', this.type), true);\\n\\n    // Add provider class\\n    toggleClass(this.elements.container, this.config.classNames.provider.replace('{0}', this.provider), true);\\n\\n    // Add video class for embeds\\n    // This will require changes if audio embeds are added\\n    if (this.isEmbed) {\\n      toggleClass(this.elements.container, this.config.classNames.type.replace('{0}', 'video'), true);\\n    }\\n\\n    // Inject the player wrapper\\n    if (this.isVideo) {\\n      // Create the wrapper div\\n      this.elements.wrapper = createElement('div', {\\n        class: this.config.classNames.video,\\n      });\\n\\n      // Wrap the video in a container\\n      wrap(this.media, this.elements.wrapper);\\n\\n      // Poster image container\\n      this.elements.poster = createElement('div', {\\n        class: this.config.classNames.poster,\\n      });\\n\\n      this.elements.wrapper.appendChild(this.elements.poster);\\n    }\\n\\n    if (this.isHTML5) {\\n      html5.setup.call(this);\\n    } else if (this.isYouTube) {\\n      youtube.setup.call(this);\\n    } else if (this.isVimeo) {\\n      vimeo.setup.call(this);\\n    }\\n  },\\n};\\n\\nexport default media;\\n\",\"// ==========================================================================\\n// Advertisement plugin using Google IMA HTML5 SDK\\n// Create an account with our ad partner, vi here:\\n// https://www.vi.ai/publisher-video-monetization/\\n// ==========================================================================\\n\\n/* global google */\\n\\nimport { createElement } from '../utils/elements';\\nimport { triggerEvent } from '../utils/events';\\nimport i18n from '../utils/i18n';\\nimport is from '../utils/is';\\nimport loadScript from '../utils/load-script';\\nimport { silencePromise } from '../utils/promise';\\nimport { formatTime } from '../utils/time';\\nimport { buildUrlParams } from '../utils/urls';\\n\\nconst destroy = (instance) => {\\n  // Destroy our adsManager\\n  if (instance.manager) {\\n    instance.manager.destroy();\\n  }\\n\\n  // Destroy our adsManager\\n  if (instance.elements.displayContainer) {\\n    instance.elements.displayContainer.destroy();\\n  }\\n\\n  instance.elements.container.remove();\\n};\\n\\nclass Ads {\\n  /**\\n   * Ads constructor.\\n   * @param {Object} player\\n   * @return {Ads}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.config = player.config.ads;\\n    this.playing = false;\\n    this.initialized = false;\\n    this.elements = {\\n      container: null,\\n      displayContainer: null,\\n    };\\n    this.manager = null;\\n    this.loader = null;\\n    this.cuePoints = null;\\n    this.events = {};\\n    this.safetyTimer = null;\\n    this.countdownTimer = null;\\n\\n    // Setup a promise to resolve when the IMA manager is ready\\n    this.managerPromise = new Promise((resolve, reject) => {\\n      // The ad is loaded and ready\\n      this.on('loaded', resolve);\\n\\n      // Ads failed\\n      this.on('error', reject);\\n    });\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    const { config } = this;\\n\\n    return (\\n      this.player.isHTML5 &&\\n      this.player.isVideo &&\\n      config.enabled &&\\n      (!is.empty(config.publisherId) || is.url(config.tagUrl))\\n    );\\n  }\\n\\n  /**\\n   * Load the IMA SDK\\n   */\\n  load = () => {\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Check if the Google IMA3 SDK is loaded or load it ourselves\\n    if (!is.object(window.google) || !is.object(window.google.ima)) {\\n      loadScript(this.player.config.urls.googleIMA.sdk)\\n        .then(() => {\\n          this.ready();\\n        })\\n        .catch(() => {\\n          // Script failed to load or is blocked\\n          this.trigger('error', new Error('Google IMA SDK failed to load'));\\n        });\\n    } else {\\n      this.ready();\\n    }\\n  };\\n\\n  /**\\n   * Get the ads instance ready\\n   */\\n  ready = () => {\\n    // Double check we're enabled\\n    if (!this.enabled) {\\n      destroy(this);\\n    }\\n\\n    // Start ticking our safety timer. If the whole advertisement\\n    // thing doesn't resolve within our set time; we bail\\n    this.startSafetyTimer(12000, 'ready()');\\n\\n    // Clear the safety timer\\n    this.managerPromise.then(() => {\\n      this.clearSafetyTimer('onAdsManagerLoaded()');\\n    });\\n\\n    // Set listeners on the Plyr instance\\n    this.listeners();\\n\\n    // Setup the IMA SDK\\n    this.setupIMA();\\n  };\\n\\n  // Build the tag URL\\n  get tagUrl() {\\n    const { config } = this;\\n\\n    if (is.url(config.tagUrl)) {\\n      return config.tagUrl;\\n    }\\n\\n    const params = {\\n      AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',\\n      AV_CHANNELID: '5a0458dc28a06145e4519d21',\\n      AV_URL: window.location.hostname,\\n      cb: Date.now(),\\n      AV_WIDTH: 640,\\n      AV_HEIGHT: 480,\\n      AV_CDIM2: config.publisherId,\\n    };\\n\\n    const base = 'https://go.aniview.com/api/adserver6/vast/';\\n\\n    return `${base}?${buildUrlParams(params)}`;\\n  }\\n\\n  /**\\n   * In order for the SDK to display ads for our video, we need to tell it where to put them,\\n   * so here we define our ad container. This div is set up to render on top of the video player.\\n   * Using the code below, we tell the SDK to render ads within that div. We also provide a\\n   * handle to the content video player - the SDK will poll the current time of our player to\\n   * properly place mid-rolls. After we create the ad display container, we initialize it. On\\n   * mobile devices, this initialization is done as the result of a user action.\\n   */\\n  setupIMA = () => {\\n    // Create the container for our advertisements\\n    this.elements.container = createElement('div', {\\n      class: this.player.config.classNames.ads,\\n    });\\n\\n    this.player.elements.container.appendChild(this.elements.container);\\n\\n    // So we can run VPAID2\\n    google.ima.settings.setVpaidMode(google.ima.ImaSdkSettings.VpaidMode.ENABLED);\\n\\n    // Set language\\n    google.ima.settings.setLocale(this.player.config.ads.language);\\n\\n    // Set playback for iOS10+\\n    google.ima.settings.setDisableCustomPlaybackForIOS10Plus(this.player.config.playsinline);\\n\\n    // We assume the adContainer is the video container of the plyr element that will house the ads\\n    this.elements.displayContainer = new google.ima.AdDisplayContainer(this.elements.container, this.player.media);\\n\\n    // Create ads loader\\n    this.loader = new google.ima.AdsLoader(this.elements.displayContainer);\\n\\n    // Listen and respond to ads loaded and error events\\n    this.loader.addEventListener(\\n      google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED,\\n      (event) => this.onAdsManagerLoaded(event),\\n      false,\\n    );\\n    this.loader.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error), false);\\n\\n    // Request video ads to be pre-loaded\\n    this.requestAds();\\n  };\\n\\n  /**\\n   * Request advertisements\\n   */\\n  requestAds = () => {\\n    const { container } = this.player.elements;\\n\\n    try {\\n      // Request video ads\\n      const request = new google.ima.AdsRequest();\\n      request.adTagUrl = this.tagUrl;\\n\\n      // Specify the linear and nonlinear slot sizes. This helps the SDK\\n      // to select the correct creative if multiple are returned\\n      request.linearAdSlotWidth = container.offsetWidth;\\n      request.linearAdSlotHeight = container.offsetHeight;\\n      request.nonLinearAdSlotWidth = container.offsetWidth;\\n      request.nonLinearAdSlotHeight = container.offsetHeight;\\n\\n      // We only overlay ads as we only support video.\\n      request.forceNonLinearFullSlot = false;\\n\\n      // Mute based on current state\\n      request.setAdWillPlayMuted(!this.player.muted);\\n\\n      this.loader.requestAds(request);\\n    } catch (error) {\\n      this.onAdError(error);\\n    }\\n  };\\n\\n  /**\\n   * Update the ad countdown\\n   * @param {Boolean} start\\n   */\\n  pollCountdown = (start = false) => {\\n    if (!start) {\\n      clearInterval(this.countdownTimer);\\n      this.elements.container.removeAttribute('data-badge-text');\\n      return;\\n    }\\n\\n    const update = () => {\\n      const time = formatTime(Math.max(this.manager.getRemainingTime(), 0));\\n      const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;\\n      this.elements.container.setAttribute('data-badge-text', label);\\n    };\\n\\n    this.countdownTimer = setInterval(update, 100);\\n  };\\n\\n  /**\\n   * This method is called whenever the ads are ready inside the AdDisplayContainer\\n   * @param {Event} event - adsManagerLoadedEvent\\n   */\\n  onAdsManagerLoaded = (event) => {\\n    // Load could occur after a source change (race condition)\\n    if (!this.enabled) {\\n      return;\\n    }\\n\\n    // Get the ads manager\\n    const settings = new google.ima.AdsRenderingSettings();\\n\\n    // Tell the SDK to save and restore content video state on our behalf\\n    settings.restoreCustomPlaybackStateOnAdBreakComplete = true;\\n    settings.enablePreloading = true;\\n\\n    // The SDK is polling currentTime on the contentPlayback. And needs a duration\\n    // so it can determine when to start the mid- and post-roll\\n    this.manager = event.getAdsManager(this.player, settings);\\n\\n    // Get the cue points for any mid-rolls by filtering out the pre- and post-roll\\n    this.cuePoints = this.manager.getCuePoints();\\n\\n    // Add listeners to the required events\\n    // Advertisement error events\\n    this.manager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, (error) => this.onAdError(error));\\n\\n    // Advertisement regular events\\n    Object.keys(google.ima.AdEvent.Type).forEach((type) => {\\n      this.manager.addEventListener(google.ima.AdEvent.Type[type], (e) => this.onAdEvent(e));\\n    });\\n\\n    // Resolve our adsManager\\n    this.trigger('loaded');\\n  };\\n\\n  addCuePoints = () => {\\n    // Add advertisement cue's within the time line if available\\n    if (!is.empty(this.cuePoints)) {\\n      this.cuePoints.forEach((cuePoint) => {\\n        if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {\\n          const seekElement = this.player.elements.progress;\\n\\n          if (is.element(seekElement)) {\\n            const cuePercentage = (100 / this.player.duration) * cuePoint;\\n            const cue = createElement('span', {\\n              class: this.player.config.classNames.cues,\\n            });\\n\\n            cue.style.left = `${cuePercentage.toString()}%`;\\n            seekElement.appendChild(cue);\\n          }\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * This is where all the event handling takes place. Retrieve the ad from the event. Some\\n   * events (e.g. ALL_ADS_COMPLETED) don't have the ad object associated\\n   * https://developers.google.com/interactive-media-ads/docs/sdks/html5/v3/apis#ima.AdEvent.Type\\n   * @param {Event} event\\n   */\\n  onAdEvent = (event) => {\\n    const { container } = this.player.elements;\\n    // Retrieve the ad from the event. Some events (e.g. ALL_ADS_COMPLETED)\\n    // don't have ad object associated\\n    const ad = event.getAd();\\n    const adData = event.getAdData();\\n\\n    // Proxy event\\n    const dispatchEvent = (type) => {\\n      triggerEvent.call(this.player, this.player.media, `ads${type.replace(/_/g, '').toLowerCase()}`);\\n    };\\n\\n    // Bubble the event\\n    dispatchEvent(event.type);\\n\\n    switch (event.type) {\\n      case google.ima.AdEvent.Type.LOADED:\\n        // This is the first event sent for an ad - it is possible to determine whether the\\n        // ad is a video ad or an overlay\\n        this.trigger('loaded');\\n\\n        // Start countdown\\n        this.pollCountdown(true);\\n\\n        if (!ad.isLinear()) {\\n          // Position AdDisplayContainer correctly for overlay\\n          ad.width = container.offsetWidth;\\n          ad.height = container.offsetHeight;\\n        }\\n\\n        // console.info('Ad type: ' + event.getAd().getAdPodInfo().getPodIndex());\\n        // console.info('Ad time: ' + event.getAd().getAdPodInfo().getTimeOffset());\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.STARTED:\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:\\n        // All ads for the current videos are done. We can now request new advertisements\\n        // in case the video is re-played\\n\\n        // TODO: Example for what happens when a next video in a playlist would be loaded.\\n        // So here we load a new video when all ads are done.\\n        // Then we load new ads within a new adsManager. When the video\\n        // Is started - after - the ads are loaded, then we get ads.\\n        // You can also easily test cancelling and reloading by running\\n        // player.ads.cancel() and player.ads.play from the console I guess.\\n        // this.player.source = {\\n        //     type: 'video',\\n        //     title: 'View From A Blue Moon',\\n        //     sources: [{\\n        //         src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4', type:\\n        // 'video/mp4', }], poster:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg', tracks:\\n        // [ { kind: 'captions', label: 'English', srclang: 'en', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',\\n        // default: true, }, { kind: 'captions', label: 'French', srclang: 'fr', src:\\n        // 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt', }, ],\\n        // };\\n\\n        // TODO: So there is still this thing where a video should only be allowed to start\\n        // playing when the IMA SDK is ready or has failed\\n\\n        if (this.player.ended) {\\n          this.loadAds();\\n        } else {\\n          // The SDK won't allow new ads to be called without receiving a contentComplete()\\n          this.loader.contentComplete();\\n        }\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED:\\n        // This event indicates the ad has started - the video player can adjust the UI,\\n        // for example display a pause button and remaining time. Fired when content should\\n        // be paused. This usually happens right before an ad is about to cover the content\\n\\n        this.pauseContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED:\\n        // This event indicates the ad has finished - the video player can perform\\n        // appropriate UI actions, such as removing the timer for remaining time detection.\\n        // Fired when content should be resumed. This usually happens when an ad finishes\\n        // or collapses\\n\\n        this.pollCountdown();\\n\\n        this.resumeContent();\\n\\n        break;\\n\\n      case google.ima.AdEvent.Type.LOG:\\n        if (adData.adError) {\\n          this.player.debug.warn(`Non-fatal ad error: ${adData.adError.getMessage()}`);\\n        }\\n\\n        break;\\n\\n      default:\\n        break;\\n    }\\n  };\\n\\n  /**\\n   * Any ad error handling comes through here\\n   * @param {Event} event\\n   */\\n  onAdError = (event) => {\\n    this.cancel();\\n    this.player.debug.warn('Ads error', event);\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events. This ensures\\n   * the mid- and post-roll launch at the correct time. And\\n   * resize the advertisement when the player resizes\\n   */\\n  listeners = () => {\\n    const { container } = this.player.elements;\\n    let time;\\n\\n    this.player.on('canplay', () => {\\n      this.addCuePoints();\\n    });\\n\\n    this.player.on('ended', () => {\\n      this.loader.contentComplete();\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      time = this.player.currentTime;\\n    });\\n\\n    this.player.on('seeked', () => {\\n      const seekedTime = this.player.currentTime;\\n\\n      if (is.empty(this.cuePoints)) {\\n        return;\\n      }\\n\\n      this.cuePoints.forEach((cuePoint, index) => {\\n        if (time < cuePoint && cuePoint < seekedTime) {\\n          this.manager.discardAdBreak();\\n          this.cuePoints.splice(index, 1);\\n        }\\n      });\\n    });\\n\\n    // Listen to the resizing of the window. And resize ad accordingly\\n    // TODO: eventually implement ResizeObserver\\n    window.addEventListener('resize', () => {\\n      if (this.manager) {\\n        this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n      }\\n    });\\n  };\\n\\n  /**\\n   * Initialize the adsManager and start playing advertisements\\n   */\\n  play = () => {\\n    const { container } = this.player.elements;\\n\\n    if (!this.managerPromise) {\\n      this.resumeContent();\\n    }\\n\\n    // Play the requested advertisement whenever the adsManager is ready\\n    this.managerPromise\\n      .then(() => {\\n        // Set volume to match player\\n        this.manager.setVolume(this.player.volume);\\n\\n        // Initialize the container. Must be done via a user action on mobile devices\\n        this.elements.displayContainer.initialize();\\n\\n        try {\\n          if (!this.initialized) {\\n            // Initialize the ads manager. Ad rules playlist will start at this time\\n            this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);\\n\\n            // Call play to start showing the ad. Single video and overlay ads will\\n            // start at this time; the call will be ignored for ad rules\\n            this.manager.start();\\n          }\\n\\n          this.initialized = true;\\n        } catch (adError) {\\n          // An error may be thrown if there was a problem with the\\n          // VAST response\\n          this.onAdError(adError);\\n        }\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Resume our video\\n   */\\n  resumeContent = () => {\\n    // Hide the advertisement container\\n    this.elements.container.style.zIndex = '';\\n\\n    // Ad is stopped\\n    this.playing = false;\\n\\n    // Play video\\n    silencePromise(this.player.media.play());\\n  };\\n\\n  /**\\n   * Pause our video\\n   */\\n  pauseContent = () => {\\n    // Show the advertisement container\\n    this.elements.container.style.zIndex = 3;\\n\\n    // Ad is playing\\n    this.playing = true;\\n\\n    // Pause our video.\\n    this.player.media.pause();\\n  };\\n\\n  /**\\n   * Destroy the adsManager so we can grab new ads after this. If we don't then we're not\\n   * allowed to call new ads based on google policies, as they interpret this as an accidental\\n   * video requests. https://developers.google.com/interactive-\\n   * media-ads/docs/sdks/android/faq#8\\n   */\\n  cancel = () => {\\n    // Pause our video\\n    if (this.initialized) {\\n      this.resumeContent();\\n    }\\n\\n    // Tell our instance that we're done for now\\n    this.trigger('error');\\n\\n    // Re-create our adsManager\\n    this.loadAds();\\n  };\\n\\n  /**\\n   * Re-create our adsManager\\n   */\\n  loadAds = () => {\\n    // Tell our adsManager to go bye bye\\n    this.managerPromise\\n      .then(() => {\\n        // Destroy our adsManager\\n        if (this.manager) {\\n          this.manager.destroy();\\n        }\\n\\n        // Re-set our adsManager promises\\n        this.managerPromise = new Promise((resolve) => {\\n          this.on('loaded', resolve);\\n          this.player.debug.log(this.manager);\\n        });\\n        // Now that the manager has been destroyed set it to also be un-initialized\\n        this.initialized = false;\\n\\n        // Now request some new advertisements\\n        this.requestAds();\\n      })\\n      .catch(() => {});\\n  };\\n\\n  /**\\n   * Handles callbacks after an ad event was invoked\\n   * @param {String} event - Event type\\n   * @param args\\n   */\\n  trigger = (event, ...args) => {\\n    const handlers = this.events[event];\\n\\n    if (is.array(handlers)) {\\n      handlers.forEach((handler) => {\\n        if (is.function(handler)) {\\n          handler.apply(this, args);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   * @return {Ads}\\n   */\\n  on = (event, callback) => {\\n    if (!is.array(this.events[event])) {\\n      this.events[event] = [];\\n    }\\n\\n    this.events[event].push(callback);\\n\\n    return this;\\n  };\\n\\n  /**\\n   * Setup a safety timer for when the ad network doesn't respond for whatever reason.\\n   * The advertisement has 12 seconds to get its things together. We stop this timer when the\\n   * advertisement is playing, or when a user action is required to start, then we clear the\\n   * timer on ad ready\\n   * @param {Number} time\\n   * @param {String} from\\n   */\\n  startSafetyTimer = (time, from) => {\\n    this.player.debug.log(`Safety timer invoked from: ${from}`);\\n\\n    this.safetyTimer = setTimeout(() => {\\n      this.cancel();\\n      this.clearSafetyTimer('startSafetyTimer()');\\n    }, time);\\n  };\\n\\n  /**\\n   * Clear our safety timer(s)\\n   * @param {String} from\\n   */\\n  clearSafetyTimer = (from) => {\\n    if (!is.nullOrUndefined(this.safetyTimer)) {\\n      this.player.debug.log(`Safety timer cleared from: ${from}`);\\n\\n      clearTimeout(this.safetyTimer);\\n      this.safetyTimer = null;\\n    }\\n  };\\n}\\n\\nexport default Ads;\\n\",\"/**\\n * Returns a number whose value is limited to the given range.\\n *\\n * Example: limit the output of this computation to between 0 and 255\\n * (x * 255).clamp(0, 255)\\n *\\n * @param {Number} input\\n * @param {Number} min The lower boundary of the output range\\n * @param {Number} max The upper boundary of the output range\\n * @returns A number within the bounds of min and max\\n * @type Number\\n */\\nexport function clamp(input = 0, min = 0, max = 255) {\\n  return Math.min(Math.max(input, min), max);\\n}\\n\\nexport default { clamp };\\n\",\"import { createElement } from '../utils/elements';\\nimport { once } from '../utils/events';\\nimport fetch from '../utils/fetch';\\nimport is from '../utils/is';\\nimport { clamp } from '../utils/numbers';\\nimport { formatTime } from '../utils/time';\\n\\n// Arg: vttDataString example: \\\"WEBVTT\\\\n\\\\n1\\\\n00:00:05.000 --> 00:00:10.000\\\\n1080p-00001.jpg\\\"\\nconst parseVtt = (vttDataString) => {\\n  const processedList = [];\\n  const frames = vttDataString.split(/\\\\r\\\\n\\\\r\\\\n|\\\\n\\\\n|\\\\r\\\\r/);\\n\\n  frames.forEach((frame) => {\\n    const result = {};\\n    const lines = frame.split(/\\\\r\\\\n|\\\\n|\\\\r/);\\n\\n    lines.forEach((line) => {\\n      if (!is.number(result.startTime)) {\\n        // The line with start and end times on it is the first line of interest\\n        const matchTimes = line.match(\\n          /([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})( ?--> ?)([0-9]{2})?:?([0-9]{2}):([0-9]{2}).([0-9]{2,3})/,\\n        ); // Note that this currently ignores caption formatting directives that are optionally on the end of this line - fine for non-captions VTT\\n\\n        if (matchTimes) {\\n          result.startTime =\\n            Number(matchTimes[1] || 0) * 60 * 60 +\\n            Number(matchTimes[2]) * 60 +\\n            Number(matchTimes[3]) +\\n            Number(`0.${matchTimes[4]}`);\\n          result.endTime =\\n            Number(matchTimes[6] || 0) * 60 * 60 +\\n            Number(matchTimes[7]) * 60 +\\n            Number(matchTimes[8]) +\\n            Number(`0.${matchTimes[9]}`);\\n        }\\n      } else if (!is.empty(line.trim()) && is.empty(result.text)) {\\n        // If we already have the startTime, then we're definitely up to the text line(s)\\n        const lineSplit = line.trim().split('#xywh=');\\n        [result.text] = lineSplit;\\n\\n        // If there's content in lineSplit[1], then we have sprites. If not, then it's just one frame per image\\n        if (lineSplit[1]) {\\n          [result.x, result.y, result.w, result.h] = lineSplit[1].split(',');\\n        }\\n      }\\n    });\\n\\n    if (result.text) {\\n      processedList.push(result);\\n    }\\n  });\\n\\n  return processedList;\\n};\\n\\n/**\\n * Preview thumbnails for seek hover and scrubbing\\n * Seeking: Hover over the seek bar (desktop only): shows a small preview container above the seek bar\\n * Scrubbing: Click and drag the seek bar (desktop and mobile): shows the preview image over the entire video, as if the video is scrubbing at very high speed\\n *\\n * Notes:\\n * - Thumbs are set via JS settings on Plyr init, not HTML5 'track' property. Using the track property would be a bit gross, because it doesn't support custom 'kinds'. kind=metadata might be used for something else, and we want to allow multiple thumbnails tracks. Tracks must have a unique combination of 'kind' and 'label'. We would have to do something like kind=metadata,label=thumbnails1 / kind=metadata,label=thumbnails2. Square peg, round hole\\n * - VTT info: the image URL is relative to the VTT, not the current document. But if the url starts with a slash, it will naturally be relative to the current domain. https://support.jwplayer.com/articles/how-to-add-preview-thumbnails\\n * - This implementation uses multiple separate img elements. Other implementations use background-image on one element. This would be nice and simple, but Firefox and Safari have flickering issues with replacing backgrounds of larger images. It seems that YouTube perhaps only avoids this because they don't have the option for high-res previews (even the fullscreen ones, when mousedown/seeking). Images appear over the top of each other, and previous ones are discarded once the new ones have been rendered\\n */\\n\\nconst fitRatio = (ratio, outer) => {\\n  const targetRatio = outer.width / outer.height;\\n  const result = {};\\n  if (ratio > targetRatio) {\\n    result.width = outer.width;\\n    result.height = (1 / ratio) * outer.width;\\n  } else {\\n    result.height = outer.height;\\n    result.width = ratio * outer.height;\\n  }\\n\\n  return result;\\n};\\n\\nclass PreviewThumbnails {\\n  /**\\n   * PreviewThumbnails constructor.\\n   * @param {Plyr} player\\n   * @return {PreviewThumbnails}\\n   */\\n  constructor(player) {\\n    this.player = player;\\n    this.thumbnails = [];\\n    this.loaded = false;\\n    this.lastMouseMoveTime = Date.now();\\n    this.mouseDown = false;\\n    this.loadedImages = [];\\n\\n    this.elements = {\\n      thumb: {},\\n      scrubbing: {},\\n    };\\n\\n    this.load();\\n  }\\n\\n  get enabled() {\\n    return this.player.isHTML5 && this.player.isVideo && this.player.config.previewThumbnails.enabled;\\n  }\\n\\n  load = () => {\\n    // Toggle the regular seek tooltip\\n    if (this.player.elements.display.seekTooltip) {\\n      this.player.elements.display.seekTooltip.hidden = this.enabled;\\n    }\\n\\n    if (!this.enabled) return;\\n\\n    this.getThumbnails().then(() => {\\n      if (!this.enabled) {\\n        return;\\n      }\\n\\n      // Render DOM elements\\n      this.render();\\n\\n      // Check to see if thumb container size was specified manually in CSS\\n      this.determineContainerAutoSizing();\\n\\n      // Set up listeners\\n      this.listeners();\\n\\n      this.loaded = true;\\n    });\\n  };\\n\\n  // Download VTT files and parse them\\n  getThumbnails = () => {\\n    return new Promise((resolve) => {\\n      const { src } = this.player.config.previewThumbnails;\\n\\n      if (is.empty(src)) {\\n        throw new Error('Missing previewThumbnails.src config attribute');\\n      }\\n\\n      // Resolve promise\\n      const sortAndResolve = () => {\\n        // Sort smallest to biggest (e.g., [120p, 480p, 1080p])\\n        this.thumbnails.sort((x, y) => x.height - y.height);\\n\\n        this.player.debug.log('Preview thumbnails', this.thumbnails);\\n\\n        resolve();\\n      };\\n\\n      // Via callback()\\n      if (is.function(src)) {\\n        src((thumbnails) => {\\n          this.thumbnails = thumbnails;\\n          sortAndResolve();\\n        });\\n      }\\n      // VTT urls\\n      else {\\n        // If string, convert into single-element list\\n        const urls = is.string(src) ? [src] : src;\\n        // Loop through each src URL. Download and process the VTT file, storing the resulting data in this.thumbnails\\n        const promises = urls.map((u) => this.getThumbnail(u));\\n        // Resolve\\n        Promise.all(promises).then(sortAndResolve);\\n      }\\n    });\\n  };\\n\\n  // Process individual VTT file\\n  getThumbnail = (url) => {\\n    return new Promise((resolve) => {\\n      fetch(url).then((response) => {\\n        const thumbnail = {\\n          frames: parseVtt(response),\\n          height: null,\\n          urlPrefix: '',\\n        };\\n\\n        // If the URLs don't start with '/', then we need to set their relative path to be the location of the VTT file\\n        // If the URLs do start with '/', then they obviously don't need a prefix, so it will remain blank\\n        // If the thumbnail URLs start with with none of '/', 'http://' or 'https://', then we need to set their relative path to be the location of the VTT file\\n        if (\\n          !thumbnail.frames[0].text.startsWith('/') &&\\n          !thumbnail.frames[0].text.startsWith('http://') &&\\n          !thumbnail.frames[0].text.startsWith('https://')\\n        ) {\\n          thumbnail.urlPrefix = url.substring(0, url.lastIndexOf('/') + 1);\\n        }\\n\\n        // Download the first frame, so that we can determine/set the height of this thumbnailsDef\\n        const tempImage = new Image();\\n\\n        tempImage.onload = () => {\\n          thumbnail.height = tempImage.naturalHeight;\\n          thumbnail.width = tempImage.naturalWidth;\\n\\n          this.thumbnails.push(thumbnail);\\n\\n          resolve();\\n        };\\n\\n        tempImage.src = thumbnail.urlPrefix + thumbnail.frames[0].text;\\n      });\\n    });\\n  };\\n\\n  startMove = (event) => {\\n    if (!this.loaded) return;\\n\\n    if (!is.event(event) || !['touchmove', 'mousemove'].includes(event.type)) return;\\n\\n    // Wait until media has a duration\\n    if (!this.player.media.duration) return;\\n\\n    if (event.type === 'touchmove') {\\n      // Calculate seek hover position as approx video seconds\\n      this.seekTime = this.player.media.duration * (this.player.elements.inputs.seek.value / 100);\\n    } else {\\n      // Calculate seek hover position as approx video seconds\\n      const clientRect = this.player.elements.progress.getBoundingClientRect();\\n      const percentage = (100 / clientRect.width) * (event.pageX - clientRect.left);\\n      this.seekTime = this.player.media.duration * (percentage / 100);\\n\\n      if (this.seekTime < 0) {\\n        // The mousemove fires for 10+px out to the left\\n        this.seekTime = 0;\\n      }\\n\\n      if (this.seekTime > this.player.media.duration - 1) {\\n        // Took 1 second off the duration for safety, because different players can disagree on the real duration of a video\\n        this.seekTime = this.player.media.duration - 1;\\n      }\\n\\n      this.mousePosX = event.pageX;\\n\\n      // Set time text inside image container\\n      this.elements.thumb.time.innerText = formatTime(this.seekTime);\\n\\n      // Get marker point for time\\n      const point = this.player.config.markers?.points?.find(({ time: t }) => t === Math.round(this.seekTime));\\n\\n      // Append the point label to the tooltip\\n      if (point) {\\n        // this.elements.thumb.time.innerText.concat('\\\\n');\\n        this.elements.thumb.time.insertAdjacentHTML('afterbegin', `${point.label}<br>`);\\n      }\\n    }\\n\\n    // Download and show image\\n    this.showImageAtCurrentTime();\\n  };\\n\\n  endMove = () => {\\n    this.toggleThumbContainer(false, true);\\n  };\\n\\n  startScrubbing = (event) => {\\n    // Only act on left mouse button (0), or touch device (event.button does not exist or is false)\\n    if (is.nullOrUndefined(event.button) || event.button === false || event.button === 0) {\\n      this.mouseDown = true;\\n\\n      // Wait until media has a duration\\n      if (this.player.media.duration) {\\n        this.toggleScrubbingContainer(true);\\n        this.toggleThumbContainer(false, true);\\n\\n        // Download and show image\\n        this.showImageAtCurrentTime();\\n      }\\n    }\\n  };\\n\\n  endScrubbing = () => {\\n    this.mouseDown = false;\\n\\n    // Hide scrubbing preview. But wait until the video has successfully seeked before hiding the scrubbing preview\\n    if (Math.ceil(this.lastTime) === Math.ceil(this.player.media.currentTime)) {\\n      // The video was already seeked/loaded at the chosen time - hide immediately\\n      this.toggleScrubbingContainer(false);\\n    } else {\\n      // The video hasn't seeked yet. Wait for that\\n      once.call(this.player, this.player.media, 'timeupdate', () => {\\n        // Re-check mousedown - we might have already started scrubbing again\\n        if (!this.mouseDown) {\\n          this.toggleScrubbingContainer(false);\\n        }\\n      });\\n    }\\n  };\\n\\n  /**\\n   * Setup hooks for Plyr and window events\\n   */\\n  listeners = () => {\\n    // Hide thumbnail preview - on mouse click, mouse leave (in listeners.js for now), and video play/seek. All four are required, e.g., for buffering\\n    this.player.on('play', () => {\\n      this.toggleThumbContainer(false, true);\\n    });\\n\\n    this.player.on('seeked', () => {\\n      this.toggleThumbContainer(false);\\n    });\\n\\n    this.player.on('timeupdate', () => {\\n      this.lastTime = this.player.media.currentTime;\\n    });\\n  };\\n\\n  /**\\n   * Create HTML elements for image containers\\n   */\\n  render = () => {\\n    // Create HTML element: plyr__preview-thumbnail-container\\n    this.elements.thumb.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.thumbContainer,\\n    });\\n\\n    // Wrapper for the image for styling\\n    this.elements.thumb.imageContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.imageContainer,\\n    });\\n    this.elements.thumb.container.appendChild(this.elements.thumb.imageContainer);\\n\\n    // Create HTML element, parent+span: time text (e.g., 01:32:00)\\n    const timeContainer = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.timeContainer,\\n    });\\n\\n    this.elements.thumb.time = createElement('span', {}, '00:00');\\n    timeContainer.appendChild(this.elements.thumb.time);\\n\\n    this.elements.thumb.imageContainer.appendChild(timeContainer);\\n\\n    // Inject the whole thumb\\n    if (is.element(this.player.elements.progress)) {\\n      this.player.elements.progress.appendChild(this.elements.thumb.container);\\n    }\\n\\n    // Create HTML element: plyr__preview-scrubbing-container\\n    this.elements.scrubbing.container = createElement('div', {\\n      class: this.player.config.classNames.previewThumbnails.scrubbingContainer,\\n    });\\n\\n    this.player.elements.wrapper.appendChild(this.elements.scrubbing.container);\\n  };\\n\\n  destroy = () => {\\n    if (this.elements.thumb.container) {\\n      this.elements.thumb.container.remove();\\n    }\\n    if (this.elements.scrubbing.container) {\\n      this.elements.scrubbing.container.remove();\\n    }\\n  };\\n\\n  showImageAtCurrentTime = () => {\\n    if (this.mouseDown) {\\n      this.setScrubbingContainerSize();\\n    } else {\\n      this.setThumbContainerSizeAndPos();\\n    }\\n\\n    // Find the desired thumbnail index\\n    // TODO: Handle a video longer than the thumbs where thumbNum is null\\n    const thumbNum = this.thumbnails[0].frames.findIndex(\\n      (frame) => this.seekTime >= frame.startTime && this.seekTime <= frame.endTime,\\n    );\\n    const hasThumb = thumbNum >= 0;\\n    let qualityIndex = 0;\\n\\n    // Show the thumb container if we're not scrubbing\\n    if (!this.mouseDown) {\\n      this.toggleThumbContainer(hasThumb);\\n    }\\n\\n    // No matching thumb found\\n    if (!hasThumb) {\\n      return;\\n    }\\n\\n    // Check to see if we've already downloaded higher quality versions of this image\\n    this.thumbnails.forEach((thumbnail, index) => {\\n      if (this.loadedImages.includes(thumbnail.frames[thumbNum].text)) {\\n        qualityIndex = index;\\n      }\\n    });\\n\\n    // Only proceed if either thumb num or thumbfilename has changed\\n    if (thumbNum !== this.showingThumb) {\\n      this.showingThumb = thumbNum;\\n      this.loadImage(qualityIndex);\\n    }\\n  };\\n\\n  // Show the image that's currently specified in this.showingThumb\\n  loadImage = (qualityIndex = 0) => {\\n    const thumbNum = this.showingThumb;\\n    const thumbnail = this.thumbnails[qualityIndex];\\n    const { urlPrefix } = thumbnail;\\n    const frame = thumbnail.frames[thumbNum];\\n    const thumbFilename = thumbnail.frames[thumbNum].text;\\n    const thumbUrl = urlPrefix + thumbFilename;\\n\\n    if (!this.currentImageElement || this.currentImageElement.dataset.filename !== thumbFilename) {\\n      // If we're already loading a previous image, remove its onload handler - we don't want it to load after this one\\n      // Only do this if not using sprites. Without sprites we really want to show as many images as possible, as a best-effort\\n      if (this.loadingImage && this.usingSprites) {\\n        this.loadingImage.onload = null;\\n      }\\n\\n      // We're building and adding a new image. In other implementations of similar functionality (YouTube), background image\\n      // is instead used. But this causes issues with larger images in Firefox and Safari - switching between background\\n      // images causes a flicker. Putting a new image over the top does not\\n      const previewImage = new Image();\\n      previewImage.src = thumbUrl;\\n      previewImage.dataset.index = thumbNum;\\n      previewImage.dataset.filename = thumbFilename;\\n      this.showingThumbFilename = thumbFilename;\\n\\n      this.player.debug.log(`Loading image: ${thumbUrl}`);\\n\\n      // For some reason, passing the named function directly causes it to execute immediately. So I've wrapped it in an anonymous function...\\n      previewImage.onload = () => this.showImage(previewImage, frame, qualityIndex, thumbNum, thumbFilename, true);\\n      this.loadingImage = previewImage;\\n      this.removeOldImages(previewImage);\\n    } else {\\n      // Update the existing image\\n      this.showImage(this.currentImageElement, frame, qualityIndex, thumbNum, thumbFilename, false);\\n      this.currentImageElement.dataset.index = thumbNum;\\n      this.removeOldImages(this.currentImageElement);\\n    }\\n  };\\n\\n  showImage = (previewImage, frame, qualityIndex, thumbNum, thumbFilename, newImage = true) => {\\n    this.player.debug.log(\\n      `Showing thumb: ${thumbFilename}. num: ${thumbNum}. qual: ${qualityIndex}. newimg: ${newImage}`,\\n    );\\n    this.setImageSizeAndOffset(previewImage, frame);\\n\\n    if (newImage) {\\n      this.currentImageContainer.appendChild(previewImage);\\n      this.currentImageElement = previewImage;\\n\\n      if (!this.loadedImages.includes(thumbFilename)) {\\n        this.loadedImages.push(thumbFilename);\\n      }\\n    }\\n\\n    // Preload images before and after the current one\\n    // Show higher quality of the same frame\\n    // Each step here has a short time delay, and only continues if still hovering/seeking the same spot. This is to protect slow connections from overloading\\n    this.preloadNearby(thumbNum, true)\\n      .then(this.preloadNearby(thumbNum, false))\\n      .then(this.getHigherQuality(qualityIndex, previewImage, frame, thumbFilename));\\n  };\\n\\n  // Remove all preview images that aren't the designated current image\\n  removeOldImages = (currentImage) => {\\n    // Get a list of all images, convert it from a DOM list to an array\\n    Array.from(this.currentImageContainer.children).forEach((image) => {\\n      if (image.tagName.toLowerCase() !== 'img') {\\n        return;\\n      }\\n\\n      const removeDelay = this.usingSprites ? 500 : 1000;\\n\\n      if (image.dataset.index !== currentImage.dataset.index && !image.dataset.deleting) {\\n        // Wait 200ms, as the new image can take some time to show on certain browsers (even though it was downloaded before showing). This will prevent flicker, and show some generosity towards slower clients\\n        // First set attribute 'deleting' to prevent multi-handling of this on repeat firing of this function\\n        // eslint-disable-next-line no-param-reassign\\n        image.dataset.deleting = true;\\n\\n        // This has to be set before the timeout - to prevent issues switching between hover and scrub\\n        const { currentImageContainer } = this;\\n\\n        setTimeout(() => {\\n          currentImageContainer.removeChild(image);\\n          this.player.debug.log(`Removing thumb: ${image.dataset.filename}`);\\n        }, removeDelay);\\n      }\\n    });\\n  };\\n\\n  // Preload images before and after the current one. Only if the user is still hovering/seeking the same frame\\n  // This will only preload the lowest quality\\n  preloadNearby = (thumbNum, forward = true) => {\\n    return new Promise((resolve) => {\\n      setTimeout(() => {\\n        const oldThumbFilename = this.thumbnails[0].frames[thumbNum].text;\\n\\n        if (this.showingThumbFilename === oldThumbFilename) {\\n          // Find the nearest thumbs with different filenames. Sometimes it'll be the next index, but in the case of sprites, it might be 100+ away\\n          let thumbnailsClone;\\n          if (forward) {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(thumbNum);\\n          } else {\\n            thumbnailsClone = this.thumbnails[0].frames.slice(0, thumbNum).reverse();\\n          }\\n\\n          let foundOne = false;\\n\\n          thumbnailsClone.forEach((frame) => {\\n            const newThumbFilename = frame.text;\\n\\n            if (newThumbFilename !== oldThumbFilename) {\\n              // Found one with a different filename. Make sure it hasn't already been loaded on this page visit\\n              if (!this.loadedImages.includes(newThumbFilename)) {\\n                foundOne = true;\\n                this.player.debug.log(`Preloading thumb filename: ${newThumbFilename}`);\\n\\n                const { urlPrefix } = this.thumbnails[0];\\n                const thumbURL = urlPrefix + newThumbFilename;\\n                const previewImage = new Image();\\n                previewImage.src = thumbURL;\\n                previewImage.onload = () => {\\n                  this.player.debug.log(`Preloaded thumb filename: ${newThumbFilename}`);\\n                  if (!this.loadedImages.includes(newThumbFilename)) this.loadedImages.push(newThumbFilename);\\n\\n                  // We don't resolve until the thumb is loaded\\n                  resolve();\\n                };\\n              }\\n            }\\n          });\\n\\n          // If there are none to preload then we want to resolve immediately\\n          if (!foundOne) {\\n            resolve();\\n          }\\n        }\\n      }, 300);\\n    });\\n  };\\n\\n  // If user has been hovering current image for half a second, look for a higher quality one\\n  getHigherQuality = (currentQualityIndex, previewImage, frame, thumbFilename) => {\\n    if (currentQualityIndex < this.thumbnails.length - 1) {\\n      // Only use the higher quality version if it's going to look any better - if the current thumb is of a lower pixel density than the thumbnail container\\n      let previewImageHeight = previewImage.naturalHeight;\\n\\n      if (this.usingSprites) {\\n        previewImageHeight = frame.h;\\n      }\\n\\n      if (previewImageHeight < this.thumbContainerHeight) {\\n        // Recurse back to the loadImage function - show a higher quality one, but only if the viewer is on this frame for a while\\n        setTimeout(() => {\\n          // Make sure the mouse hasn't already moved on and started hovering at another image\\n          if (this.showingThumbFilename === thumbFilename) {\\n            this.player.debug.log(`Showing higher quality thumb for: ${thumbFilename}`);\\n            this.loadImage(currentQualityIndex + 1);\\n          }\\n        }, 300);\\n      }\\n    }\\n  };\\n\\n  get currentImageContainer() {\\n    return this.mouseDown ? this.elements.scrubbing.container : this.elements.thumb.imageContainer;\\n  }\\n\\n  get usingSprites() {\\n    return Object.keys(this.thumbnails[0].frames[0]).includes('w');\\n  }\\n\\n  get thumbAspectRatio() {\\n    if (this.usingSprites) {\\n      return this.thumbnails[0].frames[0].w / this.thumbnails[0].frames[0].h;\\n    }\\n\\n    return this.thumbnails[0].width / this.thumbnails[0].height;\\n  }\\n\\n  get thumbContainerHeight() {\\n    if (this.mouseDown) {\\n      const { height } = fitRatio(this.thumbAspectRatio, {\\n        width: this.player.media.clientWidth,\\n        height: this.player.media.clientHeight,\\n      });\\n      return height;\\n    }\\n\\n    // If css is used this needs to return the css height for sprites to work (see setImageSizeAndOffset)\\n    if (this.sizeSpecifiedInCSS) {\\n      return this.elements.thumb.imageContainer.clientHeight;\\n    }\\n\\n    return Math.floor(this.player.media.clientWidth / this.thumbAspectRatio / 4);\\n  }\\n\\n  get currentImageElement() {\\n    return this.mouseDown ? this.currentScrubbingImageElement : this.currentThumbnailImageElement;\\n  }\\n\\n  set currentImageElement(element) {\\n    if (this.mouseDown) {\\n      this.currentScrubbingImageElement = element;\\n    } else {\\n      this.currentThumbnailImageElement = element;\\n    }\\n  }\\n\\n  toggleThumbContainer = (toggle = false, clearShowing = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.thumbContainerShown;\\n    this.elements.thumb.container.classList.toggle(className, toggle);\\n\\n    if (!toggle && clearShowing) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  toggleScrubbingContainer = (toggle = false) => {\\n    const className = this.player.config.classNames.previewThumbnails.scrubbingContainerShown;\\n    this.elements.scrubbing.container.classList.toggle(className, toggle);\\n\\n    if (!toggle) {\\n      this.showingThumb = null;\\n      this.showingThumbFilename = null;\\n    }\\n  };\\n\\n  determineContainerAutoSizing = () => {\\n    if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) {\\n      // This will prevent auto sizing in this.setThumbContainerSizeAndPos()\\n      this.sizeSpecifiedInCSS = true;\\n    }\\n  };\\n\\n  // Set the size to be about a quarter of the size of video. Unless option dynamicSize === false, in which case it needs to be set in CSS\\n  setThumbContainerSizeAndPos = () => {\\n    const { imageContainer } = this.elements.thumb;\\n\\n    if (!this.sizeSpecifiedInCSS) {\\n      const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio);\\n      imageContainer.style.height = `${this.thumbContainerHeight}px`;\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight > 20 && imageContainer.clientWidth < 20) {\\n      const thumbWidth = Math.floor(imageContainer.clientHeight * this.thumbAspectRatio);\\n      imageContainer.style.width = `${thumbWidth}px`;\\n    } else if (imageContainer.clientHeight < 20 && imageContainer.clientWidth > 20) {\\n      const thumbHeight = Math.floor(imageContainer.clientWidth / this.thumbAspectRatio);\\n      imageContainer.style.height = `${thumbHeight}px`;\\n    }\\n\\n    this.setThumbContainerPos();\\n  };\\n\\n  setThumbContainerPos = () => {\\n    const scrubberRect = this.player.elements.progress.getBoundingClientRect();\\n    const containerRect = this.player.elements.container.getBoundingClientRect();\\n    const { container } = this.elements.thumb;\\n    // Find the lowest and highest desired left-position, so we don't slide out the side of the video container\\n    const min = containerRect.left - scrubberRect.left + 10;\\n    const max = containerRect.right - scrubberRect.left - container.clientWidth - 10;\\n    // Set preview container position to: mousepos, minus seekbar.left, minus half of previewContainer.clientWidth\\n    const position = this.mousePosX - scrubberRect.left - container.clientWidth / 2;\\n    const clamped = clamp(position, min, max);\\n\\n    // Move the popover position\\n    container.style.left = `${clamped}px`;\\n\\n    // The arrow can follow the cursor\\n    container.style.setProperty('--preview-arrow-offset', `${position - clamped}px`);\\n  };\\n\\n  // Can't use 100% width, in case the video is a different aspect ratio to the video container\\n  setScrubbingContainerSize = () => {\\n    const { width, height } = fitRatio(this.thumbAspectRatio, {\\n      width: this.player.media.clientWidth,\\n      height: this.player.media.clientHeight,\\n    });\\n    this.elements.scrubbing.container.style.width = `${width}px`;\\n    this.elements.scrubbing.container.style.height = `${height}px`;\\n  };\\n\\n  // Sprites need to be offset to the correct location\\n  setImageSizeAndOffset = (previewImage, frame) => {\\n    if (!this.usingSprites) return;\\n\\n    // Find difference between height and preview container height\\n    const multiplier = this.thumbContainerHeight / frame.h;\\n\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.height = `${previewImage.naturalHeight * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.width = `${previewImage.naturalWidth * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.left = `-${frame.x * multiplier}px`;\\n    // eslint-disable-next-line no-param-reassign\\n    previewImage.style.top = `-${frame.y * multiplier}px`;\\n  };\\n}\\n\\nexport default PreviewThumbnails;\\n\",\"// ==========================================================================\\n// Plyr source update\\n// ==========================================================================\\n\\nimport { providers } from './config/types';\\nimport html5 from './html5';\\nimport media from './media';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport support from './support';\\nimport ui from './ui';\\nimport { createElement, insertElement, removeElement } from './utils/elements';\\nimport is from './utils/is';\\nimport { getDeep } from './utils/objects';\\n\\nconst source = {\\n  // Add elements to HTML5 media (source, tracks, etc)\\n  insertElements(type, attributes) {\\n    if (is.string(attributes)) {\\n      insertElement(type, this.media, {\\n        src: attributes,\\n      });\\n    } else if (is.array(attributes)) {\\n      attributes.forEach((attribute) => {\\n        insertElement(type, this.media, attribute);\\n      });\\n    }\\n  },\\n\\n  // Update source\\n  // Sources are not checked for support so be careful\\n  change(input) {\\n    if (!getDeep(input, 'sources.length')) {\\n      this.debug.warn('Invalid source format');\\n      return;\\n    }\\n\\n    // Cancel current network requests\\n    html5.cancelRequests.call(this);\\n\\n    // Destroy instance and re-setup\\n    this.destroy.call(\\n      this,\\n      () => {\\n        // Reset quality options\\n        this.options.quality = [];\\n\\n        // Remove elements\\n        removeElement(this.media);\\n        this.media = null;\\n\\n        // Reset class name\\n        if (is.element(this.elements.container)) {\\n          this.elements.container.removeAttribute('class');\\n        }\\n\\n        // Set the type and provider\\n        const { sources, type } = input;\\n        const [{ provider = providers.html5, src }] = sources;\\n        const tagName = provider === 'html5' ? type : 'div';\\n        const attributes = provider === 'html5' ? {} : { src };\\n\\n        Object.assign(this, {\\n          provider,\\n          type,\\n          // Check for support\\n          supported: support.check(type, provider, this.config.playsinline),\\n          // Create new element\\n          media: createElement(tagName, attributes),\\n        });\\n\\n        // Inject the new element\\n        this.elements.container.appendChild(this.media);\\n\\n        // Autoplay the new source?\\n        if (is.boolean(input.autoplay)) {\\n          this.config.autoplay = input.autoplay;\\n        }\\n\\n        // Set attributes for audio and video\\n        if (this.isHTML5) {\\n          if (this.config.crossorigin) {\\n            this.media.setAttribute('crossorigin', '');\\n          }\\n          if (this.config.autoplay) {\\n            this.media.setAttribute('autoplay', '');\\n          }\\n          if (!is.empty(input.poster)) {\\n            this.poster = input.poster;\\n          }\\n          if (this.config.loop.active) {\\n            this.media.setAttribute('loop', '');\\n          }\\n          if (this.config.muted) {\\n            this.media.setAttribute('muted', '');\\n          }\\n          if (this.config.playsinline) {\\n            this.media.setAttribute('playsinline', '');\\n          }\\n        }\\n\\n        // Restore class hook\\n        ui.addStyleHook.call(this);\\n\\n        // Set new sources for html5\\n        if (this.isHTML5) {\\n          source.insertElements.call(this, 'source', sources);\\n        }\\n\\n        // Set video title\\n        this.config.title = input.title;\\n\\n        // Set up from scratch\\n        media.setup.call(this);\\n\\n        // HTML5 stuff\\n        if (this.isHTML5) {\\n          // Setup captions\\n          if (Object.keys(input).includes('tracks')) {\\n            source.insertElements.call(this, 'track', input.tracks);\\n          }\\n        }\\n\\n        // If HTML5 or embed but not fully supported, setupInterface and call ready now\\n        if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n          // Setup interface\\n          ui.build.call(this);\\n        }\\n\\n        // Load HTML5 sources\\n        if (this.isHTML5) {\\n          this.media.load();\\n        }\\n\\n        // Update previewThumbnails config & reload plugin\\n        if (!is.empty(input.previewThumbnails)) {\\n          Object.assign(this.config.previewThumbnails, input.previewThumbnails);\\n\\n          // Cleanup previewThumbnails plugin if it was loaded\\n          if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n            this.previewThumbnails.destroy();\\n            this.previewThumbnails = null;\\n          }\\n\\n          // Create new instance if it is still enabled\\n          if (this.config.previewThumbnails.enabled) {\\n            this.previewThumbnails = new PreviewThumbnails(this);\\n          }\\n        }\\n\\n        // Update the fullscreen support\\n        this.fullscreen.update();\\n      },\\n      true,\\n    );\\n  },\\n};\\n\\nexport default source;\\n\",\"// ==========================================================================\\n// Plyr\\n// plyr.js v3.7.8\\n// https://github.com/sampotts/plyr\\n// License: The MIT License (MIT)\\n// ==========================================================================\\n\\nimport captions from './captions';\\nimport defaults from './config/defaults';\\nimport { pip } from './config/states';\\nimport { getProviderByUrl, providers, types } from './config/types';\\nimport Console from './console';\\nimport controls from './controls';\\nimport Fullscreen from './fullscreen';\\nimport html5 from './html5';\\nimport Listeners from './listeners';\\nimport media from './media';\\nimport Ads from './plugins/ads';\\nimport PreviewThumbnails from './plugins/preview-thumbnails';\\nimport source from './source';\\nimport Storage from './storage';\\nimport support from './support';\\nimport ui from './ui';\\nimport { closest } from './utils/arrays';\\nimport { createElement, hasClass, removeElement, replaceElement, toggleClass, wrap } from './utils/elements';\\nimport { off, on, once, triggerEvent, unbindListeners } from './utils/events';\\nimport is from './utils/is';\\nimport loadSprite from './utils/load-sprite';\\nimport { clamp } from './utils/numbers';\\nimport { cloneDeep, extend } from './utils/objects';\\nimport { silencePromise } from './utils/promise';\\nimport { getAspectRatio, reduceAspectRatio, setAspectRatio, validateAspectRatio } from './utils/style';\\nimport { parseUrl } from './utils/urls';\\n\\n// Private properties\\n// TODO: Use a WeakMap for private globals\\n// const globals = new WeakMap();\\n\\n// Plyr instance\\nclass Plyr {\\n  constructor(target, options) {\\n    this.timers = {};\\n\\n    // State\\n    this.ready = false;\\n    this.loading = false;\\n    this.failed = false;\\n\\n    // Touch device\\n    this.touch = support.touch;\\n\\n    // Set the media element\\n    this.media = target;\\n\\n    // String selector passed\\n    if (is.string(this.media)) {\\n      this.media = document.querySelectorAll(this.media);\\n    }\\n\\n    // jQuery, NodeList or Array passed, use first element\\n    if ((window.jQuery && this.media instanceof jQuery) || is.nodeList(this.media) || is.array(this.media)) {\\n      // eslint-disable-next-line\\n      this.media = this.media[0];\\n    }\\n\\n    // Set config\\n    this.config = extend(\\n      {},\\n      defaults,\\n      Plyr.defaults,\\n      options || {},\\n      (() => {\\n        try {\\n          return JSON.parse(this.media.getAttribute('data-plyr-config'));\\n        } catch (_) {\\n          return {};\\n        }\\n      })(),\\n    );\\n\\n    // Elements cache\\n    this.elements = {\\n      container: null,\\n      fullscreen: null,\\n      captions: null,\\n      buttons: {},\\n      display: {},\\n      progress: {},\\n      inputs: {},\\n      settings: {\\n        popup: null,\\n        menu: null,\\n        panels: {},\\n        buttons: {},\\n      },\\n    };\\n\\n    // Captions\\n    this.captions = {\\n      active: null,\\n      currentTrack: -1,\\n      meta: new WeakMap(),\\n    };\\n\\n    // Fullscreen\\n    this.fullscreen = {\\n      active: false,\\n    };\\n\\n    // Options\\n    this.options = {\\n      speed: [],\\n      quality: [],\\n    };\\n\\n    // Debugging\\n    // TODO: move to globals\\n    this.debug = new Console(this.config.debug);\\n\\n    // Log config options and support\\n    this.debug.log('Config', this.config);\\n    this.debug.log('Support', support);\\n\\n    // We need an element to setup\\n    if (is.nullOrUndefined(this.media) || !is.element(this.media)) {\\n      this.debug.error('Setup failed: no suitable element passed');\\n      return;\\n    }\\n\\n    // Bail if the element is initialized\\n    if (this.media.plyr) {\\n      this.debug.warn('Target already setup');\\n      return;\\n    }\\n\\n    // Bail if not enabled\\n    if (!this.config.enabled) {\\n      this.debug.error('Setup failed: disabled by config');\\n      return;\\n    }\\n\\n    // Bail if disabled or no basic support\\n    // You may want to disable certain UAs etc\\n    if (!support.check().api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    // Cache original element state for .destroy()\\n    const clone = this.media.cloneNode(true);\\n    clone.autoplay = false;\\n    this.elements.original = clone;\\n\\n    // Set media type based on tag or data attribute\\n    // Supported: video, audio, vimeo, youtube\\n    const type = this.media.tagName.toLowerCase();\\n    // Embed properties\\n    let iframe = null;\\n    let url = null;\\n\\n    // Different setup based on type\\n    switch (type) {\\n      case 'div':\\n        // Find the frame\\n        iframe = this.media.querySelector('iframe');\\n\\n        // <iframe> type\\n        if (is.element(iframe)) {\\n          // Detect provider\\n          url = parseUrl(iframe.getAttribute('src'));\\n          this.provider = getProviderByUrl(url.toString());\\n\\n          // Rework elements\\n          this.elements.container = this.media;\\n          this.media = iframe;\\n\\n          // Reset classname\\n          this.elements.container.className = '';\\n\\n          // Get attributes from URL and set config\\n          if (url.search.length) {\\n            const truthy = ['1', 'true'];\\n\\n            if (truthy.includes(url.searchParams.get('autoplay'))) {\\n              this.config.autoplay = true;\\n            }\\n            if (truthy.includes(url.searchParams.get('loop'))) {\\n              this.config.loop.active = true;\\n            }\\n\\n            // TODO: replace fullscreen.iosNative with this playsinline config option\\n            // YouTube requires the playsinline in the URL\\n            if (this.isYouTube) {\\n              this.config.playsinline = truthy.includes(url.searchParams.get('playsinline'));\\n              this.config.youtube.hl = url.searchParams.get('hl'); // TODO: Should this be setting language?\\n            } else {\\n              this.config.playsinline = true;\\n            }\\n          }\\n        } else {\\n          // <div> with attributes\\n          this.provider = this.media.getAttribute(this.config.attributes.embed.provider);\\n\\n          // Remove attribute\\n          this.media.removeAttribute(this.config.attributes.embed.provider);\\n        }\\n\\n        // Unsupported or missing provider\\n        if (is.empty(this.provider) || !Object.values(providers).includes(this.provider)) {\\n          this.debug.error('Setup failed: Invalid provider');\\n          return;\\n        }\\n\\n        // Audio will come later for external providers\\n        this.type = types.video;\\n\\n        break;\\n\\n      case 'video':\\n      case 'audio':\\n        this.type = type;\\n        this.provider = providers.html5;\\n\\n        // Get config from attributes\\n        if (this.media.hasAttribute('crossorigin')) {\\n          this.config.crossorigin = true;\\n        }\\n        if (this.media.hasAttribute('autoplay')) {\\n          this.config.autoplay = true;\\n        }\\n        if (this.media.hasAttribute('playsinline') || this.media.hasAttribute('webkit-playsinline')) {\\n          this.config.playsinline = true;\\n        }\\n        if (this.media.hasAttribute('muted')) {\\n          this.config.muted = true;\\n        }\\n        if (this.media.hasAttribute('loop')) {\\n          this.config.loop.active = true;\\n        }\\n\\n        break;\\n\\n      default:\\n        this.debug.error('Setup failed: unsupported type');\\n        return;\\n    }\\n\\n    // Check for support again but with type\\n    this.supported = support.check(this.type, this.provider);\\n\\n    // If no support for even API, bail\\n    if (!this.supported.api) {\\n      this.debug.error('Setup failed: no support');\\n      return;\\n    }\\n\\n    this.eventListeners = [];\\n\\n    // Create listeners\\n    this.listeners = new Listeners(this);\\n\\n    // Setup local storage for user settings\\n    this.storage = new Storage(this);\\n\\n    // Store reference\\n    this.media.plyr = this;\\n\\n    // Wrap media\\n    if (!is.element(this.elements.container)) {\\n      this.elements.container = createElement('div');\\n      wrap(this.media, this.elements.container);\\n    }\\n\\n    // Migrate custom properties from media to container (so they work 😉)\\n    ui.migrateStyles.call(this);\\n\\n    // Add style hook\\n    ui.addStyleHook.call(this);\\n\\n    // Setup media\\n    media.setup.call(this);\\n\\n    // Listen for events if debugging\\n    if (this.config.debug) {\\n      on.call(this, this.elements.container, this.config.events.join(' '), (event) => {\\n        this.debug.log(`event: ${event.type}`);\\n      });\\n    }\\n\\n    // Setup fullscreen\\n    this.fullscreen = new Fullscreen(this);\\n\\n    // Setup interface\\n    // If embed but not fully supported, build interface now to avoid flash of controls\\n    if (this.isHTML5 || (this.isEmbed && !this.supported.ui)) {\\n      ui.build.call(this);\\n    }\\n\\n    // Container listeners\\n    this.listeners.container();\\n\\n    // Global listeners\\n    this.listeners.global();\\n\\n    // Setup ads if provided\\n    if (this.config.ads.enabled) {\\n      this.ads = new Ads(this);\\n    }\\n\\n    // Autoplay if required\\n    if (this.isHTML5 && this.config.autoplay) {\\n      this.once('canplay', () => silencePromise(this.play()));\\n    }\\n\\n    // Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek\\n    this.lastSeekTime = 0;\\n\\n    // Setup preview thumbnails if enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  // ---------------------------------------\\n  // API\\n  // ---------------------------------------\\n\\n  /**\\n   * Types and provider helpers\\n   */\\n  get isHTML5() {\\n    return this.provider === providers.html5;\\n  }\\n\\n  get isEmbed() {\\n    return this.isYouTube || this.isVimeo;\\n  }\\n\\n  get isYouTube() {\\n    return this.provider === providers.youtube;\\n  }\\n\\n  get isVimeo() {\\n    return this.provider === providers.vimeo;\\n  }\\n\\n  get isVideo() {\\n    return this.type === types.video;\\n  }\\n\\n  get isAudio() {\\n    return this.type === types.audio;\\n  }\\n\\n  /**\\n   * Play the media, or play the advertisement (if they are not blocked)\\n   */\\n  play = () => {\\n    if (!is.function(this.media.play)) {\\n      return null;\\n    }\\n\\n    // Intecept play with ads\\n    if (this.ads && this.ads.enabled) {\\n      this.ads.managerPromise.then(() => this.ads.play()).catch(() => silencePromise(this.media.play()));\\n    }\\n\\n    // Return the promise (for HTML5)\\n    return this.media.play();\\n  };\\n\\n  /**\\n   * Pause the media\\n   */\\n  pause = () => {\\n    if (!this.playing || !is.function(this.media.pause)) {\\n      return null;\\n    }\\n\\n    return this.media.pause();\\n  };\\n\\n  /**\\n   * Get playing state\\n   */\\n  get playing() {\\n    return Boolean(this.ready && !this.paused && !this.ended);\\n  }\\n\\n  /**\\n   * Get paused state\\n   */\\n  get paused() {\\n    return Boolean(this.media.paused);\\n  }\\n\\n  /**\\n   * Get stopped state\\n   */\\n  get stopped() {\\n    return Boolean(this.paused && this.currentTime === 0);\\n  }\\n\\n  /**\\n   * Get ended state\\n   */\\n  get ended() {\\n    return Boolean(this.media.ended);\\n  }\\n\\n  /**\\n   * Toggle playback based on current status\\n   * @param {Boolean} input\\n   */\\n  togglePlay = (input) => {\\n    // Toggle based on current state if nothing passed\\n    const toggle = is.boolean(input) ? input : !this.playing;\\n\\n    if (toggle) {\\n      return this.play();\\n    }\\n\\n    return this.pause();\\n  };\\n\\n  /**\\n   * Stop playback\\n   */\\n  stop = () => {\\n    if (this.isHTML5) {\\n      this.pause();\\n      this.restart();\\n    } else if (is.function(this.media.stop)) {\\n      this.media.stop();\\n    }\\n  };\\n\\n  /**\\n   * Restart playback\\n   */\\n  restart = () => {\\n    this.currentTime = 0;\\n  };\\n\\n  /**\\n   * Rewind\\n   * @param {Number} seekTime - how far to rewind in seconds. Defaults to the config.seekTime\\n   */\\n  rewind = (seekTime) => {\\n    this.currentTime -= is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Fast forward\\n   * @param {Number} seekTime - how far to fast forward in seconds. Defaults to the config.seekTime\\n   */\\n  forward = (seekTime) => {\\n    this.currentTime += is.number(seekTime) ? seekTime : this.config.seekTime;\\n  };\\n\\n  /**\\n   * Seek to a time\\n   * @param {Number} input - where to seek to in seconds. Defaults to 0 (the start)\\n   */\\n  set currentTime(input) {\\n    // Bail if media duration isn't available yet\\n    if (!this.duration) {\\n      return;\\n    }\\n\\n    // Validate input\\n    const inputIsValid = is.number(input) && input > 0;\\n\\n    // Set\\n    this.media.currentTime = inputIsValid ? Math.min(input, this.duration) : 0;\\n\\n    // Logging\\n    this.debug.log(`Seeking to ${this.currentTime} seconds`);\\n  }\\n\\n  /**\\n   * Get current time\\n   */\\n  get currentTime() {\\n    return Number(this.media.currentTime);\\n  }\\n\\n  /**\\n   * Get buffered\\n   */\\n  get buffered() {\\n    const { buffered } = this.media;\\n\\n    // YouTube / Vimeo return a float between 0-1\\n    if (is.number(buffered)) {\\n      return buffered;\\n    }\\n\\n    // HTML5\\n    // TODO: Handle buffered chunks of the media\\n    // (i.e. seek to another section buffers only that section)\\n    if (buffered && buffered.length && this.duration > 0) {\\n      return buffered.end(0) / this.duration;\\n    }\\n\\n    return 0;\\n  }\\n\\n  /**\\n   * Get seeking status\\n   */\\n  get seeking() {\\n    return Boolean(this.media.seeking);\\n  }\\n\\n  /**\\n   * Get the duration of the current media\\n   */\\n  get duration() {\\n    // Faux duration set via config\\n    const fauxDuration = parseFloat(this.config.duration);\\n    // Media duration can be NaN or Infinity before the media has loaded\\n    const realDuration = (this.media || {}).duration;\\n    const duration = !is.number(realDuration) || realDuration === Infinity ? 0 : realDuration;\\n\\n    // If config duration is funky, use regular duration\\n    return fauxDuration || duration;\\n  }\\n\\n  /**\\n   * Set the player volume\\n   * @param {Number} value - must be between 0 and 1. Defaults to the value from local storage and config.volume if not set in storage\\n   */\\n  set volume(value) {\\n    let volume = value;\\n    const max = 1;\\n    const min = 0;\\n\\n    if (is.string(volume)) {\\n      volume = Number(volume);\\n    }\\n\\n    // Load volume from storage if no value specified\\n    if (!is.number(volume)) {\\n      volume = this.storage.get('volume');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.number(volume)) {\\n      ({ volume } = this.config);\\n    }\\n\\n    // Maximum is volumeMax\\n    if (volume > max) {\\n      volume = max;\\n    }\\n    // Minimum is volumeMin\\n    if (volume < min) {\\n      volume = min;\\n    }\\n\\n    // Update config\\n    this.config.volume = volume;\\n\\n    // Set the player volume\\n    this.media.volume = volume;\\n\\n    // If muted, and we're increasing volume manually, reset muted state\\n    if (!is.empty(value) && this.muted && volume > 0) {\\n      this.muted = false;\\n    }\\n  }\\n\\n  /**\\n   * Get the current player volume\\n   */\\n  get volume() {\\n    return Number(this.media.volume);\\n  }\\n\\n  /**\\n   * Increase volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  increaseVolume = (step) => {\\n    const volume = this.media.muted ? 0 : this.volume;\\n    this.volume = volume + (is.number(step) ? step : 0);\\n  };\\n\\n  /**\\n   * Decrease volume\\n   * @param {Boolean} step - How much to decrease by (between 0 and 1)\\n   */\\n  decreaseVolume = (step) => {\\n    this.increaseVolume(-step);\\n  };\\n\\n  /**\\n   * Set muted state\\n   * @param {Boolean} mute\\n   */\\n  set muted(mute) {\\n    let toggle = mute;\\n\\n    // Load muted state from storage\\n    if (!is.boolean(toggle)) {\\n      toggle = this.storage.get('muted');\\n    }\\n\\n    // Use config if all else fails\\n    if (!is.boolean(toggle)) {\\n      toggle = this.config.muted;\\n    }\\n\\n    // Update config\\n    this.config.muted = toggle;\\n\\n    // Set mute on the player\\n    this.media.muted = toggle;\\n  }\\n\\n  /**\\n   * Get current muted state\\n   */\\n  get muted() {\\n    return Boolean(this.media.muted);\\n  }\\n\\n  /**\\n   * Check if the media has audio\\n   */\\n  get hasAudio() {\\n    // Assume yes for all non HTML5 (as we can't tell...)\\n    if (!this.isHTML5) {\\n      return true;\\n    }\\n\\n    if (this.isAudio) {\\n      return true;\\n    }\\n\\n    // Get audio tracks\\n    return (\\n      Boolean(this.media.mozHasAudio) ||\\n      Boolean(this.media.webkitAudioDecodedByteCount) ||\\n      Boolean(this.media.audioTracks && this.media.audioTracks.length)\\n    );\\n  }\\n\\n  /**\\n   * Set playback speed\\n   * @param {Number} input - the speed of playback (0.5-2.0)\\n   */\\n  set speed(input) {\\n    let speed = null;\\n\\n    if (is.number(input)) {\\n      speed = input;\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.storage.get('speed');\\n    }\\n\\n    if (!is.number(speed)) {\\n      speed = this.config.speed.selected;\\n    }\\n\\n    // Clamp to min/max\\n    const { minimumSpeed: min, maximumSpeed: max } = this;\\n    speed = clamp(speed, min, max);\\n\\n    // Update config\\n    this.config.speed.selected = speed;\\n\\n    // Set media speed\\n    setTimeout(() => {\\n      if (this.media) {\\n        this.media.playbackRate = speed;\\n      }\\n    }, 0);\\n  }\\n\\n  /**\\n   * Get current playback speed\\n   */\\n  get speed() {\\n    return Number(this.media.playbackRate);\\n  }\\n\\n  /**\\n   * Get the minimum allowed speed\\n   */\\n  get minimumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.min(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 0.5;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 0.0625;\\n  }\\n\\n  /**\\n   * Get the maximum allowed speed\\n   */\\n  get maximumSpeed() {\\n    if (this.isYouTube) {\\n      // https://developers.google.com/youtube/iframe_api_reference#setPlaybackRate\\n      return Math.max(...this.options.speed);\\n    }\\n\\n    if (this.isVimeo) {\\n      // https://github.com/vimeo/player.js/#setplaybackrateplaybackrate-number-promisenumber-rangeerrorerror\\n      return 2;\\n    }\\n\\n    // https://stackoverflow.com/a/32320020/1191319\\n    return 16;\\n  }\\n\\n  /**\\n   * Set playback quality\\n   * Currently HTML5 & YouTube only\\n   * @param {Number} input - Quality level\\n   */\\n  set quality(input) {\\n    const config = this.config.quality;\\n    const options = this.options.quality;\\n\\n    if (!options.length) {\\n      return;\\n    }\\n\\n    let quality = [\\n      !is.empty(input) && Number(input),\\n      this.storage.get('quality'),\\n      config.selected,\\n      config.default,\\n    ].find(is.number);\\n\\n    let updateStorage = true;\\n\\n    if (!options.includes(quality)) {\\n      const value = closest(options, quality);\\n      this.debug.warn(`Unsupported quality option: ${quality}, using ${value} instead`);\\n      quality = value;\\n\\n      // Don't update storage if quality is not supported\\n      updateStorage = false;\\n    }\\n\\n    // Update config\\n    config.selected = quality;\\n\\n    // Set quality\\n    this.media.quality = quality;\\n\\n    // Save to storage\\n    if (updateStorage) {\\n      this.storage.set({ quality });\\n    }\\n  }\\n\\n  /**\\n   * Get current quality level\\n   */\\n  get quality() {\\n    return this.media.quality;\\n  }\\n\\n  /**\\n   * Toggle loop\\n   * TODO: Finish fancy new logic. Set the indicator on load as user may pass loop as config\\n   * @param {Boolean} input - Whether to loop or not\\n   */\\n  set loop(input) {\\n    const toggle = is.boolean(input) ? input : this.config.loop.active;\\n    this.config.loop.active = toggle;\\n    this.media.loop = toggle;\\n\\n    // Set default to be a true toggle\\n    /* const type = ['start', 'end', 'all', 'none', 'toggle'].includes(input) ? input : 'toggle';\\n\\n        switch (type) {\\n            case 'start':\\n                if (this.config.loop.end && this.config.loop.end <= this.currentTime) {\\n                    this.config.loop.end = null;\\n                }\\n                this.config.loop.start = this.currentTime;\\n                // this.config.loop.indicator.start = this.elements.display.played.value;\\n                break;\\n\\n            case 'end':\\n                if (this.config.loop.start >= this.currentTime) {\\n                    return this;\\n                }\\n                this.config.loop.end = this.currentTime;\\n                // this.config.loop.indicator.end = this.elements.display.played.value;\\n                break;\\n\\n            case 'all':\\n                this.config.loop.start = 0;\\n                this.config.loop.end = this.duration - 2;\\n                this.config.loop.indicator.start = 0;\\n                this.config.loop.indicator.end = 100;\\n                break;\\n\\n            case 'toggle':\\n                if (this.config.loop.active) {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = null;\\n                } else {\\n                    this.config.loop.start = 0;\\n                    this.config.loop.end = this.duration - 2;\\n                }\\n                break;\\n\\n            default:\\n                this.config.loop.start = 0;\\n                this.config.loop.end = null;\\n                break;\\n        } */\\n  }\\n\\n  /**\\n   * Get current loop state\\n   */\\n  get loop() {\\n    return Boolean(this.media.loop);\\n  }\\n\\n  /**\\n   * Set new media source\\n   * @param {Object} input - The new source object (see docs)\\n   */\\n  set source(input) {\\n    source.change.call(this, input);\\n  }\\n\\n  /**\\n   * Get current source\\n   */\\n  get source() {\\n    return this.media.currentSrc;\\n  }\\n\\n  /**\\n   * Get a download URL (either source or custom)\\n   */\\n  get download() {\\n    const { download } = this.config.urls;\\n\\n    return is.url(download) ? download : this.source;\\n  }\\n\\n  /**\\n   * Set the download URL\\n   */\\n  set download(input) {\\n    if (!is.url(input)) {\\n      return;\\n    }\\n\\n    this.config.urls.download = input;\\n\\n    controls.setDownloadUrl.call(this);\\n  }\\n\\n  /**\\n   * Set the poster image for a video\\n   * @param {String} input - the URL for the new poster image\\n   */\\n  set poster(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Poster can only be set for video');\\n      return;\\n    }\\n\\n    ui.setPoster.call(this, input, false).catch(() => {});\\n  }\\n\\n  /**\\n   * Get the current poster image\\n   */\\n  get poster() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    return this.media.getAttribute('poster') || this.media.getAttribute('data-poster');\\n  }\\n\\n  /**\\n   * Get the current aspect ratio in use\\n   */\\n  get ratio() {\\n    if (!this.isVideo) {\\n      return null;\\n    }\\n\\n    const ratio = reduceAspectRatio(getAspectRatio.call(this));\\n\\n    return is.array(ratio) ? ratio.join(':') : ratio;\\n  }\\n\\n  /**\\n   * Set video aspect ratio\\n   */\\n  set ratio(input) {\\n    if (!this.isVideo) {\\n      this.debug.warn('Aspect ratio can only be set for video');\\n      return;\\n    }\\n\\n    if (!is.string(input) || !validateAspectRatio(input)) {\\n      this.debug.error(`Invalid aspect ratio specified (${input})`);\\n      return;\\n    }\\n\\n    this.config.ratio = reduceAspectRatio(input);\\n\\n    setAspectRatio.call(this);\\n  }\\n\\n  /**\\n   * Set the autoplay state\\n   * @param {Boolean} input - Whether to autoplay or not\\n   */\\n  set autoplay(input) {\\n    this.config.autoplay = is.boolean(input) ? input : this.config.autoplay;\\n  }\\n\\n  /**\\n   * Get the current autoplay state\\n   */\\n  get autoplay() {\\n    return Boolean(this.config.autoplay);\\n  }\\n\\n  /**\\n   * Toggle captions\\n   * @param {Boolean} input - Whether to enable captions\\n   */\\n  toggleCaptions(input) {\\n    captions.toggle.call(this, input, false);\\n  }\\n\\n  /**\\n   * Set the caption track by index\\n   * @param {Number} input - Caption index\\n   */\\n  set currentTrack(input) {\\n    captions.set.call(this, input, false);\\n    captions.setup.call(this);\\n  }\\n\\n  /**\\n   * Get the current caption track index (-1 if disabled)\\n   */\\n  get currentTrack() {\\n    const { toggled, currentTrack } = this.captions;\\n    return toggled ? currentTrack : -1;\\n  }\\n\\n  /**\\n   * Set the wanted language for captions\\n   * Since tracks can be added later it won't update the actual caption track until there is a matching track\\n   * @param {String} input - Two character ISO language code (e.g. EN, FR, PT, etc)\\n   */\\n  set language(input) {\\n    captions.setLanguage.call(this, input, false);\\n  }\\n\\n  /**\\n   * Get the current track's language\\n   */\\n  get language() {\\n    return (captions.getCurrentTrack.call(this) || {}).language;\\n  }\\n\\n  /**\\n   * Toggle picture-in-picture playback on WebKit/MacOS\\n   * TODO: update player with state, support, enabled\\n   * TODO: detect outside changes\\n   */\\n  set pip(input) {\\n    // Bail if no support\\n    if (!support.pip) {\\n      return;\\n    }\\n\\n    // Toggle based on current state if not passed\\n    const toggle = is.boolean(input) ? input : !this.pip;\\n\\n    // Toggle based on current state\\n    // Safari\\n    if (is.function(this.media.webkitSetPresentationMode)) {\\n      this.media.webkitSetPresentationMode(toggle ? pip.active : pip.inactive);\\n    }\\n\\n    // Chrome\\n    if (is.function(this.media.requestPictureInPicture)) {\\n      if (!this.pip && toggle) {\\n        this.media.requestPictureInPicture();\\n      } else if (this.pip && !toggle) {\\n        document.exitPictureInPicture();\\n      }\\n    }\\n  }\\n\\n  /**\\n   * Get the current picture-in-picture state\\n   */\\n  get pip() {\\n    if (!support.pip) {\\n      return null;\\n    }\\n\\n    // Safari\\n    if (!is.empty(this.media.webkitPresentationMode)) {\\n      return this.media.webkitPresentationMode === pip.active;\\n    }\\n\\n    // Chrome\\n    return this.media === document.pictureInPictureElement;\\n  }\\n\\n  /**\\n   * Sets the preview thumbnails for the current source\\n   */\\n  setPreviewThumbnails(thumbnailSource) {\\n    if (this.previewThumbnails && this.previewThumbnails.loaded) {\\n      this.previewThumbnails.destroy();\\n      this.previewThumbnails = null;\\n    }\\n\\n    Object.assign(this.config.previewThumbnails, thumbnailSource);\\n\\n    // Create new instance if it is still enabled\\n    if (this.config.previewThumbnails.enabled) {\\n      this.previewThumbnails = new PreviewThumbnails(this);\\n    }\\n  }\\n\\n  /**\\n   * Trigger the airplay dialog\\n   * TODO: update player with state, support, enabled\\n   */\\n  airplay = () => {\\n    // Show dialog if supported\\n    if (support.airplay) {\\n      this.media.webkitShowPlaybackTargetPicker();\\n    }\\n  };\\n\\n  /**\\n   * Toggle the player controls\\n   * @param {Boolean} [toggle] - Whether to show the controls\\n   */\\n  toggleControls = (toggle) => {\\n    // Don't toggle if missing UI support or if it's audio\\n    if (this.supported.ui && !this.isAudio) {\\n      // Get state before change\\n      const isHidden = hasClass(this.elements.container, this.config.classNames.hideControls);\\n      // Negate the argument if not undefined since adding the class to hides the controls\\n      const force = typeof toggle === 'undefined' ? undefined : !toggle;\\n      // Apply and get updated state\\n      const hiding = toggleClass(this.elements.container, this.config.classNames.hideControls, force);\\n\\n      // Close menu\\n      if (\\n        hiding &&\\n        is.array(this.config.controls) &&\\n        this.config.controls.includes('settings') &&\\n        !is.empty(this.config.settings)\\n      ) {\\n        controls.toggleMenu.call(this, false);\\n      }\\n\\n      // Trigger event on change\\n      if (hiding !== isHidden) {\\n        const eventName = hiding ? 'controlshidden' : 'controlsshown';\\n        triggerEvent.call(this, this.media, eventName);\\n      }\\n\\n      return !hiding;\\n    }\\n\\n    return false;\\n  };\\n\\n  /**\\n   * Add event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  on = (event, callback) => {\\n    on.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Add event listeners once\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  once = (event, callback) => {\\n    once.call(this, this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Remove event listeners\\n   * @param {String} event - Event type\\n   * @param {Function} callback - Callback for when event occurs\\n   */\\n  off = (event, callback) => {\\n    off(this.elements.container, event, callback);\\n  };\\n\\n  /**\\n   * Destroy an instance\\n   * Event listeners are removed when elements are removed\\n   * http://stackoverflow.com/questions/12528049/if-a-dom-element-is-removed-are-its-listeners-also-removed-from-memory\\n   * @param {Function} callback - Callback for when destroy is complete\\n   * @param {Boolean} soft - Whether it's a soft destroy (for source changes etc)\\n   */\\n  destroy = (callback, soft = false) => {\\n    if (!this.ready) {\\n      return;\\n    }\\n\\n    const done = () => {\\n      // Reset overflow (incase destroyed while in fullscreen)\\n      document.body.style.overflow = '';\\n\\n      // GC for embed\\n      this.embed = null;\\n\\n      // If it's a soft destroy, make minimal changes\\n      if (soft) {\\n        if (Object.keys(this.elements).length) {\\n          // Remove elements\\n          removeElement(this.elements.buttons.play);\\n          removeElement(this.elements.captions);\\n          removeElement(this.elements.controls);\\n          removeElement(this.elements.wrapper);\\n\\n          // Clear for GC\\n          this.elements.buttons.play = null;\\n          this.elements.captions = null;\\n          this.elements.controls = null;\\n          this.elements.wrapper = null;\\n        }\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback();\\n        }\\n      } else {\\n        // Unbind listeners\\n        unbindListeners.call(this);\\n\\n        // Cancel current network requests\\n        html5.cancelRequests.call(this);\\n\\n        // Replace the container with the original element provided\\n        replaceElement(this.elements.original, this.elements.container);\\n\\n        // Event\\n        triggerEvent.call(this, this.elements.original, 'destroyed', true);\\n\\n        // Callback\\n        if (is.function(callback)) {\\n          callback.call(this.elements.original);\\n        }\\n\\n        // Reset state\\n        this.ready = false;\\n\\n        // Clear for garbage collection\\n        setTimeout(() => {\\n          this.elements = null;\\n          this.media = null;\\n        }, 200);\\n      }\\n    };\\n\\n    // Stop playback\\n    this.stop();\\n\\n    // Clear timeouts\\n    clearTimeout(this.timers.loading);\\n    clearTimeout(this.timers.controls);\\n    clearTimeout(this.timers.resized);\\n\\n    // Provider specific stuff\\n    if (this.isHTML5) {\\n      // Restore native video controls\\n      ui.toggleNativeControls.call(this, true);\\n\\n      // Clean up\\n      done();\\n    } else if (this.isYouTube) {\\n      // Clear timers\\n      clearInterval(this.timers.buffering);\\n      clearInterval(this.timers.playing);\\n\\n      // Destroy YouTube API\\n      if (this.embed !== null && is.function(this.embed.destroy)) {\\n        this.embed.destroy();\\n      }\\n\\n      // Clean up\\n      done();\\n    } else if (this.isVimeo) {\\n      // Destroy Vimeo API\\n      // then clean up (wait, to prevent postmessage errors)\\n      if (this.embed !== null) {\\n        this.embed.unload().then(done);\\n      }\\n\\n      // Vimeo does not always return\\n      setTimeout(done, 200);\\n    }\\n  };\\n\\n  /**\\n   * Check for support for a mime type (HTML5 only)\\n   * @param {String} type - Mime type\\n   */\\n  supports = (type) => support.mime.call(this, type);\\n\\n  /**\\n   * Check for support\\n   * @param {String} type - Player type (audio/video)\\n   * @param {String} provider - Provider (html5/youtube/vimeo)\\n   */\\n  static supported(type, provider) {\\n    return support.check(type, provider);\\n  }\\n\\n  /**\\n   * Load an SVG sprite into the page\\n   * @param {String} url - URL for the SVG sprite\\n   * @param {String} [id] - Unique ID\\n   */\\n  static loadSprite(url, id) {\\n    return loadSprite(url, id);\\n  }\\n\\n  /**\\n   * Setup multiple instances\\n   * @param {*} selector\\n   * @param {Object} options\\n   */\\n  static setup(selector, options = {}) {\\n    let targets = null;\\n\\n    if (is.string(selector)) {\\n      targets = Array.from(document.querySelectorAll(selector));\\n    } else if (is.nodeList(selector)) {\\n      targets = Array.from(selector);\\n    } else if (is.array(selector)) {\\n      targets = selector.filter(is.element);\\n    }\\n\\n    if (is.empty(targets)) {\\n      return null;\\n    }\\n\\n    return targets.map((t) => new Plyr(t, options));\\n  }\\n}\\n\\nPlyr.defaults = cloneDeep(defaults);\\n\\nexport default Plyr;\\n\"]}\n\\ No newline at end of file\ndiff --git a/node_modules/plyr/dist/plyr.polyfilled.mjs b/node_modules/plyr/dist/plyr.polyfilled.mjs\nindex 47ec1cc..1b73d86 100644\n--- a/node_modules/plyr/dist/plyr.polyfilled.mjs\n+++ b/node_modules/plyr/dist/plyr.polyfilled.mjs\n@@ -4009,21 +4009,7 @@ const defaults = {\n     }\n   },\n   // URLs\n-  urls: {\n-    download: null,\n-    vimeo: {\n-      sdk: 'https://player.vimeo.com/api/player.js',\n-      iframe: 'https://player.vimeo.com/video/{0}?{1}',\n-      api: 'https://vimeo.com/api/oembed.json?url={0}'\n-    },\n-    youtube: {\n-      sdk: 'https://www.youtube.com/iframe_api',\n-      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}'\n-    },\n-    googleIMA: {\n-      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js'\n-    }\n-  },\n+  urls: null,\n   // Custom control listeners\n   listeners: {\n     seek: null,\ndiff --git a/node_modules/plyr/src/js/config/defaults.js b/node_modules/plyr/src/js/config/defaults.js\nindex 2d88d58..d541220 100644\n--- a/node_modules/plyr/src/js/config/defaults.js\n+++ b/node_modules/plyr/src/js/config/defaults.js\n@@ -194,21 +194,7 @@ const defaults = {\n   },\n \n   // URLs\n-  urls: {\n-    download: null,\n-    vimeo: {\n-      sdk: 'https://player.vimeo.com/api/player.js',\n-      iframe: 'https://player.vimeo.com/video/{0}?{1}',\n-      api: 'https://vimeo.com/api/oembed.json?url={0}',\n-    },\n-    youtube: {\n-      sdk: 'https://www.youtube.com/iframe_api',\n-      api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}',\n-    },\n-    googleIMA: {\n-      sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js',\n-    },\n-  },\n+  urls: null,\n \n   // Custom control listeners\n   listeners: {\n"
  },
  {
    "path": "scripts/patch-radix.js",
    "content": "const fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst root = path.resolve(__dirname, \"..\");\nconst targets = [\n  \"node_modules/@radix-ui/react-use-callback-ref/dist/index.js\",\n  \"node_modules/@radix-ui/react-use-callback-ref/dist/index.mjs\",\n];\n\nfor (const rel of targets) {\n  const p = path.join(root, rel);\n  try {\n    if (!fs.existsSync(p)) continue;\n    let src = fs.readFileSync(p, \"utf8\");\n\n    // Replace the concise optional-chaining one-liner with a guarded call\n    const replaced = src.replace(\n      /return\\s+React\\.useMemo\\([\\s\\S]*?callbackRef\\.current\\?\\.?\\(\\.\\.\\.args\\),\\s*\\[\\s*\\]\\s*\\);/m,\n      `return React.useMemo(() => (...args) => {\\n    var _callbackRef$current;\\n    if (typeof (_callbackRef$current = callbackRef.current) === 'function') {\\n      return _callbackRef$current.call(callbackRef, ...args);\\n    }\\n  }, []);`,\n    );\n\n    if (src !== replaced) {\n      fs.writeFileSync(p, replaced, \"utf8\");\n      console.log(\"Patched\", rel);\n    } else {\n      console.log(\"No patch needed for\", rel);\n    }\n  } catch (err) {\n    console.error(\"Failed to patch\", rel, err);\n  }\n}\n"
  },
  {
    "path": "src/_locales/ca/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Grava i Anota la Pantalla\",\n    \"description\": \"Nom de l'extensió\"\n  },\n  \"extDesc\": {\n    \"message\": \"La millor gravadora de pantalla gratuïta per a Chrome sense límits. Captura, dibuixa, amplia, desenfoca, edita vídeos i molt més, sense necessitat de registre i privada.\",\n    \"description\": \"Descripció de l'extensió\"\n  },\n  \"recordTab\": {\n    \"message\": \"Enregistra\",\n    \"description\": \"Etiqueta de la pestanya 'Enregistra' en el pop-up\"\n  },\n  \"videosTab\": {\n    \"message\": \"Els teus vídeos\",\n    \"description\": \"Etiqueta de la pestanya 'Els teus vídeos' en el pop-up\"\n  },\n  \"screenType\": {\n    \"message\": \"Pantalla\",\n    \"description\": \"Etiqueta del tipus de gravació 'Pantalla' en el pop-up\"\n  },\n  \"tabType\": {\n    \"message\": \"Àrea\",\n    \"description\": \"Etiqueta del tipus de gravació 'Àrea de la pestanya' en el pop-up\"\n  },\n  \"cameraType\": {\n    \"message\": \"Càmera\",\n    \"description\": \"Etiqueta del tipus de gravació 'Càmera' en el pop-up\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etiqueta del tipus de gravació 'Maqueta' en el pop-up\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Gravació d'àrea personalitzada desactivada\",\n    \"description\": \"Avís de desactivació de gravació d'àrea personalitzada en el pop-up per a versions de Chrome anteriors a 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Actualitza la versió de Chrome a 110+\",\n    \"description\": \"Descripció de l'avís de desactivació de gravació d'àrea personalitzada en el pop-up per a versions de Chrome anteriors a 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Actualitza\",\n    \"description\": \"Acció de l'avís de desactivació de gravació d'àrea personalitzada en el pop-up per a versions de Chrome anteriors a 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Comprova els teus permisos\",\n    \"description\": \"Títol del modal d'avís de permisos de càmera / micròfon\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Screenity no pot accedir a la teva càmera o micròfon. Permet l'accés fent clic a l'ícona de la càmera a la barra d'adreces.\",\n    \"description\": \"Descripció del modal d'avís de permisos de càmera / micròfon\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Descarta\",\n    \"description\": \"Botó de descarta del modal d'avís de permisos de càmera / micròfon\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Permet l'accés a la càmera\",\n    \"description\": \"Botó per permetre l'accés a la càmera\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Permet l'accés al micròfon\",\n    \"description\": \"Botó per permetre l'accés al micròfon\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Manté premuda una tecla per parlar\",\n    \"description\": \"Etiqueta de l'interruptor 'Pulsa per parlar' en el pop-up\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Defineix una àrea per gravar\",\n    \"description\": \"Etiqueta de l'interruptor 'Defineix una àrea per gravar' en el pop-up\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Inverteix la càmera\",\n    \"description\": \"Etiqueta de l'interruptor 'Inverteix la càmera' en el pop-up\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efectes de fons\",\n    \"description\": \"Etiqueta de l'interruptor 'Efectes de fons' en el pop-up\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Inicia la gravació\",\n    \"description\": \"Etiqueta del botó d'iniciar la gravació en el pop-up\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Selecciona una càmera per gravar\",\n    \"description\": \"Etiqueta del botó d'enregistrament quan està en mode càmera i cap càmera està seleccionada\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Iniciant la gravació...\",\n    \"description\": \"Etiqueta del botó d'iniciar la gravació en progrés en el pop-up\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Mostra més opcions\",\n    \"description\": \"Etiqueta de 'Mostra més opcions' en el pop-up\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Inclou l'àudio del sistema\",\n    \"description\": \"Etiqueta de l'àudio del sistema/pestanya\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Amaga la barra d'eines\",\n    \"description\": \"Etiqueta d'amagar la barra d'eines\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Compte enrere\",\n    \"description\": \"Etiqueta de 'Compte enrere'\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Estableix un límit de temps\",\n    \"description\": \"Etiqueta de 'Estableix un límit de temps'\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Blur\",\n    \"description\": \"Etiqueta de fons 'Difumina'\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Cap\",\n    \"description\": \"Cap dispositiu seleccionat\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Sense càmera\",\n    \"description\": \"Cap càmera seleccionada\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Sense micròfon\",\n    \"description\": \"Cap micròfon seleccionat\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Selecciona una font\",\n    \"description\": \"Marc de selecció de font\"\n  },\n  \"offLabel\": {\n    \"message\": \"Apagat\",\n    \"description\": \"Etiqueta 'Apagar' a la llista desplegable\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Amplada\",\n    \"description\": \"Etiqueta de l'amplada de la regió\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Alçada\",\n    \"description\": \"Etiqueta de l'alçada de la regió\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Fes clic per col·locar una imatge\",\n    \"description\": \"Missatge emergent que apareix en afegir una nova imatge\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Finalitza la gravació\",\n    \"description\": \"Missatge emergent 'Finalitza la gravació'\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Reinicia la gravació\",\n    \"description\": \"Missatge emergent 'Reinicia la gravació'\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Pausa la gravació\",\n    \"description\": \"Missatge emergent 'Pausa la gravació'\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Continua la gravació\",\n    \"description\": \"Missatge emergent 'Continua la gravació'\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Cancel·la la gravació\",\n    \"description\": \"Missatge emergent 'Cancel·la la gravació'\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Eines de dibuix\",\n    \"description\": \"Missatge emergent 'Activa/desactiva les eines de dibuix'\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Eines de difuminar\",\n    \"description\": \"Missatge emergent 'Activa/desactiva les eines de difuminat'\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Opcions del cursor\",\n    \"description\": \"Missatge emergent 'Activa/desactiva les opcions del cursor'\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Desactiva la càmera\",\n    \"description\": \"Missatge emergent 'Desactiva la càmera'\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Activa la càmera\",\n    \"description\": \"Missatge emergent 'Activa la càmera'\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Sense permisos de càmera\",\n    \"description\": \"Missatge emergent 'Sense permisos de càmera'\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Desactiva el micròfon\",\n    \"description\": \"Missatge emergent 'Desactiva el micròfon'\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Activa el micròfon\",\n    \"description\": \"Missatge emergent 'Activa el micròfon'\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Sense permisos de micròfon\",\n    \"description\": \"Missatge emergent 'Sense permisos de micròfon'\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Eina de selecció\",\n    \"description\": \"Missatge emergent 'Eina de selecció'\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Llapis\",\n    \"description\": \"Missatge emergent 'Eina de llapis'\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Subratllador\",\n    \"description\": \"Missatge emergent 'Eina de ressaltar'\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Goma\",\n    \"description\": \"Missatge emergent 'Eina d'esborrar'\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Text\",\n    \"description\": \"Missatge emergent 'Eina de text'\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Formes\",\n    \"description\": \"Missatge emergent 'Eina de formes'\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Fletxa\",\n    \"description\": \"Missatge emergent 'Eina de fletxa'\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Imatges\",\n    \"description\": \"Missatge emergent 'Eina d'imatge'\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Desfés\",\n    \"description\": \"Missatge emergent 'Desfés'\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Refés\",\n    \"description\": \"Missatge emergent 'Refés'\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Esborra-ho tot\",\n    \"description\": \"Missatge emergent 'Neteja el llenç'\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Més colors\",\n    \"description\": \"Missatge emergent 'Més colors'\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Traç gruixut\",\n    \"description\": \"Missatge emergent 'Traç gruixut'\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Traç mitjà\",\n    \"description\": \"Missatge emergent 'Traç mitjà'\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Traç prim\",\n    \"description\": \"Missatge emergent 'Traç prim'\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Activa/desactiva el mode Picture-in-Picture (PIP)\",\n    \"description\": \"Missatge emergent 'Activa/desactiva el mode imatge en imatge'\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Activa/desactiva els contorns\",\n    \"description\": \"Missatge emergent 'Activa/desactiva el farciment'\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Mode de dibuix\",\n    \"description\": \"Missatge emergent 'Mode de dibuix'\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Mode de difuminat\",\n    \"description\": \"Missatge emergent 'Mode de difuminat'\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Restableix els elements difuminats\",\n    \"description\": \"Missatge emergent 'Neteja tots els elements difuminats'\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Ressalta els clics\",\n    \"description\": \"Missatge emergent 'Ressalta les clicades'\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Ressalta el cursor\",\n    \"description\": \"Missatge emergent 'Ressalta el cursor'\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Enfoca el cursor\",\n    \"description\": \"Missatge emergent 'Projecció del cursor'\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"No tornis a mostrar-ho\",\n    \"description\": \"Botó de descarta del modal d'avís de permisos\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Segur que vols reiniciar la gravació?\",\n    \"description\": \"Títol del modal de reiniciar la gravació\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Perdràs tot el progrés del vídeo.\",\n    \"description\": \"Descripció del modal de reiniciar la gravació\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Reinicia la gravació\",\n    \"description\": \"Botó de reiniciar la gravació en el modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Continua la gravació\",\n    \"description\": \"Botó de continuar la gravació en el modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Segur que vols descartar la gravació?\",\n    \"description\": \"Títol del modal de descartar la gravació\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Perdràs tot el progrés del vídeo.\",\n    \"description\": \"Descripció del modal de descartar la gravació\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Descarta la gravació\",\n    \"description\": \"Botó de descartar la gravació en el modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Continua la gravació\",\n    \"description\": \"Botó de continuar la gravació en el modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Escull què vols enregistrar\",\n    \"description\": \"Títol a la pàgina d'enregistrament\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Enregistrant...\",\n    \"description\": \"Títol a la pàgina d'enregistrament mentre s'està gravant\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Deixa aquesta pestanya oberta. Es tancarà quan la gravació s'hagi guardat.\",\n    \"description\": \"Descripció a la pàgina d'enregistrament\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparant la gravació...\",\n    \"description\": \"Títol a la pàgina de l'àrea de proves mentre es grava/guarda\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Deixa aquesta pestanya oberta. S'actualitzarà amb la teva gravació quan estigui llesta.\",\n    \"description\": \"Descripció a la pàgina de l'àrea de proves mentre es grava/guarda\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Títol a la navegació de la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Cancel·la\",\n    \"description\": \"Botó de cancel·lació a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Torna a l'original\",\n    \"description\": \"Botó de revertir a l'original a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Restableix\",\n    \"description\": \"Botó de restabliment a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Desa els canvis\",\n    \"description\": \"Botó de desar a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Guardant...\",\n    \"description\": \"Botó de desar a la pàgina de l'editor de l'àrea de proves mentre es guarda\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Arrossega el fitxer d'àudio\",\n    \"description\": \"Arrossega i deixa fitxer d'àudio a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"O fes clic per buscar\",\n    \"description\": \"O fes clic per buscar fitxer d'àudio a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Configuració\",\n    \"description\": \"Títol de configuració d'àudio a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volum\",\n    \"description\": \"Etiqueta de volum d'àudio a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aplica\",\n    \"description\": \"Botó d'actualització d'àudio a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Retalla\",\n    \"description\": \"Títol de retall a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Amplada\",\n    \"description\": \"Etiqueta d'amplada per a les propietats\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Alçada\",\n    \"description\": \"Etiqueta d'alçada per a les propietats\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Esquerra\",\n    \"description\": \"Etiqueta d'esquerra per a les propietats\"\n  },\n  \"topLabel\": {\n    \"message\": \"Dalt\",\n    \"description\": \"Etiqueta de dalt per a les propietats\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Arrossega els controls i utilitza els botons a l'esquerra per editar la secció seleccionada.\",\n    \"description\": \"Informació sobre el retall a l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Retalla el vídeo\",\n    \"description\": \"Botó de retall a l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Retallant...\",\n    \"description\": \"Botó de retall a l'editor de l'àrea de proves mentre es retalla\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Elimina la secció\",\n    \"description\": \"Botó de tall a l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Eliminant...\",\n    \"description\": \"Botó de tall a l'editor de l'àrea de proves mentre es talla\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Silencia l'àudio\",\n    \"description\": \"Botó de silenciar a l'editor de l'àrea de proves\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Silenciant...\",\n    \"description\": \"Botó de silenciar a l'editor de l'àrea de proves mentre es silencia\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Aprèn més.\",\n    \"description\": \"Aprèn més amb un punt\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Desfés\",\n    \"description\": \"Etiqueta de desfés\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Refés\",\n    \"description\": \"Etiqueta de refés\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Deixa una ressenya\",\n    \"description\": \"Botó de deixar una ressenya\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Mantén-te informat\",\n    \"description\": \"Botó de seguir per romandre al corrent de les novetats\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"No tens connexió\",\n    \"description\": \"Etiqueta sense connexió\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Algunes característiques no estan disponibles\",\n    \"description\": \"Descripció de l'etiqueta sense connexió\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Torna-ho a provar\",\n    \"description\": \"Botó de tornar-ho a provar de l'etiqueta sense connexió\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Cal actualitzar Chrome\",\n    \"description\": \"Etiqueta d'actualització de Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Actualitza per accedir més funcionalitat\",\n    \"description\": \"Descripció de l'etiqueta d'actualització de Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Aprèn més\",\n    \"description\": \"Botó d'aprèn més\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Vídeo massa llarg per processar en el teu dispositiu\",\n    \"description\": \"Etiqueta de límit de més de 5 minuts a l'editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"L'edició i la descàrrega en format MP4 no estan disponibles\",\n    \"description\": \"Descripció de límit de més de 5 minuts a l'editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"El vídeo s'està processant...\",\n    \"description\": \"Etiqueta de processament de vídeo a l'editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Subscriu-te per editar vídeos més ràpid\",\n    \"description\": \"Descripció del processament de vídeo a l'editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Edita\",\n    \"description\": \"Títol d'edició a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Sense connexió\",\n    \"description\": \"Etiqueta sense connexió\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"No està disponible\",\n    \"description\": \"Etiqueta no disponible\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Edita el vídeo\",\n    \"description\": \"Etiqueta de botó de retall\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Talla o silencia el vídeo\",\n    \"description\": \"Etiqueta de retall\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Preparant...\",\n    \"description\": \"Etiqueta de preparació\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Retalla\",\n    \"description\": \"Etiqueta de botó de retall\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Retalla i redimensiona el vídeo\",\n    \"description\": \"Etiqueta de retall\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Afegeix àudio\",\n    \"description\": \"Etiqueta de botó d'afegir àudio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Puja el teu propi àudio per afegir al vídeo\",\n    \"description\": \"Etiqueta d'afegir àudio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Integracions\",\n    \"description\": \"Títol de desar a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Tanca la sessió de Google Drive\",\n    \"description\": \"Etiqueta de tancar la sessió a Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Guardant...\",\n    \"description\": \"Etiqueta de guardat a Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Guarda el vídeo al Google Drive\",\n    \"description\": \"Botó de desar a Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Desa el vídeo a Drive\",\n    \"description\": \"Etiqueta de desar a Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Inicia la sessió i guarda al Google Drive\",\n    \"description\": \"Etiqueta d'iniciar la sessió a Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exporta\",\n    \"description\": \"Títol d'exportació a la pàgina de l'editor de l'àrea de proves\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Descarregant...\",\n    \"description\": \"Etiqueta de descàrrega\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Descarrega com a .webm\",\n    \"description\": \"Botó de descàrrega de WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Exporta un vídeo .webm\",\n    \"description\": \"Etiqueta de descàrrega de WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Descarrega com a .mp4\",\n    \"description\": \"Botó de descàrrega de MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Exporta un vídeo .mp4 (recomanat)\",\n    \"description\": \"Etiqueta de descàrrega de MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Descarrega com a .gif\",\n    \"description\": \"Botó de descàrrega de GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Exporta un GIF (màxim 30 segons)\",\n    \"description\": \"Etiqueta de descàrrega de GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Preparat per millorar les teves gravacions?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Comparteix al núvol, fes zooms, usa plantilles... i dona suport a una desenvolupadora indie que respecta la teva privadesa ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Més info sobre Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Ja tens compte? Inicia sessió\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Comparteix\",\n    \"description\": \"Botó de compartir a la pàgina de l'editor de proves\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Substitueix l'àudio\",\n    \"description\": \"Botó de substitució d'àudio a l'editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Fes zoom al cursor\",\n    \"description\": \"Finestra emergent de zoom a un punt\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Mantén-te a la pàgina en gravar\",\n    \"description\": \"Finestra emergent de mantenir-se a la pàgina\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Mostra un avís si el micròfon està apagat\",\n    \"description\": \"Finestra emergent de recordança de micròfon\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Ajuda\",\n    \"description\": \"Botó de finestra emergent d'ajuda\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Gravació aturada. Prem el botó de reproducció per continuar.\",\n    \"description\": \"Títol del modal de gravació en pausa\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity no té permís per gravar la pantalla\",\n    \"description\": \"Títol del modal de permisos\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Per habilitar la gravació de pantalla, has de concedir permisos adicionals a Screenity.\",\n    \"description\": \"Descripció del modal de permisos\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Habilita la gravació de pantalla\",\n    \"description\": \"Acció del modal de permisos\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Cancel·la\",\n    \"description\": \"Cancel·la del modal de permisos\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"El teu micròfon està apagat\",\n    \"description\": \"Títol del modal de micròfon apagat\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Per incloure àudio a la teva gravació, hauràs d'activar el teu micròfon. Vols continuar sense àudio?\",\n    \"description\": \"Descripció del modal de micròfon apagat\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Sí, continua\",\n    \"description\": \"Continua del modal de micròfon apagat\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Cancel·la\",\n    \"description\": \"Cancel·la del modal de micròfon apagat\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Comença a utilitzar Screenity en tres passos senzills:\",\n    \"description\": \"Títol dels passos de configuració\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Fes clic a la icona de \",\n    \"description\": \"Pas 1 de configuració, abans de la icona. Necessita un espai al final.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" extensions\",\n    \"description\": \"Pas 1 de configuració, després de la icona. Necessita un espai al principi.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Prem la icona de \",\n    \"description\": \"Pas 2 de configuració, abans de la icona. Necessita un espai al final.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" destacar\",\n    \"description\": \"Pas 2 de configuració, després de la icona. Necessita un espai al principi.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Fes clic a la icona de \",\n    \"description\": \"Pas 3 de configuració, abans de la icona. Necessita un espai al final.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity per començar\",\n    \"description\": \"Pas 3 de configuració, després de la icona. Necessita un espai al principi.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Fantàstic! Ja estàs llest\",\n    \"description\": \"Títol de configuració completada\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Comença a fer una gravació aquí o en qualsevol altra pestanya.\",\n    \"description\": \"Descripció de configuració completada\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Fes clic aquí per dibuixar\",\n    \"description\": \"Onboarding per fer clic aquí\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Relaxa't. Fes clic en qualsevol lloc per aturar el compte enrere.\",\n    \"description\": \"Missatge de compte enrere\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"L'edició del vídeo pot ser imprecisa a causa dels intervals entre fotogrames en duracions curtes.\",\n    \"description\": \"Informació sobre l'editor que és massa petit\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"El vídeo s'està processant, la reproducció pot ser imprecisa o interrompuda.\",\n    \"description\": \"Bàner de processament a l'editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Retallar el vídeo pot trigar una estona\",\n    \"description\": \"Títol de la informació de retallat\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Subscriu-te per editar vídeos més ràpid\",\n    \"description\": \"Descripció de la informació de retallat\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microfon activat\",\n    \"description\": \"Títol del missatge emergent amb el microfon habilitat\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microfon desactivat\",\n    \"description\": \"Títol del missatge emergent amb el microfon deshabilitat\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Amaga les notificacions d'eines\",\n    \"description\": \"Botó per amagar les alertes de la interfície\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"No tornis a mostrar\",\n    \"description\": \"Botó de no tornis a mostrar\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Amaga els controls quan no els utilitzis\",\n    \"description\": \"Només mostra el botó de la barra d'eines en passar el ratolí\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Atura la gravació\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Benvingut al nou Screenity!\",\n    \"description\": \"Títol de l'anunci d'actualització per als usuaris existents\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Hem actualitzat el disseny i desenvolupat noves funcionalitats, però no et preocupis, segueix sent la mateixa extensió gratuïta, privada i de codi obert.\",\n    \"description\": \"Descripció de l'anunci d'actualització per als usuaris existents\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Obtén més informació sobre la actualització.\",\n    \"description\": \"Enllaç 'Obtenir més informació' de l'anunci d'actualització per als usuaris existents\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Començar\",\n    \"description\": \"Botó de l'anunci d'actualització per als usuaris existents\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Error en iniciar la gravació\",\n    \"description\": \"Títol del modal d'error de transmissió\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Hi ha hagut un error en iniciar la gravació. Reinicia el teu navegador i torna-ho a provar. Si el problema persisteix, si us plau, posa't en contacte amb nosaltres a support@screenity.io.\",\n    \"description\": \"Descripció del modal d'error de transmissió\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Qualitat més alta\",\n    \"description\": \"Etiqueta de commutació de la qualitat més alta al quadre emergent\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restaurar l'última gravació\",\n    \"description\": \"Botó per restaurar l'última gravació al quadre emergent\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Tens problemes?\",\n    \"description\": \"Botó de problemes a l'editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"No pots veure la teva gravació?\",\n    \"description\": \"Títol del modal de problemes a l'editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Si has completat la teva gravació i et trobes en aquesta pàgina sense veure el teu vídeo, pots intentar descarregar les dades de vídeo en brut si estan disponibles. També pots posar-te en contacte i enviar un informe d'error.\",\n    \"description\": \"Descripció del modal de problemes a l'editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Descarregar dades de vídeo en brut\",\n    \"description\": \"Botó del modal de problemes a l'editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Informar d'un error\",\n    \"description\": \"Segon botó del modal de problemes a l'editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"No s'ha trobat cap gravació, ho sento :(\",\n    \"description\": \"Alerta de no s'ha trobat cap gravació a l'editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"S'ha assolit el límit de memòria\",\n    \"description\": \"Títol de límit de memòria assolit\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"El teu dispositiu ha esgotat la memòria disponible per a la gravació, la qual cosa ha provocat una finalització automàtica de la gravació. Si desitges gravar durant més temps, considera la possibilitat de reduir la qualitat del vídeo o lliurar espai al teu dispositiu.\",\n    \"description\": \"Descripció de límit de memòria assolit\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Entès\",\n    \"description\": \"Botó d'entès\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Espai insuficient\",\n    \"description\": \"Títol d'espai insuficient\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity necessita almenys 1 GB d'espai lliure per a la gravació. Si us plau, allibera espai i torna-ho a intentar.\",\n    \"description\": \"Descripció d'espai insuficient\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Esborra gravacions anteriors\",\n    \"description\": \"Botó d'esborra gravacions anteriors\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avançat\",\n    \"description\": \"Secció avançada a la pàgina de l'editor en l'entorn segur\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Descarrega l'arxiu de vídeo en brut\",\n    \"description\": \"Botó de descàrrega de gravació en brut\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exporta les dades d'enregistrament originals\",\n    \"description\": \"Etiqueta de descàrrega de gravació en brut\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Obtén ajuda amb la teva gravació\",\n    \"description\": \"Botó de solució de problemes\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recupera el teu vídeo i resol altres problemes\",\n    \"description\": \"Etiqueta de solució de problemes\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Descarrega l'arxiu de vídeo en brut\",\n    \"description\": \"Títol del modal de gravació en brut\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"L'arxiu de vídeo en brut és l'arxiu original enregistrat per Screenity. Aquest vídeo no ha estat processat ni editat, per la qual cosa pot ser molt gran i potser no es pot reproduir en tots els dispositius. Si tens problemes amb el vídeo processat, pots fer servir aquest arxiu per recuperar el teu vídeo.\",\n    \"description\": \"Descripció del modal de gravació en brut\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Descarrega\",\n    \"description\": \"Botó del modal de gravació en brut\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Descarrega la informació del sistema i les dades de vídeo per a l'enviament i la solució de problemes\",\n    \"description\": \"Títol del modal de solució de problemes\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Després de la descàrrega, envia l'arxiu a support@screenity.io amb qualsevol informació rellevant. Farem servir aquestes dades per ajudar-te a solucionar qualsevol problema que tinguis amb l'extensió i, si és possible, recuperar el teu vídeo.\",\n    \"description\": \"Descripció del modal de solució de problemes\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Descarrega\",\n    \"description\": \"Botó del modal de solució de problemes\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"El vídeo és massa llarg per processar-se al teu dispositiu\",\n    \"description\": \"Títol del modal de límit superior a 5 minuts\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Com que Screenity és completament privat i s'executa localment, està limitat pel maquinari del teu dispositiu. Processar un vídeo d'aquesta longitud trigarà massa temps i serà molt intensiu en recursos. No et preocupis, encara pots descarregar l'arxiu WEBM o la gravació en brut, però l'edició i l'exportació a MP4 no estan disponibles. Dicho això, encara pots intentar processar el vídeo de tota manera si entens els riscos.\",\n    \"description\": \"Descripció del modal de límit superior a 5 minuts\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Entenc el risc, intenta-ho igualment\",\n    \"description\": \"Botó del modal de límit superior a 5 minuts\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Obtén més informació sobre el processament de vídeos llargs.\",\n    \"description\": \"Enllaç 'Obtén més informació' del modal de límit superior a 5 minuts amb un punt\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Mode de recuperació\",\n    \"description\": \"Títol del mode de recuperació\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Descarregar dades per solucionar problemes\",\n    \"description\": \"Opció de descàrrega per solucionar problemes\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centre d'ajuda\",\n    \"description\": \"Element de navegació d'ajuda\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Enregistra l'àudio de la pàgina\",\n    \"description\": \"Títol d'avís d'àudio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Per enregistrar l'àudio d'aquesta pàgina, has de canviar a '$tab$'.\",\n    \"description\": \"Descripció d'avís d'àudio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Àrea de la pestanya\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extensió no compatible a la pestanya\",\n    \"description\": \"Títol d'extensió no compatible a la pestanya\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity no era compatible a la teva pestanya anterior. No obstant això, pots començar la gravació aquí i després canviar a la pestanya, però la teva càmera i la barra de ferramentes no seran visibles.\",\n    \"description\": \"Descripció d'extensió no compatible a la pestanya\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configura una còpia de seguretat local per a les teves gravacions de Screenity\",\n    \"description\": \"Títol de còpies de seguretat\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Mantingues una còpia de totes les gravacions que facis. Si no configures còpies de seguretat, les teves gravacions només es guardaran quan decideixis descarregar-les.\",\n    \"description\": \"Descripció de còpies de seguretat\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Potser necessitaràs crear una nova carpeta primer si intents guardar-ho a Descàrregues o altres carpetes del sistema.\",\n    \"description\": \"Descripció de còpies de seguretat\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Selecciona una carpeta\",\n    \"description\": \"Botó Selecciona carpeta\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Ara no\",\n    \"description\": \"Botó Ara no\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Les teves gravacions s'estan sincronitzant localment\",\n    \"description\": \"Títol de còpies de seguretat actives\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Per evitar sol·licitar permisos cada vegada que gravis, et recomanem deixar aquesta pestanya oberta.\",\n    \"description\": \"Descripció de còpies de seguretat actives\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Tanca la pestanya de tota manera\",\n    \"description\": \"Botó Tanca la pestanya de tota manera\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Atura les còpies de seguretat de totes les gravacions\",\n    \"description\": \"Botó Atura còpies de seguretat\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Per favor, torna a confirmar l'accés a la teva carpeta de còpies de seguretat local de Screenity\",\n    \"description\": \"Títol de confirmació de còpies de seguretat\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Ho sentim, però necessitem el teu permís una vegada més per continuar fent còpies locals de les teves gravacions. Desafortunadament, haurem de preguntar cada vegada que tanquis aquesta pestanya, el que pot passar si tanques el navegador o reinicies el teu ordinador.\",\n    \"description\": \"Descripció de confirmació de còpies de seguretat\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Permet a Screenity continuar fent còpies de seguretat de les gravacions\",\n    \"description\": \"Botó Permet en la confirmació de còpies de seguretat\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Fes còpies de seguretat de les gravacions\",\n    \"description\": \"Etiqueta d'alternar còpies de seguretat\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permís no atorgat per fer una còpia de seguretat de la gravació\",\n    \"description\": \"Títol de fallada de permís de còpia de seguretat\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"No has seleccionat cap carpeta per fer una còpia local de les gravacions. Depenent de la versió del teu navegador o sistema operatiu, potser se't demanarà seleccionar la carpeta cada vegada que comencis a gravar. Pots intentar-ho de nou o desactivar les còpies de seguretat per complet si prefereixes no atorgar permisos repetidament.\",\n    \"description\": \"Descripció de fallada de permís de còpia de seguretat\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Enregistra l'audio de l'ordinador\",\n    \"description\": \"Títol d'avís d'àudio per a macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"A macOS, només pots enregistrar l'àudio de les pestanyes. Seleccioneu l'opció 'Pestanya de Chrome' i assegureu-vos que 'Comparteix també l'audio de la pestanya' estigui habilitat.\",\n    \"description\": \"Descripció d'avís d'àudio per a macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Enregistra l'audio de l'ordinador\",\n    \"description\": \"Títol d'avís d'àudio per a altres sistemes\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Assegureu-vos d'activar l'opció 'Comparteix l'audio del sistema' si voleu enregistrar l'audio de l'ordinador. Si no veieu aquesta opció, proveu d'actualitzar Chrome o canviar a l'opció d'enregistrament de pestanyes.\",\n    \"description\": \"Descripció d'avís d'àudio per a altres sistemes\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Resolució màxima\",\n    \"description\": \"Etiqueta de resolució màxima al menú de configuració\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"FPS màxims\",\n    \"description\": \"Etiqueta de FPS màxims al menú de configuració\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"No hi ha prou RAM\",\n    \"description\": \"Etiqueta 'No hi ha prou RAM' al menú de configuració\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Redimensionar finestra\",\n    \"description\": \"Etiqueta de redimensionar finestra al menú de configuració\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"La pantalla és massa petita per a aquesta resolució\",\n    \"description\": \"Consell per a pantalles massa petites al menú de configuració\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"No es pot enregistrar en aquesta resolució al seu dispositiu\",\n    \"description\": \"Consell per a la resolució màxima al menú de configuració\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Revisar permisos\",\n    \"description\": \"Botó de revisar permisos\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Afegeix una altra gravació\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Preparant-ho tot...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Espera un moment, estem preparant la teva gravació.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barra d'eines amagada. Torna a activar-la a les opcions del.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Inicia sessió o registra't\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Tanca la sessió\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Has tancat la sessió\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Has arribat al límit d'emmagatzematge\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Has utilitzat els 100 GB d'emmagatzematge. Elimina projectes o escenes antics per continuar gravant.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gestiona l'emmagatzematge\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Tanca\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"No s'ha pogut comprovar l'emmagatzematge\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Hi ha hagut un problema en comprovar l'emmagatzematge. Torna a iniciar sessió i intenta gravar de nou.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"No hem pogut verificar la teva quota d'emmagatzematge. Torna-ho a provar d'aquí uns segons o actualitza la pàgina.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Torna-ho a provar\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Escena afegida a la gravació Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Preparat per gravar una nova escena al teu projecte\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"La gravació s'apropa al límit de 90 minuts, s'aturarà aviat\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Gravació aturada pel límit de 90 minuts\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"La gravació de pestanyes està desactivada en aquesta pàgina. S'ha activat la gravació de pantalla per defecte.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Gravació del projecte cancel·lada\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Enllaç copiat al porta-retalls\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"No s'ha pogut copiar l'enllaç\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Més recents\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Més antics\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Tots els vídeos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"No s'han trobat vídeos\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Carregant vídeos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Ves al panell de control\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Benvingut a Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Sense anuncis. Sense seguiment. Sense límits.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Grava, simplement.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Fes-ho servir gratis — no cal registrar-se!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Vols fer més amb els teus vídeos?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Emmagatzematge al núvol, comparteix amb un enllaç\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combina diversos clips com capítols en una història\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zooms intel·ligents amb clic (o afegeix els teus!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Afegeix plantilles, subtítols, mockups...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Fons i distribució de càmera personalitzats\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Desbloqueja funcions extres\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Dona suport al desenvolupament independent!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Configuració del compte\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Suport\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Subscripció Pro inactiva\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"La teva subscripció a Screenity Pro està inactiva.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Reactiva-la per tornar-hi a accedir.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Els teus vídeos i dades s'eliminaran definitivament el \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gestiona la teva subscripció\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Vols continuar utilitzant la versió gratuïta?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Tanca la sessió i baixa de pla\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Has tancat la sessió\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Per mantenir les teves gravacions sincronitzades i accedir a funcions Pro, cal que iniciïs sessió de nou.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Torna a iniciar sessió\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Pots continuar utilitzant l'extensió sense compte, però les gravacions no es guardaran ni es podran recuperar més tard.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continua sense iniciar sessió\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Mode de gravació instantani\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Descarrega immediata, però la distribució de la càmera no es podrà editar un cop la gravació hagi començat.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Mode de gravació instantani\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Aquest mode grava tot en un sol vídeo per descarregar i compartir a l'instant. No podràs modificar la distribució de la càmera, però podràs fer altres tipus d'edicions.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Entès\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"La gravació de pestanyes està desactivada en aquesta pàgina\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Afegint a: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Finalitza\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Vols fer més amb les teves gravacions?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Inicia sessió per guardar els vídeos al núvol, compartir amb enllaç i accedir a funcions avançades d'edició.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Inicia sessió per desbloquejar funcions Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Dona suport a la desenvolupadora independent!\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Desbloqueja més funcions\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Inicia sessió per compartir (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Desbloqueja emmagatzematge al núvol, edició multi-escena, zooms automàtics, subtítols i més\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity sempre serà gratuït, de codi obert i sense anuncis. Pro ajuda a cobrir costos de núvol i infraestructura, i dona suport a una desenvolupadora independent! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"No ho mostris més\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Prova-ho\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Mode Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Grava múltiples escenes, com la pantalla, la càmera o totes dues, una rere l'altra. És ideal per fer diverses preses, canviar de vista o dividir la gravació en parts. Quan acabis, fes clic a Finalitza per obrir l'editor amb totes les escenes combinades en un sol vídeo.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Activa Pro per començar\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Subscriu-te per accedir a les funcions Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Subscriu-te a Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Gaudeixes de Screenity localment? Dona suport al seu futur!\",\n    \"description\": \"Títol del bàner d'autoallotjament a l'editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity està creada per una desenvolupadora indie. Autoallotjar és gratuït, però si et resulta útil, considera donar-hi suport amb Pro ❤️\",\n    \"description\": \"Descripció del bàner d'autoallotjament a l'editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Benvingut de nou a Screenity\",\n    \"description\": \"Títol del popup de benvinguda mostrat als usuaris que tornen\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Novetat: Porta les teves gravacions al següent nivell\",\n    \"description\": \"Títol per a la funció d'emmagatzematge al núvol en el popup de benvinguda\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro és una nova plataforma opcional per desar vídeos al núvol, compartir-los amb un enllaç i desbloquejar eines avançades d’edició, si i quan les necessitis.\",\n    \"description\": \"Descripció de la funció d'emmagatzematge al núvol en el popup de benvinguda\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Descobreix Screenity Pro\",\n    \"description\": \"Botó de crida a l'acció per a la funció d'emmagatzematge al núvol en el popup de benvinguda\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Benvingut a Screenity Pro\",\n    \"description\": \"Títol del modal de benvinguda a Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Ja ho tens tot a punt per començar a gravar! Tingues en compte aquestes coses:\\n\\n- La bombolla de la càmera pot desaparèixer durant la gravació; és normal. Es grava per separat en segon pla perquè la puguis col·locar on vulguis més endavant.\\n- Els zooms només es creen automàticament quan es fa clic dins de pestanyes de Chrome, per una limitació del navegador. Sempre pots afegir-ne més manualment després de gravar.\\n- Per a gravacions ràpides amb descàrrega instantània, prova el Mode Instantani. Només tingues en compte que les opcions d’edició són limitades: sense fons, disposicions ni ajustaments avançats.\",\n    \"description\": \"Descripció del modal de benvinguda a Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Entès\",\n    \"description\": \"Botó d’acció del modal de benvinguda a Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 fallat — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Benvingut/da a l'extensió Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Uns quants consells ràpids abans de gravar.<br/>Els zooms automàtics només funcionen amb clics dins de <strong>pestanyes de Chrome</strong>. Sempre en pots afegir manualment després.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barra d'eines i efectes de gravació\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Fes servir aquesta barra per dibuixar, aplicar efectes del cursor, desenfocar i controlar la gravació.<br/><br/><strong>Aquesta barra surt al vídeo</strong> si no l'amagues.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"La càmera es captura per separat\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"La càmera pot <strong>amagar-se o passar a PiP</strong> durant la gravació, és normal.<br/><br/>Es captura per separat perquè la puguis col·locar després.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Mode instantani\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Ideal per compartir ràpid amb <strong>descàrregues il·limitades</strong>.<br/><br/>En aquest mode no hi ha opcions avançades de disseny ni d'edició.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Entesos\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Següent\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Enrere\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Més informació\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Reinicia l'onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Poc espai al disc. S'està desant l'enregistrament. Tot el que s'ha capturat és segur.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Enregistrament llarg\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"El teu enregistrament és complet i segur. Descarrega'l com a WebM a continuació.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"No disponible per a enregistraments llargs\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"No disponible en mode de recuperació\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Edició d'àudio massa llarga\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Aquesta edició d'àudio no és compatible amb clips de més de 15 minuts. L'original no s'ha modificat. Prova a retallar el clip primer.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"L'edició ha superat el temps d'espera\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"L'edició no s'ha completat a temps. L'enregistrament original no s'ha modificat. Torna a intentar-ho o retalla el clip primer.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"La compartició de pantalla s'ha aturat. L'enregistrament s'ha desat. Atura quan vulguis.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Hi ha hagut un problema de processament, però l'enregistrament és segur. Pots descarregar-lo a continuació.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"S'està processant l'edició. L'enregistrament original no s'ha modificat.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"No s'ha pogut aplicar l'edició\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Alguna cosa ha fallat durant el processament. L'enregistrament original és segur. Pots tornar-ho a provar o descarregar-lo tal com és.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"La compartició de pantalla s'ha aturat. S'està desant l'enregistrament. Tot el que s'ha capturat és segur.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"L'àudio s'ha desconnectat. S'està desant l'enregistrament. El vídeo capturat és segur.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"L'enregistrament s'ha desat. L'editor no s'ha obert. Fes clic a la icona de Screenity per tornar-ho a provar.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"El desament està trigant més del previst. Les dades capturades són segures. L'editor s'obrirà en breu.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copiar info de depuració\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info de depuració copiada\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Obtenir ajuda\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"L'enregistrament era massa curt per desar-lo\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"L'enregistrador ràpid ha fallat\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"La sortida de l'enregistrador ràpid no s'ha pogut validar en aquest dispositiu.\\nPots descarregar el fitxer igualment.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Descarrega igualment\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Cancel\\u00b7la\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/de/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Bildschirmaufnahme & Beschriftung\",\n    \"description\": \"Erweiterungsname\"\n  },\n  \"extDesc\": {\n    \"message\": \"Der beste kostenlose Bildschirmrekorder ohne Einschränkungen. Erfassen, kommentieren, zoomen, verwischen, Videos bearbeiten und mehr - ohne Anmeldung erforderlich und datenschutzfreundlich.\",\n    \"description\": \"Beschreibung der Erweiterung\"\n  },\n  \"recordTab\": {\n    \"message\": \"Aufnehmen\",\n    \"description\": \"Registerkarte 'Aufnehmen' in der Popup-Ansicht\"\n  },\n  \"videosTab\": {\n    \"message\": \"Ihre Videos\",\n    \"description\": \"Registerkarte 'Ihre Videos' in der Popup-Ansicht\"\n  },\n  \"screenType\": {\n    \"message\": \"Bildschirm\",\n    \"description\": \"Label für die Aufnahmeart 'Bildschirm' in der Popup-Ansicht\"\n  },\n  \"tabType\": {\n    \"message\": \"Tab-Bereich\",\n    \"description\": \"Label für die Aufnahmeart 'Tab-Bereich' in der Popup-Ansicht\"\n  },\n  \"cameraType\": {\n    \"message\": \"Kamera\",\n    \"description\": \"Label für die Aufnahmeart 'Kamera' in der Popup-Ansicht\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Label für die Aufnahmeart 'Mockup' in der Popup-Ansicht\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Benutzerdefinierte Bereichsaufnahme deaktiviert\",\n    \"description\": \"Warnung 'Benutzerdefinierte Bereichsaufnahme deaktiviert' in der Popup-Ansicht für Chrome-Versionen älter als 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Aktualisieren Sie die Chrome-Version auf 110+\",\n    \"description\": \"Beschreibung der Warnung 'Benutzerdefinierte Bereichsaufnahme deaktiviert' in der Popup-Ansicht für Chrome-Versionen älter als 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Aktualisieren\",\n    \"description\": \"Aktion der Warnung 'Benutzerdefinierte Bereichsaufnahme deaktiviert' in der Popup-Ansicht für Chrome-Versionen älter als 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Überprüfen Sie Ihre Berechtigungen\",\n    \"description\": \"Titel des Modals für Kamera- / Mikrofonberechtigungen\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Es scheint, dass Screenity keinen Zugriff auf Ihre Kamera oder Ihr Mikrofon hat. Stellen Sie sicher, dass Sie den Zugriff zulassen, indem Sie auf das Kamera-Symbol in der Adressleiste klicken.\",\n    \"description\": \"Beschreibung des Modals für Kamera- / Mikrofonberechtigungen\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Verwerfen\",\n    \"description\": \"Schaltfläche 'Verwerfen' im Modal für Kamera- / Mikrofonberechtigungen\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Kamerazugriff zulassen\",\n    \"description\": \"Schaltfläche 'Kamerazugriff zulassen'\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Mikrofonzugriff zulassen\",\n    \"description\": \"Schaltfläche 'Mikrofonzugriff zulassen'\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Push-to-Talk\",\n    \"description\": \"Label für den Schalter 'Push-to-Talk'\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Bereich zum Aufnehmen festlegen\",\n    \"description\": \"Label für den Schalter 'Benutzerdefinierte Bereichsaufnahme'\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Kamera umdrehen\",\n    \"description\": \"Label für den Schalter 'Kamera umdrehen'\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Hintergrundeffekte\",\n    \"description\": \"Label für den Schalter 'Hintergrundeffekte'\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Aufnahme starten\",\n    \"description\": \"Label für die Schaltfläche 'Aufnahme starten'\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Starte Aufnahme...\",\n    \"description\": \"Label für die Schaltfläche 'Aufnahme starten' im Fortschritt\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Kamera auswählen, um aufzunehmen\",\n    \"description\": \"Label für die Schaltfläche 'Aufnahme starten' im Kameramodus, wenn keine Kamera ausgewählt ist\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Weitere Optionen anzeigen\",\n    \"description\": \"Label für die Schaltfläche 'Weitere Optionen anzeigen'\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Systemaudio einschließen\",\n    \"description\": \"Tab/Systemaudio-Beschriftung\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Symbolleiste ausblenden\",\n    \"description\": \"Beschreibung für das Ausblenden der Symbolleiste\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Countdown\",\n    \"description\": \"Beschreibung für das Countdown-Label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Zeitlimit setzen\",\n    \"description\": \"Beschreibung für das Alarm-Label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Unschärfe\",\n    \"description\": \"Beschreibung für das Unschärfe-Hintergrund-Label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Kein\",\n    \"description\": \"Beschreibung für die Auswahl 'Kein Gerät ausgewählt'\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Keine Kamera\",\n    \"description\": \"Beschreibung für die Auswahl 'Keine Kamera ausgewählt'\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Kein Mikrofon\",\n    \"description\": \"Beschreibung für die Auswahl 'Kein Mikrofon ausgewählt'\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Quelle auswählen\",\n    \"description\": \"Platzhalter für die Auswahl der Quelle\"\n  },\n  \"offLabel\": {\n    \"message\": \"Aus\",\n    \"description\": \"Beschreibung für 'Aus' in der Dropdown-Liste\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Breite\",\n    \"description\": \"Beschreibung für das Breiten-Label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Höhe\",\n    \"description\": \"Beschreibung für das Höhen-Label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Klicken Sie, um ein Bild einzufügen\",\n    \"description\": \"Toast-Nachricht, die angezeigt wird, wenn ein neues Bild hinzugefügt wird\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Aufnahme beenden\",\n    \"description\": \"Tooltip für das Beenden der Aufnahme\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Aufnahme neu starten\",\n    \"description\": \"Tooltip für das Neustarten der Aufnahme\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Aufnahme pausieren\",\n    \"description\": \"Tooltip für das Pausieren der Aufnahme\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Aufnahme fortsetzen\",\n    \"description\": \"Tooltip für das Fortsetzen der Aufnahme\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Aufnahme abbrechen\",\n    \"description\": \"Tooltip für das Abbrechen der Aufnahme\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Zeichenwerkzeuge ein-/ausschalten\",\n    \"description\": \"Tooltip für das Ein- und Ausschalten der Zeichenwerkzeuge\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Unschärfe-Werkzeug ein-/ausschalten\",\n    \"description\": \"Tooltip für das Ein- und Ausschalten des Unschärfe-Werkzeugs\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Cursor-Optionen ein-/ausschalten\",\n    \"description\": \"Tooltip für das Ein- und Ausschalten der Cursor-Optionen\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Kamera deaktivieren\",\n    \"description\": \"Tooltip für das Deaktivieren der Kamera\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Kamera aktivieren\",\n    \"description\": \"Tooltip für das Aktivieren der Kamera\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Keine Kameraberechtigungen\",\n    \"description\": \"Tooltip für fehlende Kameraberechtigungen\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Mikrofon deaktivieren\",\n    \"description\": \"Tooltip für das Deaktivieren des Mikrofons\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Mikrofon aktivieren\",\n    \"description\": \"Tooltip für das Aktivieren des Mikrofons\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Keine Mikrofonberechtigungen\",\n    \"description\": \"Tooltip für fehlende Mikrofonberechtigungen\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Werkzeug auswählen\",\n    \"description\": \"Tooltip für das Auswählen des Werkzeugs\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Stift-Werkzeug\",\n    \"description\": \"Tooltip für das Stift-Werkzeug\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Textmarker-Werkzeug\",\n    \"description\": \"Tooltip für das Textmarker-Werkzeug\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Radiergummi-Werkzeug\",\n    \"description\": \"Tooltip für das Radiergummi-Werkzeug\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Text-Werkzeug\",\n    \"description\": \"Tooltip für das Text-Werkzeug\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Form-Werkzeug\",\n    \"description\": \"Tooltip für das Form-Werkzeug\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Pfeil-Werkzeug\",\n    \"description\": \"Tooltip für das Pfeil-Werkzeug\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Bild-Werkzeug\",\n    \"description\": \"Tooltip für das Bild-Werkzeug\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Rückgängig machen\",\n    \"description\": \"Tooltip für das Rückgängig machen\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Wiederherstellen\",\n    \"description\": \"Tooltip für das Wiederherstellen\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Leinwand löschen\",\n    \"description\": \"Tooltip für das Löschen der Leinwand\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Weitere Farben\",\n    \"description\": \"Tooltip für mehr Farben\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Dicke Linie\",\n    \"description\": \"Tooltip für dicke Linien\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Mittlere Linie\",\n    \"description\": \"Tooltip für mittlere Linien\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Dünne Linie\",\n    \"description\": \"Tooltip für dünne Linien\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Bild-in-Bild-Modus umschalten\",\n    \"description\": \"Tooltip für das Umschalten des Bild-in-Bild-Modus\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Füllung umschalten\",\n    \"description\": \"Tooltip für das Umschalten der Füllung\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Zeichenmodus\",\n    \"description\": \"Toast für Zeichenmodus\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Unschärfe-Modus\",\n    \"description\": \"Toast für Unschärfe-Modus\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Alle unscharfen Elemente löschen\",\n    \"description\": \"Tooltip zum Löschen aller unscharfen Elemente\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Klicks hervorheben\",\n    \"description\": \"Tooltip zum Hervorheben von Klicks\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Cursor hervorheben\",\n    \"description\": \"Tooltip zum Hervorheben des Cursors\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Cursor im Spotlight\",\n    \"description\": \"Tooltip für den Cursor im Spotlight\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Nicht mehr anzeigen\",\n    \"description\": \"Schaltfläche zum Ausblenden des Berechtigungsmodals\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Möchten Sie die Aufnahme wirklich neu starten?\",\n    \"description\": \"Titel des Modals zum Neustart der Aufnahme\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Ihr aktueller Fortschritt im Video geht verloren.\",\n    \"description\": \"Beschreibung des Modals zum Neustart der Aufnahme\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Aufnahme neu starten\",\n    \"description\": \"Schaltfläche zum Neustart der Aufnahme im Modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Aufnahme fortsetzen\",\n    \"description\": \"Schaltfläche zum Fortsetzen der Aufnahme im Modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Möchten Sie die Aufnahme wirklich verwerfen?\",\n    \"description\": \"Titel des Modals zum Verwerfen der Aufnahme\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Ihr aktueller Fortschritt im Video geht verloren.\",\n    \"description\": \"Beschreibung des Modals zum Verwerfen der Aufnahme\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Aufnahme verwerfen\",\n    \"description\": \"Schaltfläche zum Verwerfen der Aufnahme im Modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Aufnahme fortsetzen\",\n    \"description\": \"Schaltfläche zum Fortsetzen der Aufnahme im Modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Wählen Sie, was Sie aufnehmen möchten\",\n    \"description\": \"Titel auf der Aufnahmeseite\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Aufnahme...\",\n    \"description\": \"Titel auf der Aufnahmeseite während der Aufnahme\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Halten Sie diesen Tab geöffnet. Er wird sich schließen, sobald Ihre Aufnahme gespeichert wurde.\",\n    \"description\": \"Beschreibung auf der Aufnahmeseite\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Vorbereitung der Aufnahme...\",\n    \"description\": \"Titel auf der Sandbox-Seite während der Aufnahme / Speicherung\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Halten Sie diesen Tab geöffnet. Er wird sich aktualisieren, wenn Ihre Aufnahme fertig ist.\",\n    \"description\": \"Beschreibung auf der Sandbox-Seite während der Aufnahme / Speicherung\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Titel in der Navigation der Sandbox-Editor-Seite\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Abbrechen\",\n    \"description\": \"Abbrechen-Schaltfläche auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Auf Original zurücksetzen\",\n    \"description\": \"Schaltfläche zum Zurücksetzen auf das Original auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Zurücksetzen\",\n    \"description\": \"Schaltfläche zum Zurücksetzen auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Änderungen speichern\",\n    \"description\": \"Schaltfläche zum Speichern auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Speichern...\",\n    \"description\": \"Schaltfläche zum Speichern auf der Sandbox-Editor-Seite während des Speicherns\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Ziehen Sie Ihre Audio-Datei hierhin\",\n    \"description\": \"Audio-Datei per Drag & Drop auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Oder klicken Sie hier, um zu durchsuchen\",\n    \"description\": \"Oder klicken Sie hier, um die Audio-Datei auf der Sandbox-Editor-Seite zu durchsuchen\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Einstellungen\",\n    \"description\": \"Titel der Audio-Einstellungen auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Lautstärke\",\n    \"description\": \"Beschriftung für die Lautstärke-Einstellungen auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aktualisieren\",\n    \"description\": \"Schaltfläche zum Aktualisieren der Audio-Einstellungen auf der Sandbox-Editor-Seite\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Zuschneiden\",\n    \"description\": \"Titel auf der Sandbox-Editor-Seite zum Zuschneiden\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Breite\",\n    \"description\": \"Beschriftung für die Breiten-Einstellung\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Höhe\",\n    \"description\": \"Beschriftung für die Höhen-Einstellung\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Links\",\n    \"description\": \"Beschriftung für die Links-Einstellung\"\n  },\n  \"topLabel\": {\n    \"message\": \"Oben\",\n    \"description\": \"Beschriftung für die Oben-Einstellung\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Ziehen Sie die Griffe und verwenden Sie die Schaltflächen auf der linken Seite, um den ausgewählten Abschnitt zu bearbeiten.\",\n    \"description\": \"Informationen zum Zuschneiden im Sandbox-Editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Video zuschneiden\",\n    \"description\": \"Zuschneide-Schaltfläche im Sandbox-Editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Zuschneiden...\",\n    \"description\": \"Zuschneide-Schaltfläche im Sandbox-Editor während des Zuschneidens\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Abschnitt schneiden\",\n    \"description\": \"Schneide-Schaltfläche im Sandbox-Editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Schneiden...\",\n    \"description\": \"Schneide-Schaltfläche im Sandbox-Editor während des Schneidens\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Audio stummschalten\",\n    \"description\": \"Stummschaltungs-Schaltfläche im Sandbox-Editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Stummschalten...\",\n    \"description\": \"Stummschaltungs-Schaltfläche im Sandbox-Editor während des Stummschaltens\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Erfahren Sie mehr.\",\n    \"description\": \"Weitere Informationen mit einem Punkt\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Rückgängig\",\n    \"description\": \"Rückgängig-Schriftzug\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Wiederholen\",\n    \"description\": \"Wiederholen-Schriftzug\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Bewertung hinterlassen\",\n    \"description\": \"Schaltfläche 'Bewertung abgeben'\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Folgen Sie für Aktualisierungen\",\n    \"description\": \"Schaltfläche 'Folgen für Aktualisierungen'\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Sie sind derzeit offline\",\n    \"description\": \"Offline-Schriftzug\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Einige Funktionen sind nicht verfügbar, bis eine Verbindung wiederhergestellt ist\",\n    \"description\": \"Beschreibung des Offline-Schriftzugs\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Erneut versuchen\",\n    \"description\": \"Schaltfläche 'Erneut versuchen' des Offline-Schriftzugs\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome muss aktualisiert werden\",\n    \"description\": \"Aktualisieren Sie den Chrome-Schriftzug\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Aktualisieren Sie, um auf mehr Funktionen zugreifen zu können\",\n    \"description\": \"Beschreibung des Aktualisieren Sie den Chrome-Schriftzugs\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Erfahren Sie mehr\",\n    \"description\": \"Schaltfläche 'Erfahren Sie mehr'\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video zu lang für lokale Verarbeitung\",\n    \"description\": \"Kennzeichnung für über 5 Minuten in der Bearbeitung\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Bearbeitung und MP4-Export nicht verfügbar\",\n    \"description\": \"Beschreibung für über 5 Minuten in der Bearbeitung\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Das Video wird verarbeitet...\",\n    \"description\": \"Schriftzug 'Videoverarbeitung' im Editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Aktualisieren Sie für schnellere Bearbeitung\",\n    \"description\": \"Beschreibung der Videoverarbeitung im Editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Bearbeiten\",\n    \"description\": \"Bearbeitungstitel auf der Sandbox-Editor-Seite\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Keine Verbindung\",\n    \"description\": \"Schriftzug 'Keine Verbindung'\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Nicht verfügbar\",\n    \"description\": \"Schriftzug 'Nicht verfügbar'\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Video bearbeiten\",\n    \"description\": \"Schriftzug 'Zuschneiden'-Schaltfläche\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Video zuschneiden, schneiden oder stummschalten\",\n    \"description\": \"Beschriftung 'Zuschneiden'\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Vorbereitung...\",\n    \"description\": \"Beschriftung 'Vorbereitung'\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Zuschneiden\",\n    \"description\": \"Beschriftung 'Zuschneiden'-Schaltfläche\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Video zuschneiden und neu dimensionieren\",\n    \"description\": \"Beschriftung 'Zuschneiden'\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Audio hinzufügen\",\n    \"description\": \"Beschriftung 'Audio hinzufügen'-Schaltfläche\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Laden Sie Ihr eigenes Audio hoch, um es zum Video hinzuzufügen\",\n    \"description\": \"Beschriftung 'Audio hinzufügen'\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Speichern\",\n    \"description\": \"Titel 'Speichern' auf der Sandbox-Editor-Seite\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Aus Drive abmelden\",\n    \"description\": \"Beschriftung 'Aus Google Drive abmelden'\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Wird gespeichert...\",\n    \"description\": \"Beschriftung 'Speichern in Google Drive'\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"In Drive speichern\",\n    \"description\": \"Beschriftung 'In Google Drive speichern'-Schaltfläche\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Speichern Sie die aktuelle Version in Google Drive\",\n    \"description\": \"Beschriftung 'In Google Drive speichern'\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Anmelden und in Drive speichern\",\n    \"description\": \"Beschriftung 'In Google Drive anmelden'\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exportieren\",\n    \"description\": \"Titel 'Exportieren' auf der Sandbox-Editor-Seite\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Wird heruntergeladen...\",\n    \"description\": \"Beschriftung 'Herunterladen'\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Als .webm herunterladen\",\n    \"description\": \"Beschriftung 'WEBM herunterladen'-Schaltfläche\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \".webm-Video exportieren\",\n    \"description\": \"Beschriftung 'WEBM herunterladen'\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Als .mp4 herunterladen\",\n    \"description\": \"Beschriftung 'MP4 herunterladen'-Schaltfläche\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \".mp4-Video exportieren (empfohlen)\",\n    \"description\": \"Beschriftung 'MP4 herunterladen'\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Als .gif herunterladen\",\n    \"description\": \"Beschriftung 'GIF herunterladen'-Schaltfläche\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"GIF exportieren (max. 30 Sekunden)\",\n    \"description\": \"Beschriftung 'GIF herunterladen'\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Bereit, deine Aufnahmen zu verbessern?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Teile in der Cloud, nutze Zooms, Vorlagen... und unterstütze eine Indie-Entwicklerin, die deine Privatsphäre respektiert ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Mehr über Pro erfahren\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Schon ein Konto? Jetzt einloggen\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Video teilen\",\n    \"description\": \"Schaltfläche 'Video teilen' auf der Sandbox-Editor-Seite\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Bestehendes Audio ersetzen\",\n    \"description\": \"Beschriftung 'Audio ersetzen'-Schaltfläche im Editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zum Cursor zoomen\",\n    \"description\": \"Pop-up 'Zum Punkt zoomen'\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"In der Seite bleiben während der Aufnahme\",\n    \"description\": \"Pop-up 'In Seite bleiben'\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Warnung: Mikrofon ist ausgeschaltet\",\n    \"description\": \"Pop-up 'Mikrofon-Erinnerung'\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Hilfe\",\n    \"description\": \"Schaltfläche 'Hilfe' im Pop-up\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Aufnahme pausiert. Drücken Sie die Wiedergabetaste, um fortzusetzen.\",\n    \"description\": \"Titel des Pop-ups 'Aufnahme pausiert'\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity hat keine Berechtigung zur Bildschirmaufnahme\",\n    \"description\": \"Titel des Berechtigungspop-ups\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Um die Bildschirmaufnahme zu aktivieren, müssen Sie Screenity die Berechtigung zum Erfassen Ihres Bildschirms erteilen, wenn Sie dazu aufgefordert werden.\",\n    \"description\": \"Beschreibung des Berechtigungspop-ups\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Bildschirmaufnahme aktivieren\",\n    \"description\": \"Aktion des Berechtigungspop-ups\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Abbrechen\",\n    \"description\": \"Abbrechen des Berechtigungspop-ups\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Ihr Mikrofon ist stummgeschaltet\",\n    \"description\": \"Titel des Pop-ups 'Mikrofon stummgeschaltet'\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Um Audio in Ihrer Aufnahme einzuschließen, müssen Sie Ihr Mikrofon wieder aktivieren. Möchten Sie ohne Audio fortfahren?\",\n    \"description\": \"Beschreibung des Pop-ups 'Mikrofon stummgeschaltet'\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Ja, fortfahren\",\n    \"description\": \"Fortsetzen der Mikrofon-Stummschaltungsmodalität\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Abbrechen\",\n    \"description\": \"Abbrechen der Mikrofon-Stummschaltungsmodalität\"\n  },\n  \"setupTitle\": {\n    \"message\": \"So starten Sie mit Screenity in drei einfachen Schritten:\",\n    \"description\": \"Titel der Einrichtungsschritte\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Klicken Sie auf das \",\n    \"description\": \"Einrichtungsschritt 1, vor dem Symbol. Es benötigt ein Leerzeichen am Ende.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" Symbol der Erweiterungen\",\n    \"description\": \"Einrichtungsschritt 1, nach dem Symbol. Es benötigt ein Leerzeichen am Anfang.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Drücken Sie das \",\n    \"description\": \"Einrichtungsschritt 2, vor dem Symbol. Es benötigt ein Leerzeichen am Ende.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" Symbol für das Anheften\",\n    \"description\": \"Einrichtungsschritt 2, nach dem Symbol. Es benötigt ein Leerzeichen am Anfang.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Klicken Sie auf das \",\n    \"description\": \"Einrichtungsschritt 3, vor dem Symbol. Es benötigt ein Leerzeichen am Ende.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity-Symbol, um zu starten\",\n    \"description\": \"Einrichtungsschritt 3, nach dem Symbol. Es benötigt ein Leerzeichen am Anfang.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Fantastisch! Sie sind bereit\",\n    \"description\": \"Titel der Einrichtung abgeschlossen\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Sie können jetzt hier oder in einem anderen Tab mit der Aufnahme beginnen.\",\n    \"description\": \"Beschreibung der Einrichtung abgeschlossen\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Hier klicken, um zu zeichnen\",\n    \"description\": \"Onboarding 'Hier klicken'\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Entspannen Sie sich. Klicken Sie überall hin, um den Countdown zu stoppen.\",\n    \"description\": \"Meldung 'Countdown'\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Bearbeitungen können aufgrund der Intervalle zwischen den Frames für kurze Zeiträume ungenau sein.\",\n    \"description\": \"Informationen darüber, dass der Editor zu klein ist\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Das Video wird verarbeitet, die Wiedergabe kann ungenau oder unterbrochen sein.\",\n    \"description\": \"Verarbeitungsbanner im Editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Das Zuschneiden kann eine Weile dauern\",\n    \"description\": \"Titel der Informationen zum Zuschneiden\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Upgrade für eine schnellere Verarbeitung\",\n    \"description\": \"Beschreibung der Informationen zum Zuschneiden\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Mikrofon aktiviert\",\n    \"description\": \"Toast-Titel 'Mikrofon aktiviert'\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Mikrofon deaktiviert\",\n    \"description\": \"Toast-Titel 'Mikrofon deaktiviert'\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"UI-Benachrichtigungen ausblenden\",\n    \"description\": \"Schaltfläche 'UI-Benachrichtigungen ausblenden'\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Nicht erneut anzeigen\",\n    \"description\": \"Schaltfläche 'Nicht erneut anzeigen'\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Symbolleiste nur bei Bedarf anzeigen\",\n    \"description\": \"Schaltfläche 'Symbolleiste nur bei Bedarf anzeigen'\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Aufnahme beenden\",\n    \"description\": \"Schaltfläche 'Aufnahme beenden' im Aufnahmescreen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Willkommen beim neuen Screenity!\",\n    \"description\": \"Titel der Update-Ankündigung für bestehende Benutzer\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Wir stellen ein neues Erscheinungsbild und viele spannende Funktionen vor, aber keine Sorge, es handelt sich immer noch um die gleiche kostenlose, private und Open-Source-Erweiterung, die Sie kennen und lieben.\",\n    \"description\": \"Beschreibung der Update-Ankündigung für bestehende Benutzer\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Erfahren Sie mehr über das Update.\",\n    \"description\": \"Link zur Update-Ankündigung für bestehende Benutzer\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Los geht's\",\n    \"description\": \"Schaltfläche 'Los geht's' für bestehende Benutzer in der Update-Ankündigung\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Fehler beim Starten der Aufnahme\",\n    \"description\": \"Titel des Stream-Fehlermodals\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Es sieht so aus, als ob beim Starten der Aufnahme ein Fehler aufgetreten ist. Starten Sie Ihren Browser neu und versuchen Sie es erneut. Wenn das Problem weiterhin besteht, kontaktieren Sie uns bitte unter support@screenity.io.\",\n    \"description\": \"Beschreibung des Stream-Fehlermodals\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Höchste Qualität\",\n    \"description\": \"Beschriftung für den Wechsel zur höchsten Qualität im Popup-Fenster\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Letzte Aufnahme wiederherstellen\",\n    \"description\": \"Schaltfläche zum Wiederherstellen der letzten Aufnahme im Popup-Fenster\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Haben Sie Probleme?\",\n    \"description\": \"Schaltfläche 'Probleme' im Editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Können Sie Ihre Aufnahme nicht sehen?\",\n    \"description\": \"Titel des Modalfensters 'Probleme' im Editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Wenn Sie Ihre Aufnahme abgeschlossen haben und sich auf dieser Seite befinden, ohne Ihr Video zu sehen, können Sie versuchen, die Rohvideodaten herunterzuladen, sofern verfügbar. Sie können auch Kontakt aufnehmen und einen Fehlerbericht senden.\",\n    \"description\": \"Beschreibung des Modalfensters 'Probleme' im Editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Rohvideodaten herunterladen\",\n    \"description\": \"Schaltfläche im Modalfenster 'Probleme' im Editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Fehler melden\",\n    \"description\": \"Zweite Schaltfläche im Modalfenster 'Probleme' im Editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Keine Aufzeichnung gefunden, tut uns leid :(\",\n    \"description\": \"Meldung 'Keine Aufzeichnung gefunden' im Editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Speichergrenze erreicht\",\n    \"description\": \"Titel für erreichte Speichergrenze\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Ihr Gerät hat seinen verfügbaren Speicherplatz für die Aufnahme erschöpft, wodurch die Aufnahme automatisch gestoppt wurde. Wenn Sie länger aufnehmen möchten, können Sie versuchen, die Qualität des Videos zu reduzieren oder Speicherplatz auf Ihrem Gerät freizugeben.\",\n    \"description\": \"Beschreibung für erreichte Speichergrenze\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Verstanden\",\n    \"description\": \"Verstanden-Button\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Nicht genügend Speicherplatz\",\n    \"description\": \"Titel für nicht genügend Speicherplatz\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity benötigt mindestens 1 GB freien Speicherplatz für die Aufnahme. Bitte schaffen Sie Speicherplatz frei und versuchen Sie es erneut.\",\n    \"description\": \"Beschreibung für nicht genügend Speicherplatz\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Vorherige Aufnahmen löschen\",\n    \"description\": \"Button zum Löschen vorheriger Aufnahmen\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Erweitert\",\n    \"description\": \"Erweiterte Sektion auf der Sandbox-Editor-Seite\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Rohvideo herunterladen\",\n    \"description\": \"Schaltfläche zum Herunterladen des Rohvideos\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exportieren Sie die originalen Aufnahmedaten\",\n    \"description\": \"Beschriftung der Schaltfläche zum Herunterladen des Rohvideos\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Hilfe bei Ihrer Aufnahme erhalten\",\n    \"description\": \"Schaltfläche zur Problembehandlung\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Stellen Sie Ihr Video wieder her und lösen Sie andere Probleme\",\n    \"description\": \"Beschriftung zur Problembehandlung\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Rohvideo herunterladen\",\n    \"description\": \"Titel des Modals für das Rohvideo\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Das Rohvideo ist das Originalvideo, das von Screenity aufgenommen wurde. Dieses Video wurde nicht bearbeitet oder verändert und kann daher sehr groß sein und auf allen Geräten möglicherweise nicht abspielbar sein. Wenn Sie Probleme mit dem bearbeiteten Video haben, können Sie diese Datei verwenden, um Ihr Video wiederherzustellen.\",\n    \"description\": \"Beschreibung des Modals für das Rohvideo\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Herunterladen\",\n    \"description\": \"Schaltfläche des Modals für das Rohvideo\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Systeminformationen und Videodaten herunterladen und zur Fehlerbehebung senden\",\n    \"description\": \"Titel des Modals zur Problembehandlung\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Nach dem Herunterladen senden Sie die Datei an support@screenity.io mit allen relevanten Informationen. Wir verwenden diese Daten, um Ihnen bei der Fehlerbehebung von Problemen mit der Erweiterung zu helfen und Ihr Video gegebenenfalls wiederherzustellen.\",\n    \"description\": \"Beschreibung des Modals zur Problembehandlung\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Herunterladen\",\n    \"description\": \"Schaltfläche des Modals zur Problembehandlung\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Das Video ist zu lang, um auf Ihrem Gerät verarbeitet zu werden\",\n    \"description\": \"Titel des Modals für die 5-Minuten-Limitierung\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Da Screenity komplett privat ist und lokal ausgeführt wird, ist es von der Hardware Ihres Geräts begrenzt. Die Verarbeitung eines Videos dieser Länge würde zu lange dauern und sehr ressourcenintensiv sein. Keine Sorge, Sie können immer noch die WEBM-Datei oder die Rohaufnahme herunterladen, aber Bearbeitung und MP4-Export sind nicht verfügbar. Sie können das Video dennoch versuchen zu verarbeiten, wenn Sie die Risiken verstehen.\",\n    \"description\": \"Beschreibung des Modals für die 5-Minuten-Limitierung\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Ich verstehe das Risiko, trotzdem versuchen\",\n    \"description\": \"Schaltfläche des Modals für die 5-Minuten-Limitierung\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Mehr über die Verarbeitung langer Videos erfahren.\",\n    \"description\": \"Link 'Erfahren Sie mehr' im Modal für die 5-Minuten-Limitierung mit einem Punkt\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Wiederherstellungsmodus\",\n    \"description\": \"Titel des Wiederherstellungsmodus\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Daten zum Beheben von Problemen herunterladen\",\n    \"description\": \"Option zum Herunterladen zur Fehlerbehebung\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Hilfe-Center\",\n    \"description\": \"Hilfe-Navigationspunkt\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Computer-Audio aufzeichnen\",\n    \"description\": \"Titel der Audio-Warnung\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Um Audio von dieser Seite aufzuzeichnen, müssen Sie zu „$tab$“ wechseln.\",\n    \"description\": \"Beschreibung der Audio-Warnung\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Tab-Bereich\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Erweiterung auf Tab nicht unterstützt\",\n    \"description\": \"Titel der Erweiterung nicht auf Tab unterstützt\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity wurde in Ihrem vorherigen Tab nicht unterstützt. Sie können Ihre Aufnahme hier dennoch starten und dann zum Tab wechseln, aber Ihre Kamera und Symbolleiste werden nicht sichtbar sein.\",\n    \"description\": \"Beschreibung der Erweiterung nicht auf Tab unterstützt\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Lokales Backup für Ihre Screenity-Aufnahmen einrichten\",\n    \"description\": \"Titel des Backups\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Bewahren Sie eine Kopie aller Ihrer Aufnahmen auf. Wenn Sie keine Backups einrichten, werden Ihre Aufnahmen nur gespeichert, wenn Sie sich entscheiden, sie herunterzuladen.\",\n    \"description\": \"Beschreibung des Backups\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Es könnte erforderlich sein, zuerst einen neuen Ordner zu erstellen, wenn Sie versuchen, in Downloads oder andere Systemordner zu speichern.\",\n    \"description\": \"Beschreibung des Backups\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Ordner auswählen\",\n    \"description\": \"Schaltfläche „Ordner auswählen“\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Jetzt nicht\",\n    \"description\": \"Schaltfläche „Jetzt nicht“\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Ihre Aufnahmen werden lokal gesichert\",\n    \"description\": \"Titel der aktiven Backups\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Um zu verhindern, dass Sie jedes Mal um Berechtigungen gebeten werden, wenn Sie aufzeichnen, empfehlen wir Ihnen, diesen Tab geöffnet zu lassen.\",\n    \"description\": \"Beschreibung der aktiven Backups\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Tab dennoch schließen\",\n    \"description\": \"Schaltfläche „Tab dennoch schließen“\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Sicherung aller Aufnahmen beenden\",\n    \"description\": \"Schaltfläche „Backups beenden“\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Bitte bestätigen Sie erneut den Zugriff auf Ihren lokalen Screenity-Backup-Ordner\",\n    \"description\": \"Titel der Backup-Bestätigung\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Es tut uns leid, aber wir benötigen noch einmal Ihre Zustimmung, um Ihre Aufnahmen weiterhin lokal zu sichern. Leider müssen wir jedes Mal fragen, wenn Sie diesen Tab schließen, was passieren kann, wenn Sie den Browser schließen oder Ihren Computer neu starten.\",\n    \"description\": \"Beschreibung der Backup-Bestätigung\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Erlauben Sie Screenity, Aufnahmen weiterhin zu sichern\",\n    \"description\": \"Schaltfläche „Erlauben“ in der Backup-Bestätigung\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Aufnahmen sichern\",\n    \"description\": \"Beschriftung für die Backup-Option\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Berechtigung zur Sicherung der Aufnahme nicht erteilt\",\n    \"description\": \"Titel des Backup-Berechtigungsfehlers\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Sie haben keinen Ordner für die lokale Sicherung Ihrer Aufnahmen ausgewählt. Je nach Browser-Version oder Betriebssystem kann es erforderlich sein, den Ordner jedes Mal auszuwählen, wenn Sie mit der Aufzeichnung beginnen. Sie können es erneut versuchen oder Backups ganz deaktivieren, wenn Sie wiederholte Berechtigungsanfragen vermeiden möchten.\",\n    \"description\": \"Beschreibung des Backup-Berechtigungsfehlers\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Computer-Audio aufzeichnen\",\n    \"description\": \"Titel der Audio-Warnung für macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"Auf macOS können Sie nur den Tab-Audio aufzeichnen. Wählen Sie die Option 'Chrome-Tab' und stellen Sie sicher, dass 'Tab-Audio ebenfalls teilen' aktiviert ist.\",\n    \"description\": \"Beschreibung der Audio-Warnung für macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Computer-Audio aufzeichnen\",\n    \"description\": \"Titel der Audio-Warnung für andere Systeme\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Stellen Sie sicher, dass Sie die Option 'System-Audio teilen' aktivieren, wenn Sie Computer-Audio aufzeichnen möchten. Wenn Sie diese Option nicht sehen, versuchen Sie, Chrome zu aktualisieren oder zur Tab-Aufzeichnung zu wechseln.\",\n    \"description\": \"Beschreibung der Audio-Warnung für andere Systeme\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Maximale Auflösung\",\n    \"description\": \"Beschriftung für maximale Auflösung im Einstellungsmenü\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Maximale FPS\",\n    \"description\": \"Beschriftung für maximale FPS im Einstellungsmenü\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Nicht genügend RAM\",\n    \"description\": \"Beschriftung 'Nicht genügend RAM' im Einstellungsmenü\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Fenstergröße ändern\",\n    \"description\": \"Beschriftung für die Änderung der Fenstergröße im Einstellungsmenü\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Der Bildschirm ist zu klein für diese Auflösung\",\n    \"description\": \"Tooltip, der anzeigt, dass der Bildschirm zu klein ist, im Einstellungsmenü\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Auf diesem Gerät ist diese Auflösung nicht verfügbar\",\n    \"description\": \"Tooltip, der anzeigt, dass diese Auflösung auf dem Gerät nicht verfügbar ist, im Einstellungsmenü\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Berechtigungen überprüfen\",\n    \"description\": \"Schaltfläche zur Überprüfung von Berechtigungen\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Weitere Aufnahme hinzufügen\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Bereite alles vor...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Einen Moment Geduld, deine Aufnahme wird vorbereitet.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Symbolleiste ausgeblendet. Aktiviere sie in den Pop-up-Optionen.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Anmelden oder registrieren\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Abmelden\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Du wurdest abgemeldet\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Speicherlimit erreicht\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Du hast deine 100 GB Speicher ausgeschöpft. Bitte lösche alte Videos oder Szenen, um weiter aufzunehmen.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Speicher verwalten\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Schließen\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Speicher konnte nicht geprüft werden\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Es gab ein Problem beim Prüfen deines Speichers. Bitte melde dich erneut an und versuche es nochmal.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Dein Speicher konnte nicht überprüft werden. Versuche es in ein paar Sekunden erneut oder lade die Seite neu.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Erneut versuchen\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Szene zur Multi-Aufnahme hinzugefügt\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Bereit, eine neue Szene aufzunehmen\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Die Aufnahme nähert sich dem 90-Minuten-Limit, sie wird bald beendet\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Aufnahme wegen 90-Minuten-Limit gestoppt\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Tab-Aufnahme auf dieser Seite deaktiviert. Bildschirmaufnahme aktiviert.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Videoaufnahme abgebrochen\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Link in Zwischenablage kopiert\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Link konnte nicht kopiert werden\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Neueste\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Älteste\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Alle Videos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Keine Videos gefunden\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Videos werden geladen...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Zum Dashboard\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Willkommen bei Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Keine Werbung. Kein Tracking. Keine Limits.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Einfach aufnehmen.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Kostenlos nutzen – ohne Anmeldung!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Mehr aus deinen Videos herausholen?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Cloud-Speicher, teile Videos per Link\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Mehrere Clips zu einer Story kombinieren\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Intelligente Zooms per Klick (oder selbst hinzufügen!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Vorlagen, Untertitel, Layouts hinzufügen...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Eigene Hintergründe und Mockups\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Zusätzliche Funktionen freischalten\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Unterstütze die Entwicklung durch eine Indie-Macherin!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Kontoeinstellungen\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Support\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Dein Pro-Abo ist inaktiv\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Dein Screenity Pro-Abo ist inaktiv.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Bitte reaktiviere es, um den Zugriff fortzusetzen.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Deine Videos und Daten werden dauerhaft gelöscht am \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Abo verwalten\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Möchtest du die kostenlose Version weiterhin nutzen?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Abmelden und downgraden\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Du wurdest abgemeldet\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Melde dich an, um deine Aufnahmen zu sichern und Premium-Funktionen zu nutzen.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Erneut anmelden\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Du kannst die Erweiterung ohne Konto nutzen – aber deine Aufnahmen werden nicht gespeichert und können später nicht wiederhergestellt werden.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Ohne Anmeldung fortfahren\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Sofortaufnahme-Modus\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Sofortiger Download, aber Kamera und Layout sind später nicht bearbeitbar.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Sofortaufnahme-Modus\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Dies speichert alles in einem Video für den Sofort-Download und Teilen. Die Kamera-Anordnung kann danach nicht mehr geändert werden, andere Bearbeitungen sind aber möglich.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Verstanden\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Tab-Aufnahme ist auf dieser Seite deaktiviert\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Wird hinzugefügt zu: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Fertigstellen\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Mehr aus deinen Aufnahmen herausholen?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Melde dich an, um Videos in der Cloud zu speichern, per Link zu teilen und auf erweiterte Bearbeitung zuzugreifen.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Anmelden, um Pro-Funktionen freizuschalten\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Unterstütze die Entwicklung durch eine Indie-Macherin\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Mehr Funktionen freischalten\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Anmelden zum Teilen (Pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Cloud-Sharing, Multi-Szenen-Bearbeitung, Auto-Zooms, Untertitel & mehr freischalten\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity bleibt immer kostenlos, quelloffen und werbefrei. Pro deckt Cloud- und Infrastrukturkosten und unterstützt die Entwicklung durch eine Indie-Entwicklerin! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Nicht mehr anzeigen\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Ausprobieren\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Multi-Modus\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Nimm mehrere Szenen auf – deinen Bildschirm, deine Kamera oder beides – nacheinander. Perfekt für mehrere Takes, Perspektivwechsel oder um deine Aufnahme in Teile zu gliedern. Wenn du fertig bist, klicke auf Fertig, um den Editor mit allen Szenen in einem einzigen Video zu öffnen.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Aktiviere Pro, um loszulegen\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Abonniere, um auf die Pro-Funktionen zuzugreifen.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Pro abonnieren\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Du nutzt Screenity lokal? Hilf mit, die Weiterentwicklung zu unterstützen!\",\n    \"description\": \"Titel des Banners für Self-Hosting im Editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity wird von einer Indie-Entwicklerin gebaut. Self-Hosting ist kostenlos – wenn es dir hilft, unterstütze sie gerne mit Pro ❤️\",\n    \"description\": \"Beschreibung des Banners für Self-Hosting im Editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Willkommen zurück bei Screenity\",\n    \"description\": \"Titel für das Begrüßungs-Popup für zurückkehrende Nutzer\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Neu: Noch mehr aus deinen Aufnahmen herausholen?\",\n    \"description\": \"Titel für die Cloud-Speicher-Funktion im Begrüßungs-Popup\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro ist eine neue, optionale Plattform, mit der du Videos in der Cloud speichern, per Link teilen und erweiterte Bearbeitungsfunktionen freischalten kannst – wenn du sie brauchst.\",\n    \"description\": \"Beschreibung der Cloud-Speicher-Funktion im Begrüßungs-Popup\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Mehr über Screenity Pro erfahren\",\n    \"description\": \"Call-to-Action-Button für die Cloud-Funktion im Begrüßungs-Popup\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Willkommen bei Screenity Pro\",\n    \"description\": \"Titel des Willkommensmodals für Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Du bist bereit für deine erste Aufnahme! Hier ein paar Dinge, die du beachten solltest:\\n\\n- Die Kamerablase kann während der Aufnahme verschwinden – das ist normal. Sie wird im Hintergrund separat aufgenommen, sodass du sie später beliebig positionieren kannst.\\n- Zooms werden nur automatisch bei Klicks in Chrome-Tabs erstellt – das ist eine Einschränkung des Browsers. Du kannst später jederzeit manuell weitere Zooms hinzufügen.\\n- Für schnelle Aufnahmen mit sofortigem Download probiere den Sofortmodus. Beachte jedoch, dass die Bearbeitungsoptionen eingeschränkt sind: keine Hintergründe, Layouts oder erweiterten Anpassungen.\",\n    \"description\": \"Beschreibung des Willkommensmodals für Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Verstanden\",\n    \"description\": \"Bestätigungsbutton im Willkommensmodal für Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 Fehler — aus\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Willkommen bei der Screenity Pro-Erweiterung\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Ein paar kurze Tipps vor der Aufnahme.<br/>Auto-Zooms funktionieren nur bei Klicks in <strong>Chrome-Tabs</strong>. Zusätzliche Zooms kannst du später manuell hinzufügen.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Aufnahmeleiste & Effekte\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Nutze diese Leiste zum Zeichnen, für Cursor-Effekte, Unschärfe und Aufnahme-Steuerung.<br/><br/><strong>Diese Leiste wird mit aufgezeichnet</strong>, wenn du sie nicht ausblendest.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Kamera wird separat aufgenommen\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Deine Kamera kann während der Aufnahme <strong>ausgeblendet werden oder in PiP wechseln</strong> - das ist normal.<br/><br/>Sie wird separat erfasst, damit du sie später frei platzieren kannst.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Sofortmodus\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Ideal zum schnellen Teilen mit <strong>unbegrenzten Downloads</strong>.<br/><br/>Erweiterte Layout- und Editor-Funktionen sind in diesem Modus nicht verfügbar.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Verstanden\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Weiter\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Zurück\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Mehr erfahren\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Onboarding zurücksetzen\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Wenig Speicherplatz. Aufnahme wird jetzt gespeichert. Alles Aufgenommene ist sicher.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Lange Aufnahme\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Deine Aufnahme ist vollständig und sicher. Als WebM herunterladen.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Für lange Aufnahmen nicht verfügbar\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Im Wiederherstellungsmodus nicht verfügbar\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Audiobearbeitung zu lang\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Diese Audiobearbeitung wird für Clips über 15 Minuten nicht unterstützt. Dein Original ist unverändert. Versuche, den Clip zuerst zu kürzen.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Bearbeitung abgelaufen\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Die Bearbeitung konnte nicht rechtzeitig abgeschlossen werden. Deine Originalaufnahme ist unverändert. Versuche es erneut oder kürze den Clip zuerst.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Bildschirmfreigabe beendet. Die bisherige Aufnahme ist gespeichert. Beende die Aufnahme, wenn du bereit bist.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Es gab ein Verarbeitungsproblem, aber deine Aufnahme ist sicher. Du kannst sie unten herunterladen.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Bearbeitung wird verarbeitet. Deine Originalaufnahme bleibt unverändert.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Bearbeitung konnte nicht angewandt werden\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Bei der Verarbeitung ist etwas schiefgelaufen. Deine Originalaufnahme ist sicher. Du kannst es erneut versuchen oder sie so herunterladen.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Bildschirmfreigabe beendet. Aufnahme wird jetzt gespeichert. Alles Aufgenommene ist sicher.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio getrennt. Aufnahme wird jetzt gespeichert. Das bisherige Video ist sicher.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Aufnahme gespeichert. Der Editor konnte nicht geöffnet werden. Klicke auf das Screenity-Symbol, um es erneut zu versuchen.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Das Speichern dauert länger als erwartet. Deine Daten sind sicher. Der Editor wird gleich geöffnet.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Debug-Info kopieren\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Debug-Info kopiert\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Hilfe erhalten\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Aufnahme war zu kurz zum Speichern\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Schnellaufnahme fehlgeschlagen\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"Die Ausgabe der Schnellaufnahme konnte auf diesem Gerät nicht validiert werden.\\nDu kannst die Datei trotzdem herunterladen.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Trotzdem herunterladen\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Abbrechen\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/en/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Screen Recorder & Annotation Tool\",\n    \"description\": \"Extension name\"\n  },\n  \"extDesc\": {\n    \"message\": \"The free and privacy-friendly screen recorder for everyone. Capture, annotate, edit videos and more - all with no sign in needed.\",\n    \"description\": \"Extension description\"\n  },\n  \"recordTab\": {\n    \"message\": \"Record\",\n    \"description\": \"Record tab label on popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Your videos\",\n    \"description\": \"Videos tab label on popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Screen\",\n    \"description\": \"Screen recording type label on popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Tab area\",\n    \"description\": \"Tab area recording type label on popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Camera\",\n    \"description\": \"Camera recording type label on popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Mockup recording type label on popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Custom area recording disabled\",\n    \"description\": \"Custom area recording disabled warning on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Update Chrome version to 110+\",\n    \"description\": \"Custom area recording disabled warning description on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Update\",\n    \"description\": \"Custom area recording disabled warning action on popup for Chrome version older than 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Check your permissions\",\n    \"description\": \"Camera / microphone permissions warning modal title\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Looks like Screenity can’t access your camera or microphone. Make sure to allow access by clicking on the camera icon in the address bar.\",\n    \"description\": \"Camera / microphone permissions warning modal description\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Dismiss\",\n    \"description\": \"Camera / microphone permissions warning modal dismiss button\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Allow camera access\",\n    \"description\": \"Allow camera access button\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Allow microphone access\",\n    \"description\": \"Allow microphone access button\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Push to talk\",\n    \"description\": \"Push to talk switch label\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Set an area to record in\",\n    \"description\": \"Custom area switch label\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Flip camera\",\n    \"description\": \"Flip camera switch label\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Background effects\",\n    \"description\": \"Background effects switch label\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Start recording\",\n    \"description\": \"Record button label\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Starting recording...\",\n    \"description\": \"Record button in progress label\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Set a camera to record\",\n    \"description\": \"Record button label when in camera mode and no camera is selected\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Show more options\",\n    \"description\": \"Show more options label\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Include system audio\",\n    \"description\": \"Tab/system audio label\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Hide toolbar\",\n    \"description\": \"Hide toolbar label\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Countdown\",\n    \"description\": \"Countdown label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Set a time limit\",\n    \"description\": \"Alarm label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Blur\",\n    \"description\": \"Blur background label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"None\",\n    \"description\": \"No device selected\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"No camera\",\n    \"description\": \"No camera selected\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"No microphone\",\n    \"description\": \"No microphone selected\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Select a source\",\n    \"description\": \"Select source placeholder\"\n  },\n  \"offLabel\": {\n    \"message\": \"Off\",\n    \"description\": \"Off label on dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Width\",\n    \"description\": \"Region width label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Height\",\n    \"description\": \"Region height label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Click to place image\",\n    \"description\": \"Toast that shows when adding a new image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Finish recording\",\n    \"description\": \"Finish recording tooltip\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Restart recording\",\n    \"description\": \"Restart recording tooltip\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Pause recording\",\n    \"description\": \"Pause recording tooltip\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Resume recording\",\n    \"description\": \"Resume recording tooltip\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Cancel recording\",\n    \"description\": \"Cancel recording tooltip\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Toggle drawing tools\",\n    \"description\": \"Toggle drawing tools tooltip\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Toggle blur tool\",\n    \"description\": \"Toggle blur tools tooltip\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Toggle cursor options\",\n    \"description\": \"Toggle cursor options tooltip\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Disable camera\",\n    \"description\": \"Disable camera tooltip\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Enable camera\",\n    \"description\": \"Enable camera tooltip\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"No camera permissions\",\n    \"description\": \"No camera permissions tooltip\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Disable microphone\",\n    \"description\": \"Disable microphone tooltip\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Enable microphone\",\n    \"description\": \"Enable microphone tooltip\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"No microphone permissions\",\n    \"description\": \"No microphone permissions tooltip\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Select tool\",\n    \"description\": \"Select tool tooltip\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Pen tool\",\n    \"description\": \"Pen tool tooltip\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Highlighter tool\",\n    \"description\": \"Highlighter tool tooltip\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Eraser tool\",\n    \"description\": \"Eraser tool tooltip\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Text tool\",\n    \"description\": \"Text tool tooltip\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Shape tool\",\n    \"description\": \"Shape tool tooltip\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Arrow tool\",\n    \"description\": \"Arrow tool tooltip\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Image tool\",\n    \"description\": \"Image tool tooltip\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Undo\",\n    \"description\": \"Undo tooltip\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Redo\",\n    \"description\": \"Redo tooltip\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Clear canvas\",\n    \"description\": \"Clear canvas tooltip\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"More colors\",\n    \"description\": \"More colors tooltip\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Thick stroke\",\n    \"description\": \"Thick stroke tooltip\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Medium stroke\",\n    \"description\": \"Medium stroke tooltip\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Thin stroke\",\n    \"description\": \"Thin stroke tooltip\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Toggle picture-in-picture mode\",\n    \"description\": \"Toggle picture-in-picture mode tooltip\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Toggle fill\",\n    \"description\": \"Toggle fill tooltip\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Drawing mode\",\n    \"description\": \"Drawing mode toast\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Blur mode\",\n    \"description\": \"Blur mode toast\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Clear all blurred elements\",\n    \"description\": \"Clear all blurred elements tooltip\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Highlight clicks\",\n    \"description\": \"Highlight clicks tooltip\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Highlight cursor\",\n    \"description\": \"Highlight cursor tooltip\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Spotlight cursor\",\n    \"description\": \"Spotlight cursor tooltip\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Don't show again\",\n    \"description\": \"Permissions modal dismiss button\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Are you sure you want to restart the recording?\",\n    \"description\": \"Restart recording modal title\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Your current video progress will be lost.\",\n    \"description\": \"Restart recording modal description\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Restart recording\",\n    \"description\": \"Restart recording modal restart button\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Resume recording\",\n    \"description\": \"Restart recording modal resume button\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Are you sure you want to discard the recording?\",\n    \"description\": \"Discard recording modal title\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Your current video progress will be lost.\",\n    \"description\": \"Discard recording modal description\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Discard recording\",\n    \"description\": \"Discard recording modal discard button\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Resume recording\",\n    \"description\": \"Discard recording modal resume button\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Choose what you want to record\",\n    \"description\": \"Title in recording page\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Recording...\",\n    \"description\": \"Title in recording page while recording\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Keep this tab open. It will close once your recording has been saved.\",\n    \"description\": \"Description in recording page\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparing recording...\",\n    \"description\": \"Title in sandbox page while recording / saving\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Keep this tab open. It will update with your recording when it's ready.\",\n    \"description\": \"Description in sandbox page while recording / saving\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Title in navigation of the sandbox editor page\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Cancel\",\n    \"description\": \"Cancel button in sandbox editor page\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Revert to original\",\n    \"description\": \"Revert button in sandbox editor page\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Reset\",\n    \"description\": \"Reset button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Save changes\",\n    \"description\": \"Save button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Saving...\",\n    \"description\": \"Save button in sandbox editor page while saving\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Drag and drop your audio file\",\n    \"description\": \"Drag and drop audio file in sandbox editor page\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Or click to browse\",\n    \"description\": \"Or click to browse audio file in sandbox editor page\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Settings\",\n    \"description\": \"Audio settings title in sandbox editor page\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Audio volume label in sandbox editor page\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Update\",\n    \"description\": \"Update audio button in sandbox editor page\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Crop\",\n    \"description\": \"Crop title in sandbox editor page\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Width\",\n    \"description\": \"Width label for properties\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Height\",\n    \"description\": \"Height label for properties\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Left\",\n    \"description\": \"Left label for properties\"\n  },\n  \"topLabel\": {\n    \"message\": \"Top\",\n    \"description\": \"Top label for properties\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Drag the handles and use the buttons on the left to edit the selected section.\",\n    \"description\": \"Info about trimming in sandbox editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Trim video\",\n    \"description\": \"Trim button in sandbox editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Trimming...\",\n    \"description\": \"Trim button in sandbox editor while trimming\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Cut section\",\n    \"description\": \"Cut button in sandbox editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Cutting...\",\n    \"description\": \"Cut button in sandbox editor while cutting\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Mute audio\",\n    \"description\": \"Mute button in sandbox editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Muting...\",\n    \"description\": \"Mute button in sandbox editor while muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Learn more.\",\n    \"description\": \"Learn more with a dot\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Undo\",\n    \"description\": \"Undo label\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Redo\",\n    \"description\": \"Redo label\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Leave a review, it helps!\",\n    \"description\": \"Leave a review button\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Follow for updates\",\n    \"description\": \"Follow for updates button\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"You are currently offline\",\n    \"description\": \"Offline label\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Some features are unavailable until reconnected\",\n    \"description\": \"Offline label description\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Try again\",\n    \"description\": \"Offline label try again button\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome needs to be updated\",\n    \"description\": \"Update Chrome label\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Update to access more features\",\n    \"description\": \"Update Chrome label description\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Learn more\",\n    \"description\": \"Learn more button\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video too long to process in device\",\n    \"description\": \"Over 5 minutes limit label in editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Editing and MP4 export are unavailable\",\n    \"description\": \"Over 5 minutes limit description in editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"The video is processing locally...\",\n    \"description\": \"Video processing label in editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Learn about the faster cloud version\",\n    \"description\": \"Video processing description in editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Edit\",\n    \"description\": \"Edit title in sandbox editor page\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"No connection\",\n    \"description\": \"No connection label\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Not available\",\n    \"description\": \"Not available label\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Edit video\",\n    \"description\": \"Trim label button\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Trim, cut, or mute the video\",\n    \"description\": \"Trim label\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Preparing...\",\n    \"description\": \"Preparing label\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Crop\",\n    \"description\": \"Crop label button\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Crop and resize the video\",\n    \"description\": \"Crop label\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Add audio\",\n    \"description\": \"Add audio label button\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Upload your own audio to add to the video\",\n    \"description\": \"Add audio label\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Save\",\n    \"description\": \"Save title in sandbox editor page\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Sign out of Drive\",\n    \"description\": \"Sign out of Google Drive label\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Saving...\",\n    \"description\": \"Saving to Google Drive label\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Save to Drive\",\n    \"description\": \"Save to Google Drive button\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Save the current version to Google Drive\",\n    \"description\": \"Save to Google Drive label\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Sign in and save to Drive\",\n    \"description\": \"Sign in to Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Export\",\n    \"description\": \"Export title in sandbox editor page\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Downloading...\",\n    \"description\": \"Downloading label\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Download as .webm\",\n    \"description\": \"Download WEBM button\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Export a .webm video\",\n    \"description\": \"Download WEBM label\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Download as .mp4\",\n    \"description\": \"Download MP4 button\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Export an .mp4 video (recommended)\",\n    \"description\": \"Download MP4 label\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Download as .gif\",\n    \"description\": \"Download GIF button\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Export a GIF (max 30 seconds)\",\n    \"description\": \"Download GIF label\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Ready to level up your recordings?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Get cloud sharing, zooms, templates... And support a privacy-friendly tool made by a solo indie dev! ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Learn more about Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Already have an account? Log in\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Share video\",\n    \"description\": \"Share button in sandbox editor page\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Replace existing audio\",\n    \"description\": \"Replace audio button in editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom to cursor\",\n    \"description\": \"Zoom to point popup\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Stay in the page when recording\",\n    \"description\": \"Stay in page popup\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Microphone off warning\",\n    \"description\": \"Mic reminder popup\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Help\",\n    \"description\": \"Help popup button\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Recording paused. Press the play button to resume.\",\n    \"description\": \"Paused recording modal title\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity doesn't have permission to record your screen\",\n    \"description\": \"Permission modal title\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"To enable screen recording, you must grant Screenity permission to capture your screen when prompted.\",\n    \"description\": \"Permission modal description\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Enable screen recording\",\n    \"description\": \"Permission modal action\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Cancel\",\n    \"description\": \"Permission modal cancel\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Your microphone is muted\",\n    \"description\": \"Microphone muted modal title\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"To include audio in your recording, you'll need to unmute your microphone. Do you want to continue without audio?\",\n    \"description\": \"Microphone muted modal description\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Yes, continue\",\n    \"description\": \"Microphone muted modal continue\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Cancel\",\n    \"description\": \"Microphone muted modal cancel\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Get started with Screenity in three simple steps:\",\n    \"description\": \"Set up steps title\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Click the \",\n    \"description\": \"Set up step 1, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" extensions icon\",\n    \"description\": \"Set up step 1, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Press the \",\n    \"description\": \"Set up step 2, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" pin icon\",\n    \"description\": \"Set up step 2, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Click on the \",\n    \"description\": \"Set up step 3, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity icon to start\",\n    \"description\": \"Set up step 3, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Awesome! You’re all set\",\n    \"description\": \"Set up complete title\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"You can now start recording here, or in any other tab.\",\n    \"description\": \"Set up complete description\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Click here to draw\",\n    \"description\": \"Click here onboarding\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Relax. Click anywhere to stop the countdown.\",\n    \"description\": \"Countdown message\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Edits may be inaccurate for short time ranges due to the intervals between frames.\",\n    \"description\": \"Info about the editor being too small\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Video is processing, playback might be inaccurate or interrupted.\",\n    \"description\": \"Processing banner in editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Cropping might take a while\",\n    \"description\": \"Cropping info title\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Upgrade for faster processing\",\n    \"description\": \"Cropping info description\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microphone enabled\",\n    \"description\": \"Microphone enabled toast title\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microphone disabled\",\n    \"description\": \"Microphone disabled toast title\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Hide UI notifications\",\n    \"description\": \"Hide UI alerts button\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Don't show again\",\n    \"description\": \"Don't show again button\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Hide toolbar when not in use\",\n    \"description\": \"Only show toolbar on hover button\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Stop recording\",\n    \"description\": \"Stop recording button in recording scren\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Welcome to the new Screenity!\",\n    \"description\": \"Update announcement title for existing users\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"We’re introducing a new look and many exciting features, but don’t worry, it’s still the same free, private and open source extension you know and love.\",\n    \"description\": \"Update announcement description for existing users\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Learn more about the update.\",\n    \"description\": \"Update announcement learn more link for existing users\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Get started\",\n    \"description\": \"Update announcement button for existing users\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Recording stopped\",\n    \"description\": \"Stream error modal title\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Looks like something went wrong with the recording. Restart your browser and try again. If the issue persists, please contact us at support@screenity.io.\",\n    \"description\": \"Stream error modal description\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copy debug info\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Debug info copied to clipboard\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Get help\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Recording was too short to save\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Fast recorder failed\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"The fast recorder output couldn't be validated on this device.\\nYou can download the file anyway.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Download anyway\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Cancel\",\n    \"description\": \"Generic cancel button label\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Highest quality\",\n    \"description\": \"Highest quality toggle label in popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restore last recording\",\n    \"description\": \"Restore last recording button in popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Having issues?\",\n    \"description\": \"Having issues button in editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Can't see your recording?\",\n    \"description\": \"Having issues modal title in editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"If you've completed your recording and find yourself on this page without seeing your video, you can attempt to download the raw video data if it's available. You can also reach out and submit a bug report.\",\n    \"description\": \"Having issues modal description in editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Download raw video data\",\n    \"description\": \"Having issues modal button in editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Report bug\",\n    \"description\": \"Having issues modal button 2 in editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"No recording found, sorry :(\",\n    \"description\": \"No recording found alert in editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Memory limit reached\",\n    \"description\": \"Memory limit reached title\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Your device ran out of storage space, so the recording stopped automatically. Your recording up to that point is safe and you can download it from the editor. To record longer, try lowering the video quality or freeing up space on your device.\",\n    \"description\": \"Memory limit reached description\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Understood\",\n    \"description\": \"Understood button\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Not enough space\",\n    \"description\": \"Not enough space title\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity needs at least 1GB of free space to record. Please free up some space and try again.\",\n    \"description\": \"Not enough space description\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Clear previous recordings\",\n    \"description\": \"Clear previous recordings button\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Advanced\",\n    \"description\": \"Advanced section in sandbox editor page\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Download raw video file\",\n    \"description\": \"Download raw recording button\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Export the original recording data\",\n    \"description\": \"Download raw recording label\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Get help with your recording\",\n    \"description\": \"Troubleshoot button\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recover your video and solve other issues\",\n    \"description\": \"Troubleshoot label\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Download raw video file\",\n    \"description\": \"Raw recording modal title\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"The raw video file is the original video file that was recorded by Screenity. This video is not processed or edited, so it may be very large and may not be playable on all devices. If you are having issues with the processed video, you can use this file to recover your video.\",\n    \"description\": \"Raw recording modal description\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Download\",\n    \"description\": \"Raw recording modal button\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Download diagnostic report for troubleshooting\",\n    \"description\": \"Troubleshoot modal title\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"This will download a diagnostic report with system info and recent recording session data. Send it to support@screenity.io to help us troubleshoot your issue. No video or audio data is included.\",\n    \"description\": \"Troubleshoot modal description\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Download\",\n    \"description\": \"Troubleshoot modal button\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"The video is too long to process in your device\",\n    \"description\": \"Over 5 minutes limit modal title\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Since Screenity is completely private and runs locally, it's limited by your device's hardware. Processing a video of this length would take too long, and be very resource intensive. Don't worry, you can still download the WEBM file or the raw recording, but editing and MP4 export are not available. That said, you can still try to process the video anyway, if you understand the risks.\",\n    \"description\": \"Over 5 minutes limit modal description\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"I understand the risk, try anyway\",\n    \"description\": \"Over 5 minutes limit modal button\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Learn more about processing long videos.\",\n    \"description\": \"Over 5 minutes limit modal learn more link with a dot\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Recovery mode\",\n    \"description\": \"Recovery mode title\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Long recording\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Your recording is complete and safe. Download as WebM below.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Unavailable for long recordings\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Unavailable in recovery mode\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Download data for troubleshooting\",\n    \"description\": \"Download for troubleshooting option\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Help center\",\n    \"description\": \"Get help nav item\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Record computer audio\",\n    \"description\": \"Audio warning title\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"To record audio from this page, you need to switch to ‘$tab$’.\",\n    \"description\": \"Audio warning description\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Tab area\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extension not supported on tab\",\n    \"description\": \"Extension not supported on tab\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity wasn't supported in your previous tab. You can still start your recording here and switch back to the tab, but your camera and toolbar won't be visible.\",\n    \"description\": \"Extension not supported on tab description\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Set up a local backup for your Screenity recordings\",\n    \"description\": \"Backups title\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Keep a copy of all the recordings you make. If you don’t set up backups, your recordings will only be saved when you choose to download them.\",\n    \"description\": \"Backups description\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"You might need to create a new folder first if you're trying to save to Downloads or other system folders.\",\n    \"description\": \"Backups description\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Select a folder\",\n    \"description\": \"Select a folder button\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Not now\",\n    \"description\": \"Not now button\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \" Your recordings are being backed up locally\",\n    \"description\": \"Backups on title\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"To prevent being asked for permissions every time you record, we recommend leaving this tab open.\",\n    \"description\": \"Backups on description\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Close tab anyway\",\n    \"description\": \"Close tab anyway button\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Stop backing up all recordings\",\n    \"description\": \"Stop backups button\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Please reconfirm access to your local Screenity backup folder\",\n    \"description\": \"Backups confirm title\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"We’re sorry, but we need your permission once more to keep backing up your recordings. Unfortunately we will need to ask every time this tab is closed, which can happen if you close the browser or restart your computer.\",\n    \"description\": \"Backups confirm description\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Allow Screenity to keep backing up recordings\",\n    \"description\": \"Backups confirm allow button\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Back up recordings\",\n    \"description\": \"Backups toggle label\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permission not granted to back up the recording\",\n    \"description\": \"Backup permission fail title\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"You didn't select a folder to backup your recordings. Depending on your browser version or operating system you might be asked to select the folder every time you start recording. You can try again, or disable backups entirely if you'd rather not give permissions repeatedly.\",\n    \"description\": \"Backup permission fail description\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Record computer audio\",\n    \"description\": \"Record audio warning title\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"In macOS, you can only record Tab audio. Select the Chrome Tab option and make sure 'Also share tab audio' is enabled.\",\n    \"description\": \"macOS audio warning description\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Record computer audio\",\n    \"description\": \"Record audio warning title\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Make sure to enable the option to 'Share system audio' if you want to record computer audio. If you don't see this option, try updating Chrome, or switch to Tab recording.\",\n    \"description\": \"Other audio warning description\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Max resolution\",\n    \"description\": \"Max resolution label in settings menu\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Max FPS\",\n    \"description\": \"Max FPS label in settings menu\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Not enough RAM\",\n    \"description\": \"Not enough RAM label in settings menu\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Resize window\",\n    \"description\": \"Resize window label in settings menu\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"The screen is too small for this resolution\",\n    \"description\": \"Screen too small tooltip in settings menu\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Cannot record at this resolution in your device\",\n    \"description\": \"Maxed out resolution tooltip in settings menu\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Review permissions\",\n    \"description\": \"Review permissions button\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Add another recording\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Getting things ready...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Hang tight! Your recording is being prepared.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Toolbar hidden. Re-enable it in the popup options.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Log in or sign up\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Log out\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"You have been logged out\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Storage limit reached\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"You’ve used your full 100 GB of storage. Please delete old videos or scenes to continue recording.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Manage storage\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Close\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Couldn't check storage\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"There was a problem checking your storage. Please log in again and try recording.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"We couldn’t verify your storage quota. Please try again in a few seconds or refresh the page.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Try again\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Scene added to multi-recording\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Ready to record a new scene in your video\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Recording reaching 90 minute limit, it will stop soon\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Recording stopped due to 90 minute limit\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Tab recording is disabled on this page. Defaulted to screen recording.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Video recording cancelled\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Link copied to clipboard\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Failed to copy link\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Latest\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Oldest\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"All videos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"No videos found\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Loading videos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Go to dashboard\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Welcome to Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"No ads. No tracking. No limits.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"  Just hit record.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Use it free — no sign up needed!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Want to do more with your videos?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Cloud storage, share videos with a link\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combine multiple clips into one story\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Smart zooms on click (or add your own!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Add templates, captions, layouts...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Custom backgrounds and mockups\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Unlock extra features\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Support development by a solo indie maker!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Account settings\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Support\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Your Pro subscription is inactive\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Your Screenity Pro subscription is inactive.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Please reactivate to resume access.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Your videos and data will be permanently deleted on \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Manage your subscription\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Want to keep using the free version?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Log out and downgrade\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"You've been logged out\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"To keep your recordings synced to your account, and access premium features, you’ll need to log back in.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Log back in\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"You can keep using the extension without an account - but your recordings won’t be saved and can’t be recovered later.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continue without login\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Instant recording mode\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Instant download, but camera and layout won’t be editable later.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Instant recording mode\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"This records everything into one video for instant download and sharing. You won’t be able to change the camera layout afterward, but other edits are still possible.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Got it\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Tab recording is disabled on this page\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Adding to: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Finish\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Want to do more with your recordings?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Sign in to save your videos to the cloud, share with a link, and access advanced editing features.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Sign in to unlock paid features\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Support development by a solo indie maker\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Unlock more features\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Sign in to share (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Unlock cloud sharing, multi-scene editing, auto zooms, captions & more\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity will always be free, open-source, and ad-free. Pro helps cover cloud & infra costs, & supports development by a solo indie dev! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Don't show again\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Try it\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Multi-recording mode\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Record multiple scenes, like your screen, camera, or both, one after another. This is great for doing multiple takes, switching views, or breaking your recording into parts. When you’re done, click Finish to open the editor with all your scenes combined in one project.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Unlock Pro to get started\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Please subscribe to access Pro features.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Upgrade to Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Enjoying Screenity locally? Help support future development!\",\n    \"description\": \"Self-host banner title in the editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity is built by a solo indie dev. Self-hosting is free, but if you find it useful, consider supporting with Pro ❤️\",\n    \"description\": \"Self-host banner description in the editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Welcome back to Screenity\",\n    \"description\": \"Title for the welcome back popup shown to returning users\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ New: Need more than just recording?\",\n    \"description\": \"Title for the cloud storage feature in the welcome back popup\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro is an optional new platform for saving videos to the cloud, sharing with a link, and unlocking advanced editing tools, if and when you need them.\",\n    \"description\": \"Description for the cloud storage feature in the welcome back popup\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Learn more about Screenity Pro\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome back popup\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Welcome to Screenity Pro\",\n    \"description\": \"Welcome to Pro modal title\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"You're all set to start recording! A couple things to keep in mind:\\n\\n- The camera bubble may disappear while recording, this is normal. It's captured separately in the background so you can position it however you like later.\\n- Zooms are only auto-created on clicks within Chrome tabs, it's a browser limitation. You can always add more zooms manually after recording.\\n- For quick recordings with instant downloads, try Instant Mode. Just note that editing options are limited: no backgrounds, layouts, or advanced tweaks.\",\n    \"description\": \"Welcome to Pro modal description\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Got it\",\n    \"description\": \"Welcome to Pro modal action button\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 failed — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Welcome to the Screenity Pro extension\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"A few quick tips before you record.<br/>Auto-zooms only work for clicks inside <strong>Chrome tabs</strong>. You can still add zooms manually later.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Recording toolbar & effects\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Use this toolbar for drawing, cursor effects, blur, and recording controls.<br/><br/><strong>This toolbar is included in the video</strong> unless you hide it.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Camera is captured separately\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Your camera may <strong>hide or move to PiP</strong> while recording, that's normal.<br/><br/>It's captured separately so you can position it later.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Instant mode\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Best for quick sharing with <strong>unlimited downloads</strong>.<br/><br/>Advanced layouts/editor options aren't available in this mode.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Got it\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Next\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Back\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Learn more\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Reset onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Low disk space. Saving your recording now. Everything captured so far is safe.\",\n    \"description\": \"Toast when disk space drops below critical threshold during recording\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Audio edit too long\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"This audio edit isn't supported for clips over 15 minutes. Your original is unchanged. Try trimming first, then re-apply the edit.\",\n    \"description\": \"Alert body explaining the 15-minute limit for in-browser edits\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Edit timed out\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"The edit did not complete in time. Your original recording is unchanged. Try again, or trim the clip first.\",\n    \"description\": \"Alert body for edit timeout\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Screen sharing stopped. Your recording so far is saved. Stop when you're ready.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"There was a processing issue, but your recording is safe. You can download it below.\",\n    \"description\": \"Recovery mode banner description\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Processing your edit. Your original recording is untouched.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Edit couldn't be applied\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Something went wrong during processing. Your original recording is safe. You can try again or download as-is.\",\n    \"description\": \"Alert body for edit failure\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Screen sharing stopped. Saving your recording now. Everything captured so far is safe.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio disconnected. Saving your recording now. The video captured so far is safe.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Your recording is saved. The editor didn't open. Click the Screenity icon to try again.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Saving is taking longer than expected. Your captured data is safe. The editor will open shortly.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/es/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Graba y anota tu pantalla\",\n    \"description\": \"Nombre de la extensión\"\n  },\n  \"extDesc\": {\n    \"message\": \"La mejor grabadora de pantalla gratis para Chrome sin límites. Captura, dibuja, amplía, desenfoca, edita vídeos y mucho más, sin necesidad de registro y totalmente privada.\",\n    \"description\": \"Descripción de la extensión\"\n  },\n  \"recordTab\": {\n    \"message\": \"Grabar\",\n    \"description\": \"Etiqueta de la pestaña Grabar en el menú desplegable\"\n  },\n  \"videosTab\": {\n    \"message\": \"Tus vídeos\",\n    \"description\": \"Etiqueta de la pestaña Tus vídeos en el menú desplegable\"\n  },\n  \"screenType\": {\n    \"message\": \"Pantalla\",\n    \"description\": \"Etiqueta de tipo de grabación de pantalla en el menú desplegable\"\n  },\n  \"tabType\": {\n    \"message\": \"Área\",\n    \"description\": \"Etiqueta de tipo de grabación de área de pestaña en el menú desplegable\"\n  },\n  \"cameraType\": {\n    \"message\": \"Cámara\",\n    \"description\": \"Etiqueta de tipo de grabación de cámara en el menú desplegable\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etiqueta de tipo de grabación de maqueta en el menú desplegable\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Grabación de área personalizada deshabilitada\",\n    \"description\": \"Título del aviso de grabación de área personalizada deshabilitada en versiones de Chrome anteriores a la 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Actualiza Chrome a la version 110+\",\n    \"description\": \"Descripción del aviso de grabación de área personalizada deshabilitada en versiones de Chrome anteriores a la 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Actualizar\",\n    \"description\": \"Acción del aviso de grabación de área personalizada deshabilitada en versiones de Chrome anteriores a la 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Verifica tus permisos\",\n    \"description\": \"Título del modal de advertencia de permisos de cámara/micrófono\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Parece que Screenity no puede acceder a tu cámara o micrófono. Permite el acceso haciendo clic en el icono de la cámara en la barra de direcciones.\",\n    \"description\": \"Descripción del modal de advertencia de permisos de cámara/micrófono\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Descartar\",\n    \"description\": \"Botón de descartar del modal de advertencia de permisos de cámara/micrófono\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Permitir acceso a la cámara\",\n    \"description\": \"Botón de permitir acceso a la cámara\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Permitir acceso al micrófono\",\n    \"description\": \"Botón de permitir acceso al micrófono\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Pulsar para hablar\",\n    \"description\": \"Etiqueta del interruptor de pulsar para hablar\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Establecer un área de grabación\",\n    \"description\": \"Etiqueta del interruptor de área personalizada\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Voltear cámara\",\n    \"description\": \"Etiqueta del interruptor de voltear cámara\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efectos de fondo\",\n    \"description\": \"Etiqueta del interruptor de efectos de fondo\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Iniciar grabación\",\n    \"description\": \"Etiqueta del botón de grabación\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Selecciona una cámara para grabar\",\n    \"description\": \"Etiqueta del botón de grabación cuando está en modo de cámara y no se ha seleccionado ninguna cámara\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Iniciando grabación...\",\n    \"description\": \"Etiqueta del botón de grabación en progreso\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Mostrar más opciones\",\n    \"description\": \"Etiqueta de mostrar más opciones\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Incluir audio del sistema\",\n    \"description\": \"Etiqueta de audio del sistema/tabulador\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Ocultar barra de herramientas\",\n    \"description\": \"Etiqueta de ocultar barra de herramientas\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Cuenta atrás\",\n    \"description\": \"Etiqueta de cuenta atrás\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Establecer límite de tiempo\",\n    \"description\": \"Etiqueta de alarma\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Blur\",\n    \"description\": \"Etiqueta de fondo de desenfoque\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Ninguno\",\n    \"description\": \"Ningún dispositivo seleccionado\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Sin cámara\",\n    \"description\": \"Ninguna cámara seleccionada\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Sin micrófono\",\n    \"description\": \"Ningún micrófono seleccionado\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Seleccionar una fuente\",\n    \"description\": \"Marcador de posición de selección de fuente\"\n  },\n  \"offLabel\": {\n    \"message\": \"Apagado\",\n    \"description\": \"Etiqueta de apagar en el menú desplegable\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Ancho\",\n    \"description\": \"Etiqueta de ancho de región\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Alto\",\n    \"description\": \"Etiqueta de alto de región\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Haz clic para colocar la imagen\",\n    \"description\": \"Mensaje emergente al agregar una nueva imagen\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Finalizar grabación\",\n    \"description\": \"Consejo para finalizar la grabación\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Reiniciar grabación\",\n    \"description\": \"Consejo para reiniciar la grabación\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Pausar grabación\",\n    \"description\": \"Consejo para pausar la grabación\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Reanudar grabación\",\n    \"description\": \"Consejo para reanudar la grabación\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Cancelar grabación\",\n    \"description\": \"Consejo para cancelar la grabación\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Herramientas de dibujo\",\n    \"description\": \"Consejo para alternar herramientas de dibujo\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Herramienta de desenfoque\",\n    \"description\": \"Consejo para alternar herramientas de desenfoque\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Opciones de cursor\",\n    \"description\": \"Consejo para alternar opciones de cursor\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Desactivar cámara\",\n    \"description\": \"Consejo para desactivar la cámara\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Activar cámara\",\n    \"description\": \"Consejo para activar la cámara\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Sin permisos de cámara\",\n    \"description\": \"Consejo para sin permisos de cámara\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Desactivar micrófono\",\n    \"description\": \"Consejo para desactivar el micrófono\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Activar micrófono\",\n    \"description\": \"Consejo para activar el micrófono\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Sin permisos de micrófono\",\n    \"description\": \"Consejo para sin permisos de micrófono\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Herramienta de selecciones\",\n    \"description\": \"Consejo para seleccionar herramienta\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Lápiz\",\n    \"description\": \"Consejo para herramienta de bolígrafo\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Resaltador\",\n    \"description\": \"Consejo para herramienta de resaltador\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Borrador\",\n    \"description\": \"Consejo para herramienta de borrador\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Texto\",\n    \"description\": \"Consejo para herramienta de texto\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Formas\",\n    \"description\": \"Consejo para herramienta de formas\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Flecha\",\n    \"description\": \"Consejo para herramienta de flecha\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Imágenes\",\n    \"description\": \"Consejo para herramienta de imagen\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Deshacer\",\n    \"description\": \"Consejo para deshacer\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Rehacer\",\n    \"description\": \"Consejo para rehacer\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Borrar lienzo\",\n    \"description\": \"Consejo para borrar lienzo\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Más colores\",\n    \"description\": \"Consejo para más colores\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Trazo grueso\",\n    \"description\": \"Consejo para trazo grueso\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Trazo medio\",\n    \"description\": \"Consejo para trazo medio\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Trazo fino\",\n    \"description\": \"Consejo para trazo fino\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Modo Picture-in-Picture (PIP)\",\n    \"description\": \"Consejo para alternar el modo imagen en imagen\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Alternar relleno\",\n    \"description\": \"Consejo para alternar relleno\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Modo de dibujo\",\n    \"description\": \"Mensaje emergente de modo de dibujo\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Modo de desenfoque\",\n    \"description\": \"Mensaje emergente de modo de desenfoque\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Borrar todos los elementos desenfocados\",\n    \"description\": \"Consejo para borrar todos los elementos desenfocados\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Resaltar clics\",\n    \"description\": \"Consejo para resaltar clics\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Resaltar cursor\",\n    \"description\": \"Consejo para resaltar cursor\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Foco en cursor\",\n    \"description\": \"Consejo para foco en cursor\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"No volver a mostrar\",\n    \"description\": \"Botón de descartar del modal de advertencia de permisos\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"¿Seguro que quieres reiniciar la grabación?\",\n    \"description\": \"Título del modal de reinicio de grabación\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Se perderá todo tu progreso.\",\n    \"description\": \"Descripción del modal de reinicio de grabación\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Reiniciar grabación\",\n    \"description\": \"Botón de reinicio de grabación en el modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Reanudar grabación\",\n    \"description\": \"Botón de reanudar grabación en el modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"¿Seguro que quieres descartar la grabación?\",\n    \"description\": \"Título del modal de descarte de grabación\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Se perderá todo tu progreso.\",\n    \"description\": \"Descripción del modal de descarte de grabación\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Descartar grabación\",\n    \"description\": \"Botón de descarte de grabación en el modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Reanudar grabación\",\n    \"description\": \"Botón de reanudar grabación en el modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Elige qué quieres grabar\",\n    \"description\": \"Título en la página de grabación\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Grabando...\",\n    \"description\": \"Título en la página de grabación mientras se está grabando\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Mantén esta pestaña abierta. Se cerrará una vez que se haya guardado tu grabación.\",\n    \"description\": \"Descripción en la página de grabación\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparando la grabación...\",\n    \"description\": \"Título en la página de pruebas mientras se graba/guarda\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Deja esta pestaña abierta. Se actualizará con tu grabación cuando esté lista.\",\n    \"description\": \"Descripción en la página de pruebas mientras se graba/guarda\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Título en la navegación de la página del editor de pruebas\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Botón de cancelar en la página del editor de pruebas\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Volver al original\",\n    \"description\": \"Botón de revertir en la página del editor de pruebas\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Restablecer\",\n    \"description\": \"Botón de restablecer en la página del editor de pruebas\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Guardar cambios\",\n    \"description\": \"Botón de guardar en la página del editor de pruebas\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Guardando...\",\n    \"description\": \"Botón de guardar en la página del editor de pruebas mientras se guarda\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Arrastra tu archivo de audio\",\n    \"description\": \"Arrastra y suelta archivo de audio en la página del editor de pruebas\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"O haz clic para buscar\",\n    \"description\": \"O haz clic para buscar archivo de audio en la página del editor de pruebas\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Configuración\",\n    \"description\": \"Título de configuración de audio en la página del editor de pruebas\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volumen\",\n    \"description\": \"Etiqueta de volumen de audio en la página del editor de pruebas\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aplicar\",\n    \"description\": \"Botón de actualizar audio en la página del editor de pruebas\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Recortar\",\n    \"description\": \"Título de recorte en la página del editor de pruebas\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Ancho\",\n    \"description\": \"Etiqueta de ancho para propiedades\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Alto\",\n    \"description\": \"Etiqueta de alto para propiedades\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Izquierda\",\n    \"description\": \"Etiqueta de izquierda para propiedades\"\n  },\n  \"topLabel\": {\n    \"message\": \"Arriba\",\n    \"description\": \"Etiqueta de arriba para propiedades\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Arrastra las asas y usa los botones a la izquierda para editar la sección seleccionada.\",\n    \"description\": \"Información sobre recorte en el editor de pruebas\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Recortar vídeo\",\n    \"description\": \"Botón de recortar en el editor de pruebas\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Recortando...\",\n    \"description\": \"Botón de recortar en el editor de pruebas mientras se recorta\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Quitar sección\",\n    \"description\": \"Botón de cortar en el editor de pruebas\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Quitando...\",\n    \"description\": \"Botón de cortar en el editor de pruebas mientras se corta\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Silenciar audio\",\n    \"description\": \"Botón de silenciar en el editor de pruebas\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Silenciando...\",\n    \"description\": \"Botón de silenciar en el editor de pruebas mientras se silencia\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Aprende más.\",\n    \"description\": \"Aprende más con un punto\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Deshacer\",\n    \"description\": \"Etiqueta de deshacer\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Rehacer\",\n    \"description\": \"Etiqueta de rehacer\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Deja una reseña\",\n    \"description\": \"Botón de dejar una reseña\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Mantente informado\",\n    \"description\": \"Botón de seguir para actualizaciones\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Sin conexión\",\n    \"description\": \"Etiqueta de sin conexión\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Algunas funciones no están disponibles \",\n    \"description\": \"Descripción de la etiqueta de sin conexión\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Intenta de nuevo\",\n    \"description\": \"Botón de intentar de nuevo de la etiqueta de sin conexión\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome necesita actualizarse\",\n    \"description\": \"Etiqueta de actualizar Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Actualiza para acceder más funciones\",\n    \"description\": \"Descripción de la etiqueta de actualizar Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Aprende más\",\n    \"description\": \"Botón de aprender más\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video demasiado largo para procesar en tu dispositivo\",\n    \"description\": \"Etiqueta de límite de más de 5 minutos en el editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"La edición y descarga en formato MP4 no están disponibles\",\n    \"description\": \"Descripción de límite de más de 5 minutos en el editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"El vídeo se está procesando...\",\n    \"description\": \"Etiqueta de procesamiento de vídeo en el editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Subscríbete para editar vídeos más rápido\",\n    \"description\": \"Descripción del procesamiento de vídeo en el editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Editar\",\n    \"description\": \"Título de edición en la página del editor de pruebas\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Sin conexión\",\n    \"description\": \"Etiqueta de sin conexión\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"No está disponible\",\n    \"description\": \"Etiqueta de no disponible\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Editar vídeo\",\n    \"description\": \"Etiqueta de botón de recorte\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Corta o silencia el vídeo\",\n    \"description\": \"Etiqueta de recorte\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Preparando...\",\n    \"description\": \"Etiqueta de preparación\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Recortar\",\n    \"description\": \"Etiqueta de botón de recorte\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Recorta y redimensiona el vídeo\",\n    \"description\": \"Etiqueta de recorte\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Añadir audio\",\n    \"description\": \"Etiqueta de botón de añadir audio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Sube tu propio audio para añadir al vídeo\",\n    \"description\": \"Etiqueta de añadir audio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Integraciones\",\n    \"description\": \"Título de guardar en la página del editor de pruebas\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Cerrar sesión en Google Drive\",\n    \"description\": \"Etiqueta de cerrar sesión en Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Guardando...\",\n    \"description\": \"Etiqueta de guardado en Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Guardar en Drive\",\n    \"description\": \"Botón de guardar en Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Guarda la versión actual en Google Drive\",\n    \"description\": \"Etiqueta de guardar en Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Inicia sesión y guarda en Drive\",\n    \"description\": \"Etiqueta de iniciar sesión en Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exportar\",\n    \"description\": \"Título de exportación en la página del editor de pruebas\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Descargando...\",\n    \"description\": \"Etiqueta de descarga\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Descargar como .webm\",\n    \"description\": \"Botón de descargar WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Exporta un vídeo .webm\",\n    \"description\": \"Etiqueta de descargar WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Descargar como .mp4\",\n    \"description\": \"Botón de descargar MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Exporta un vídeo .mp4 (recomendado)\",\n    \"description\": \"Etiqueta de descargar MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Descargar como .gif\",\n    \"description\": \"Botón de descargar GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Exporta un GIF (máximo 30 segundos)\",\n    \"description\": \"Etiqueta de descargar GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"¿Listo para mejorar tus grabaciones?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Comparte en la nube, usa zooms, plantillas... y apoya a una dev indie que cuida tu privacidad ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Ver más sobre Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"¿Ya tienes cuenta? Inicia sesión\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Compartir video\",\n    \"description\": \"Botón de compartir en la página del editor de pruebas\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Reemplazar audio existente\",\n    \"description\": \"Botón de reemplazo de audio en el editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom al cursor\",\n    \"description\": \"Ventana emergente de zoom al punto\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Mantenente en la página al grabar\",\n    \"description\": \"Ventana emergente de permanecer en la página\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Muestra un aviso si el micrófono está desactivado\",\n    \"description\": \"Ventana emergente de recordatorio de micrófono\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Ayuda\",\n    \"description\": \"Botón de ventana emergente de ayuda\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Grabación aturada. Presiona el botón de reproducción para continuar.\",\n    \"description\": \"Título del modal de grabación en pausa\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity no tiene permiso para grabar tu pantalla\",\n    \"description\": \"Título del modal de permisos\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Para habilitar la grabación de pantalla, debes conceder permisos a Screenity.\",\n    \"description\": \"Descripción del modal de permisos\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Habilitar la grabación de pantalla\",\n    \"description\": \"Acción del modal de permisos\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar del modal de permisos\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Tu micrófono está apagado\",\n    \"description\": \"Título del modal de micrófono apagado\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Para incluir audio en tu grabación, deberás activar tu micrófono. ¿Quieres continuar sin audio?\",\n    \"description\": \"Descripción del modal de micrófono apagado\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Sí, continuar\",\n    \"description\": \"Continuar del modal de micrófono apagado\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar del modal de micrófono apagado\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Empieza a utilizar Screenity con tres pasos sencillos:\",\n    \"description\": \"Título de los pasos de configuración\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Haz clic en el \",\n    \"description\": \"Paso 1 de configuración, antes del icono. Necesita un espacio al final.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" icono de extensiones\",\n    \"description\": \"Paso 1 de configuración, después del icono. Necesita un espacio al principio.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Presiona el \",\n    \"description\": \"Paso 2 de configuración, antes del icono. Necesita un espacio al final.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" icono de alfiler\",\n    \"description\": \"Paso 2 de configuración, después del icono. Necesita un espacio al principio.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Haz clic en el \",\n    \"description\": \"Paso 3 de configuración, antes del icono. Necesita un espacio al final.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" icono de Screenity para empezar\",\n    \"description\": \"Paso 3 de configuración, después del icono. Necesita un espacio al principio.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"¡Genial! Ya estás listo\",\n    \"description\": \"Título de configuración completada\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Ahora puedes comenzar a grabar aquí o en cualquier otra pestaña.\",\n    \"description\": \"Descripción de configuración completada\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Haz clic aquí para dibujar\",\n    \"description\": \"Instrucciones para hacer clic aquí\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Relájate. Haz clic en cualquier lugar para detener la cuenta regresiva.\",\n    \"description\": \"Mensaje de cuenta regresiva\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"La edición del vídeo puede ser imprecisa debido a los intervalos entre fotogramas con duraciones cortas.\",\n    \"description\": \"Información sobre el editor siendo demasiado pequeño\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"El video se está procesando, la reproducción puede ser imprecisa o interrumpida.\",\n    \"description\": \"Banner de procesamiento en el editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Recortar el vídeo puede llevar algún tiempo\",\n    \"description\": \"Título de la información de recorte\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Subscríbete para editar vídeos más rápido\",\n    \"description\": \"Descripción de la información de recorte\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Micrófono activado\",\n    \"description\": \"Título del aviso de micrófono habilitado\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Micrófono desactivado\",\n    \"description\": \"Título del aviso de micrófono deshabilitado\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Ocultar las notificaciones de la interfaz\",\n    \"description\": \"Botón para ocultar las alertas de la interfaz\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"No mostrar de nuevo\",\n    \"description\": \"Botón de no mostrar de nuevo\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Oculta los controles cuando no los uses\",\n    \"description\": \"Botón para mostrar la barra de herramientas solo al pasar el cursor\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Detener grabación\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"¡Bienvenido al nuevo Screenity!\",\n    \"description\": \"Título del anuncio de actualización para usuarios existentes\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Hemos actualizado el diseño y desarrollado nuevas funcionalidades, pero no te preocupes, sigue siendo la misma extensión gratuita, privada y de código abierto.\",\n    \"description\": \"Descripción del anuncio de actualización para usuarios existentes\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Obtén más información sobre la actualización.\",\n    \"description\": \"Enlace 'Obtener más información' del anuncio de actualización para usuarios existentes\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Empezar\",\n    \"description\": \"Botón del anuncio de actualización para usuarios existentes\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Error al iniciar la grabación\",\n    \"description\": \"Título del modal de error de transmisión\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Hubo un error al iniciar la grabación. Reinicia tu navegador e inténtalo de nuevo. Si el problema persiste, por favor, contáctanos en support@screenity.io.\",\n    \"description\": \"Descripción del modal de error de transmisión\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Mejor calidad\",\n    \"description\": \"Etiqueta de alternar la mejor calidad en el popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restablecer última grabación\",\n    \"description\": \"Botón para restaurar la última grabación en el popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"¿Tienes problemas?\",\n    \"description\": \"Botón de problemas en el editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"¿No puedes ver tu grabación?\",\n    \"description\": \"Título del modal de problemas en el editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Si has completado tu grabación y te encuentras en esta página sin ver tu video, puedes intentar descargar los datos de video sin procesar si están disponibles. También puedes ponerte en contacto y enviar un informe de error.\",\n    \"description\": \"Descripción del modal de problemas en el editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Descargar datos de video sin procesar\",\n    \"description\": \"Botón del modal de problemas en el editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Informar un error\",\n    \"description\": \"Segundo botón del modal de problemas en el editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"No se encontró ninguna grabación, lo siento :(\",\n    \"description\": \"Alerta de no se encontró ninguna grabación en el editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Límite de memoria alcanzado\",\n    \"description\": \"Título de límite de memoria alcanzado\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Tu dispositivo se ha quedado sin memoria para grabar, por lo que la grabación se ha detenido automáticamente. Si deseas grabar durante más tiempo, puedes intentar reducir la calidad del vídeo o liberar espacio en tu dispositivo.\",\n    \"description\": \"Descripción de límite de memoria alcanzado\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Entendido\",\n    \"description\": \"Botón de entendido\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Espacio insuficiente\",\n    \"description\": \"Título de espacio insuficiente\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity necesita al menos 1 GB de espacio libre para grabar. Por favor, libera espacio y vuelve a intentarlo.\",\n    \"description\": \"Descripción de espacio insuficiente\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Eliminar grabaciones anteriores\",\n    \"description\": \"Botón de eliminar grabaciones anteriores\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avanzado\",\n    \"description\": \"Sección avanzada en la página del editor en el entorno seguro\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Descargar archivo de video sin procesar\",\n    \"description\": \"Botón de descarga de grabación sin procesar\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exporta los datos de grabación originales\",\n    \"description\": \"Etiqueta de descarga de grabación sin procesar\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Obtener ayuda con tu grabación\",\n    \"description\": \"Botón de solución de problemas\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recupera tu video y resuelve otros problemas\",\n    \"description\": \"Etiqueta de solución de problemas\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Descargar archivo de video sin procesar\",\n    \"description\": \"Título del modal de grabación sin procesar\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"El archivo de video sin procesar es el video original grabado por Screenity. Este video no ha sido procesado ni editado, por lo que puede ser muy grande y es posible que no se pueda reproducir en todos los dispositivos. Si tienes problemas con el video procesado, puedes usar este archivo para recuperar tu video.\",\n    \"description\": \"Descripción del modal de grabación sin procesar\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Descargar\",\n    \"description\": \"Botón del modal de grabación sin procesar\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Descargar información del sistema y datos de video para enviar y solucionar problemas\",\n    \"description\": \"Título del modal de solución de problemas\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Después de descargar, envía el archivo a support@screenity.io con cualquier información relevante. Utilizaremos estos datos para ayudarte a solucionar cualquier problema que tengas con la extensión y, si es posible, recuperar tu video.\",\n    \"description\": \"Descripción del modal de solución de problemas\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Descargar\",\n    \"description\": \"Botón del modal de solución de problemas\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"El video es demasiado largo para procesarse en tu dispositivo\",\n    \"description\": \"Título del modal de límite de más de 5 minutos\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Dado que Screenity es completamente privado y se ejecuta localmente, está limitado por el hardware de tu dispositivo. Procesar un video de esta longitud llevaría demasiado tiempo y sería muy intensivo en recursos. No te preocupes, aún puedes descargar el archivo WEBM o la grabación sin procesar, pero la edición y la exportación a MP4 no están disponibles. Dicho esto, aún puedes intentar procesar el video de todos modos si comprendes los riesgos.\",\n    \"description\": \"Descripción del modal de límite de más de 5 minutos\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Entiendo el riesgo, intentalo de todos modos\",\n    \"description\": \"Botón del modal de límite de más de 5 minutos\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Obtén más información sobre el procesamiento de videos largos.\",\n    \"description\": \"Enlace 'Obtener más información' del modal de límite de más de 5 minutos con un punto\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Modo de recuperación\",\n    \"description\": \"Título del modo de recuperación\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Descargar datos para solucionar problemas\",\n    \"description\": \"Opción de descarga para solución de problemas\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centro de ayuda\",\n    \"description\": \"Elemento de navegación de ayuda\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Grabar audio de la página\",\n    \"description\": \"Título de advertencia de audio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Para grabar audio de esta página, debes utilizar la opción de '$tab$'.\",\n    \"description\": \"Descripción de advertencia de audio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Área de la pestaña\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extensión no compatible en la página\",\n    \"description\": \"Título de extensión no compatible en la pestaña\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity no fue compatible en tu pestaña anterior. Aún así, puedes comenzar tu grabación aquí y luego cambiar a la pestaña, pero tu cámara y barra de herramientas no serán visibles.\",\n    \"description\": \"Descripción de extensión no compatible en la pestaña\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configurar una copia de seguridad local para tus grabaciones de Screenity\",\n    \"description\": \"Título de copias de seguridad\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Mantén una copia de todas las grabaciones que hagas. Si no configuras copias de seguridad, tus grabaciones solo se guardarán cuando decidas descargarlas.\",\n    \"description\": \"Descripción de copias de seguridad\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Es posible que necesites crear una nueva carpeta primero si estás intentando guardar en Descargas u otras carpetas del sistema.\",\n    \"description\": \"Descripción de copias de seguridad\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Seleccionar una carpeta\",\n    \"description\": \"Botón Seleccionar carpeta\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Ahora no\",\n    \"description\": \"Botón Ahora no\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Tus grabaciones se están sincronizadas localmente\",\n    \"description\": \"Título de copias de seguridad activadas\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Para evitar solicitar permisos cada vez que grabas, te recomendamos dejar esta pestaña abierta.\",\n    \"description\": \"Descripción de copias de seguridad activadas\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Cerrar pestaña de todos modos\",\n    \"description\": \"Botón Cerrar pestaña de todos modos\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Detener las copias de seguridad de todas las grabaciones\",\n    \"description\": \"Botón Detener copias de seguridad\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Por favor, vuelve a confirmar el acceso a tu carpeta de copias de seguridad local de Screenity\",\n    \"description\": \"Título de confirmación de copias de seguridad\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Lo lamentamos, pero necesitamos tu permiso una vez más para seguir haciendo copias locales de tus grabaciones. Desafortunadamente, necesitaremos preguntar cada vez que cierres esta pestaña, lo que puede suceder si cierras el navegador o reinicias tu computadora.\",\n    \"description\": \"Descripción de confirmación de copias de seguridad\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Permitir a Screenity seguir respaldando grabaciones\",\n    \"description\": \"Botón Permitir en la confirmación de copias de seguridad\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Hacer copias de seguridad de las grabaciones\",\n    \"description\": \"Etiqueta de alternar copias de seguridad\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permiso no otorgado para hacer una copia de seguridad la grabación\",\n    \"description\": \"Título de falla de permiso de respaldo\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"No seleccionaste una carpeta para hacer una copia local de tus grabaciones. Dependiendo de la versión de tu navegador o sistema operativo, es posible que se te solicite seleccionar la carpeta cada vez que comiences a grabar. Puedes intentarlo de nuevo o desactivar las copias de seguridad por completo si prefieres no dar permisos repetidamente.\",\n    \"description\": \"Descripción de falla de permiso de respaldo\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Grabar audio de la computadora\",\n    \"description\": \"Título de advertencia de audio para macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"En macOS, solo puedes grabar audio de pestañas. Selecciona la opción 'Pestaña de Chrome' y asegúrate de que 'Compartir también el audio de la pestaña' esté habilitado.\",\n    \"description\": \"Descripción de advertencia de audio para macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Grabar audio de la computadora\",\n    \"description\": \"Título de advertencia de audio para otros sistemas\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Asegúrate de activar la opción 'Compartir audio del sistema' si deseas grabar audio de la computadora. Si no ves esta opción, intenta actualizar Chrome o cambia a la opción de grabación de pestañas.\",\n    \"description\": \"Descripción de advertencia de audio para otros sistemas\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Resolución máxima\",\n    \"description\": \"Etiqueta de resolución máxima en el menú de configuración\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"FPS máximos\",\n    \"description\": \"Etiqueta de FPS máximos en el menú de configuración\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"No hay suficiente RAM\",\n    \"description\": \"Etiqueta de 'No hay suficiente RAM' en el menú de configuración\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Redimensionar ventana\",\n    \"description\": \"Etiqueta de redimensionar ventana en el menú de configuración\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"La pantalla es demasiado pequeña para esta resolución\",\n    \"description\": \"Consejo para pantallas demasiado pequeñas en el menú de configuración\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"No es posible grabar en esta resolución en su dispositivo\",\n    \"description\": \"Consejo para resolución máxima en el menú de configuración\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Revisar permisos\",\n    \"description\": \"Botón de revisar permisos\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Añadir otra grabación\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Preparando todo...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Espera un momento, estamos preparando tu grabación.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barra de herramientas oculta. Reactívala desde las opciones.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Inicia sesión o regístrate\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Cerrar sesión\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Sesión cerrada\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Has alcanzado el límite de almacenamiento\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Has usado los 100 GB de almacenamiento. Elimina vídeos o escenas antiguas para seguir grabando.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gestionar almacenamiento\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Cerrar\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"No se pudo comprobar el almacenamiento\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Hubo un problema al comprobar el almacenamiento. Inicia sesión de nuevo e intenta grabar otra vez.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"No se pudo verificar tu cuota de almacenamiento. Vuelve a intentarlo en unos segundos o actualiza la página.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Volver a intentar\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Escena añadida a la grabación Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Listo para grabar una nueva escena en tu vídeo\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"La grabación se acerca al límite de 90 minutos, se detendrá pronto\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Grabación detenida por alcanzar el límite de 90 minutos\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"La grabación de pestañas está desactivada en esta página. Se ha activado la grabación de pantalla por defecto.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Grabación del vídeo cancelada\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Enlace copiado al portapapeles\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"No se pudo copiar el enlace\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Más recientes\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Más antiguos\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Todos los vídeos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"No se han encontrado vídeos\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Cargando vídeos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Ir al panel de control\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Bienvenida a Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Sin anuncios. Sin seguimiento. Sin límites.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Graba, simplemente.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Úsalo gratis — ¡sin registro!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"¿Quieres hacer más con tus vídeos?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Almacenamiento en la nube, comparte con enlace\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combina varios clips como capítulos en una historia\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zooms inteligentes con clic (¡o añade los tuyos!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Añade plantillas, subtítulos, mockups...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Fondos y disposición de cámara personalizados\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Desbloquea funciones extra\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ ¡Apoya el desarrollo independiente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Configuración de cuenta\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Soporte\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Suscripción Pro inactiva\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Tu suscripción a Screenity Pro está inactiva.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Reactívala para volver a acceder.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Tus vídeos y datos se eliminarán definitivamente el \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gestionar suscripción\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"¿Quieres seguir usando la versión gratuita?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Cerrar sesión y cambiar a versión gratuita\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Sesión cerrada\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Para mantener tus grabaciones sincronizadas y acceder a funciones Pro, inicia sesión de nuevo.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Iniciar sesión de nuevo\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Puedes seguir usando la extensión sin cuenta, pero las grabaciones no se guardarán ni se podrán recuperar más adelante.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continuar sin iniciar sesión\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Modo de grabación instantánea\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Descarga inmediata, pero no se podrá editar la disposición de la cámara una vez empezada la grabación.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Modo de grabación instantánea\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Este modo graba todo en un solo vídeo para descargar y compartir al instante. No podrás modificar la disposición de la cámara, pero sí hacer otras ediciones.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Entendido\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"La grabación de pestañas está desactivada en esta página\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Añadiendo a: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Finalizar\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"¿Quieres hacer más con tus grabaciones?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Inicia sesión para guardar los vídeos en la nube, compartir con enlace y acceder a funciones avanzadas de edición.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Inicia sesión para desbloquear funciones Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"¡Apoya a la desarrolladora independiente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Desbloquea más funciones\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Inicia sesión para compartir (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Desbloquea almacenamiento en la nube, edición multi-escena, zooms automáticos, subtítulos y más\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity siempre será gratuito, de código abierto y sin anuncios. Pro ayuda a cubrir los costes de nube e infraestructura, y apoya a una desarrolladora independiente ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"No mostrar más\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Pruébalo\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Modo Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Graba varias escenas, como tu pantalla, cámara o ambas, una tras otra. Es ideal para hacer varias tomas, cambiar de vista o dividir tu grabación en partes. Al terminar, haz clic en Finalizar para abrir el editor con todas tus escenas combinadas en un solo video.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Activa Pro para empezar\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Suscríbete para acceder a las funciones Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Suscríbete a Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"¿Disfrutas de Screenity localmente? ¡Apoya su desarrollo futuro!\",\n    \"description\": \"Título del banner de autoalojamiento en el editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity está creada por una desarrolladora indie. Autoalojarlo es gratis, pero si te resulta útil, considera apoyarla con Pro ❤️\",\n    \"description\": \"Descripción del banner de autoalojamiento en el editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Bienvenido de nuevo a Screenity\",\n    \"description\": \"Título para el popup de bienvenida mostrado a los usuarios que regresan\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Novedad: Lleva tus grabaciones al siguiente nivel\",\n    \"description\": \"Título para la función de almacenamiento en la nube en el popup de bienvenida\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro es una nueva plataforma opcional para guardar videos en la nube, compartirlos con un enlace y desbloquear herramientas avanzadas de edición, si y cuando las necesites.\",\n    \"description\": \"Descripción de la función de almacenamiento en la nube en el popup de bienvenida\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Conoce más sobre Screenity Pro\",\n    \"description\": \"Botón de llamada a la acción para la función de almacenamiento en la nube en el popup de bienvenida\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Bienvenido a Screenity Pro\",\n    \"description\": \"Título del modal de bienvenida a Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"¡Todo listo para empezar a grabar! Ten en cuenta lo siguiente:\\n\\n- La burbuja de la cámara puede desaparecer durante la grabación; es normal. Se captura por separado en segundo plano para que puedas colocarla donde quieras más tarde.\\n- Los zooms solo se crean automáticamente al hacer clic dentro de pestañas de Chrome, por una limitación del navegador. Siempre puedes añadir más zooms manualmente después de grabar.\\n- Para grabaciones rápidas con descarga instantánea, prueba el Modo Instantáneo. Solo recuerda que las opciones de edición son limitadas: sin fondos, diseños ni ajustes avanzados.\",\n    \"description\": \"Descripción del modal de bienvenida a Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Entendido\",\n    \"description\": \"Botón de acción del modal de bienvenida a Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 falló — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Bienvenido a la extensión Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Unos consejos rápidos antes de grabar.<br/>Los zooms automáticos solo funcionan con clics dentro de <strong>pestañas de Chrome</strong>. Siempre puedes añadir más zooms manualmente después.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barra de grabación y efectos\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Usa esta barra para dibujar, aplicar efectos del cursor, desenfocar y controlar la grabación.<br/><br/><strong>Esta barra se verá en el video</strong> si no la ocultas.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"La cámara se captura por separado\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Tu cámara puede <strong>ocultarse o pasar a PiP</strong> durante la grabación; es normal.<br/><br/>Se captura por separado para que puedas colocarla después donde quieras.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Modo instantáneo\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Ideal para compartir rápido con <strong>descargas ilimitadas</strong>.<br/><br/>En este modo no están disponibles los diseños ni opciones avanzadas de edición.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Entendido\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Siguiente\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Atrás\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Más información\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Reiniciar onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Poco espacio en disco. Guardando tu grabación. Todo lo capturado hasta ahora está seguro.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Grabación larga\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Tu grabación está completa y segura. Descárgala como WebM abajo.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"No disponible para grabaciones largas\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"No disponible en modo de recuperación\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Edición de audio demasiado larga\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Esta edición de audio no es compatible con clips de más de 15 minutos. Tu original no se ha modificado. Intenta recortar el clip primero.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Tiempo de edición agotado\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"La edición no se completó a tiempo. Tu grabación original no se ha modificado. Inténtalo de nuevo o recorta el clip primero.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"La pantalla compartida se detuvo. Tu grabación está guardada. Para cuando quieras.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Hubo un problema de procesamiento, pero tu grabación está segura. Puedes descargarla a continuación.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Procesando tu edición. La grabación original no se ha modificado.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"No se pudo aplicar la edición\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Algo salió mal durante el procesamiento. Tu grabación original está segura. Puedes intentarlo de nuevo o descargarla tal cual.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"La pantalla compartida se detuvo. Guardando tu grabación. Todo lo capturado hasta ahora está seguro.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio desconectado. Guardando tu grabación. El video capturado hasta ahora está seguro.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Tu grabación está guardada. El editor no se abrió. Haz clic en el ícono de Screenity para intentarlo de nuevo.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"El guardado está tardando más de lo esperado. Tus datos están seguros. El editor se abrirá pronto.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copiar info de depuración\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info de depuración copiada\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Obtener ayuda\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"La grabación fue demasiado corta para guardarla\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"El grabador r\\u00e1pido fall\\u00f3\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"La salida del grabador r\\u00e1pido no se pudo validar en este dispositivo.\\nPuedes descargar el archivo de todas formas.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Descargar de todas formas\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/fr/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Capture & Annotation d'écran\",\n    \"description\": \"Nom de l'extension\"\n  },\n  \"extDesc\": {\n    \"message\": \"La meilleure enregistreuse d'écran gratuite pour Chrome, sans limites. Capturez, dessinez, zoomez, floutez, éditez des vidéos et bien plus encore, sans besoin d'inscription et totalement privée.\",\n    \"description\": \"Description de l'extension\"\n  },\n  \"recordTab\": {\n    \"message\": \"Enregistrer\",\n    \"description\": \"Étiquette de l'onglet Enregistrer dans le menu déroulant\"\n  },\n  \"videosTab\": {\n    \"message\": \"Vos vidéos\",\n    \"description\": \"Étiquette de l'onglet Vos vidéos dans le menu déroulant\"\n  },\n  \"screenType\": {\n    \"message\": \"Écran\",\n    \"description\": \"Étiquette du type d'enregistrement d'écran dans le menu déroulant\"\n  },\n  \"tabType\": {\n    \"message\": \"Zone\",\n    \"description\": \"Étiquette du type d'enregistrement de zone d'onglet dans le menu déroulant\"\n  },\n  \"cameraType\": {\n    \"message\": \"Caméra\",\n    \"description\": \"Étiquette du type d'enregistrement de caméra dans le menu déroulant\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Étiquette du type d'enregistrement de maquette dans le menu déroulant\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Enregistrement de zone personnalisée désactivé\",\n    \"description\": \"Titre de l'avis d'enregistrement de zone personnalisée désactivé dans les versions de Chrome antérieures à la 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Mettez à jour Chrome vers la version 110+\",\n    \"description\": \"Description de l'avis d'enregistrement de zone personnalisée désactivé dans les versions de Chrome antérieures à la 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Mettre à jour\",\n    \"description\": \"Action de l'avis d'enregistrement de zone personnalisée désactivé dans les versions de Chrome antérieures à la 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Vérifiez vos autorisations\",\n    \"description\": \"Titre de la fenêtre modale d'avertissement des autorisations de la caméra/du microphone\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Il semble que Screenity ne puisse pas accéder à votre caméra ou à votre microphone. Autorisez l'accès en cliquant sur l'icône de la caméra dans la barre d'adresse.\",\n    \"description\": \"Description de la fenêtre modale d'avertissement des autorisations de la caméra/du microphone\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Rejeter\",\n    \"description\": \"Bouton de rejet de la fenêtre modale d'avertissement des autorisations de la caméra/du microphone\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Autoriser l'accès à la caméra\",\n    \"description\": \"Bouton d'autorisation d'accès à la caméra\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Autoriser l'accès au microphone\",\n    \"description\": \"Bouton d'autorisation d'accès au microphone\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Appuyer pour parler\",\n    \"description\": \"Étiquette du commutateur Appuyer pour parler\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Définir une zone d'enregistrement\",\n    \"description\": \"Étiquette du commutateur de zone personnalisée\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Retourner la caméra\",\n    \"description\": \"Étiquette du commutateur Retourner la caméra\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Effets de fond\",\n    \"description\": \"Étiquette du commutateur Effets de fond\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Démarrer l'enregistrement\",\n    \"description\": \"Étiquette du bouton d'enregistrement\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Sélectionnez une caméra pour enregistrer\",\n    \"description\": \"Étiquette du bouton d'enregistrement en mode caméra lorsque aucune caméra n'est sélectionnée\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Démarrage de l'enregistrement...\",\n    \"description\": \"Étiquette du bouton d'enregistrement en cours\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Afficher plus d'options\",\n    \"description\": \"Étiquette Afficher plus d'options\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Inclure l'audio système\",\n    \"description\": \"Étiquette de l'audio système/onglet\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Masquer la barre d'outils\",\n    \"description\": \"Étiquette Masquer la barre d'outils\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Compte à rebours\",\n    \"description\": \"Étiquette du compte à rebours\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Définir la limite de temps\",\n    \"description\": \"Étiquette de l'alarme\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Flou\",\n    \"description\": \"Étiquette du type de flou de l'arrière-plan\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Aucun\",\n    \"description\": \"Aucun appareil sélectionné\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Pas de caméra\",\n    \"description\": \"Aucune caméra sélectionnée\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Pas de microphone\",\n    \"description\": \"Aucun microphone sélectionné\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Sélectionner une source\",\n    \"description\": \"Marque de sélection de la source\"\n  },\n  \"offLabel\": {\n    \"message\": \"Off\",\n    \"description\": \"Étiquette d'extinction dans le menu déroulant\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Largeur\",\n    \"description\": \"Étiquette de largeur de la région\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Hauteur\",\n    \"description\": \"Étiquette de hauteur de la région\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Cliquez pour placer l'image\",\n    \"description\": \"Message d'alerte lors de l'ajout d'une nouvelle image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Terminer l'enregistrement\",\n    \"description\": \"Conseil pour terminer l'enregistrement\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Redémarrer l'enregistrement\",\n    \"description\": \"Conseil pour redémarrer l'enregistrement\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Mettre en pause l'enregistrement\",\n    \"description\": \"Conseil pour mettre en pause l'enregistrement\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Reprendre l'enregistrement\",\n    \"description\": \"Conseil pour reprendre l'enregistrement\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Annuler l'enregistrement\",\n    \"description\": \"Conseil pour annuler l'enregistrement\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Outils de dessin\",\n    \"description\": \"Conseil pour basculer les outils de dessin\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Outil de flou\",\n    \"description\": \"Conseil pour basculer les outils de flou\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Options de curseur\",\n    \"description\": \"Conseil pour basculer les options de curseur\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Désactiver la caméra\",\n    \"description\": \"Conseil pour désactiver la caméra\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Activer la caméra\",\n    \"description\": \"Conseil pour activer la caméra\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Pas de permissions de caméra\",\n    \"description\": \"Conseil pour aucune permission de caméra\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Désactiver le microphone\",\n    \"description\": \"Conseil pour désactiver le microphone\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Activer le microphone\",\n    \"description\": \"Conseil pour activer le microphone\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Pas de permissions de microphone\",\n    \"description\": \"Conseil pour aucune permission de microphone\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Outil de sélection\",\n    \"description\": \"Conseil pour sélectionner l'outil\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Stylo\",\n    \"description\": \"Conseil pour l'outil de stylo\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Marqueur\",\n    \"description\": \"Conseil pour l'outil de surligneur\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Effaceur\",\n    \"description\": \"Conseil pour l'outil d'effaceur\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Texte\",\n    \"description\": \"Conseil pour l'outil de texte\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Formes\",\n    \"description\": \"Conseil pour l'outil de formes\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Flèche\",\n    \"description\": \"Conseil pour l'outil de flèche\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Images\",\n    \"description\": \"Conseil pour l'outil d'image\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Conseil pour annuler\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Refaire\",\n    \"description\": \"Conseil pour refaire\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Effacer la toile\",\n    \"description\": \"Conseil pour effacer la toile\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Plus de couleurs\",\n    \"description\": \"Conseil pour plus de couleurs\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Trait épais\",\n    \"description\": \"Conseil pour un trait épais\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Trait moyen\",\n    \"description\": \"Conseil pour un trait moyen\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Trait fin\",\n    \"description\": \"Conseil pour un trait fin\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Mode Image dans l'image (PIP)\",\n    \"description\": \"Conseil pour basculer le mode image dans l'image\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Activer le remplissage\",\n    \"description\": \"Conseil pour basculer le remplissage\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Mode de dessin\",\n    \"description\": \"Message d'alerte du mode de dessin\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Mode de flou\",\n    \"description\": \"Message d'alerte du mode de flou\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Effacer tous les éléments flous\",\n    \"description\": \"Conseil pour effacer tous les éléments flous\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Surligner les clics\",\n    \"description\": \"Conseil pour surligner les clics\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Surligner le curseur\",\n    \"description\": \"Conseil pour surligner le curseur\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Projecteur sur le curseur\",\n    \"description\": \"Conseil pour mettre en lumière le curseur\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Ne plus afficher\",\n    \"description\": \"Bouton de ne plus afficher du modal d'avertissement de permissions\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Êtes-vous sûr de vouloir redémarrer l'enregistrement ?\",\n    \"description\": \"Titre du modal de redémarrage d'enregistrement\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Tout votre progrès sera perdu.\",\n    \"description\": \"Description du modal de redémarrage d'enregistrement\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Redémarrer l'enregistrement\",\n    \"description\": \"Bouton de redémarrage d'enregistrement dans le modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Reprendre l'enregistrement\",\n    \"description\": \"Bouton de reprise d'enregistrement dans le modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Êtes-vous sûr de vouloir abandonner l'enregistrement ?\",\n    \"description\": \"Titre du modal d'abandon d'enregistrement\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Tout votre progrès sera perdu.\",\n    \"description\": \"Description du modal d'abandon d'enregistrement\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Abandonner l'enregistrement\",\n    \"description\": \"Bouton d'abandon d'enregistrement dans le modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Reprendre l'enregistrement\",\n    \"description\": \"Bouton de reprise d'enregistrement dans le modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Choisissez ce que vous voulez enregistrer\",\n    \"description\": \"Titre sur la page d'enregistrement\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Enregistrement en cours...\",\n    \"description\": \"Titre sur la page d'enregistrement pendant l'enregistrement en cours\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Laissez cet onglet ouvert. Il se fermera une fois votre enregistrement sauvegardé.\",\n    \"description\": \"Description sur la page d'enregistrement\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Préparation de l'enregistrement...\",\n    \"description\": \"Titre sur la page de test pendant l'enregistrement/sauvegarde\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Laissez cet onglet ouvert. Il se mettra à jour avec votre enregistrement une fois prêt.\",\n    \"description\": \"Description sur la page de test pendant l'enregistrement/sauvegarde\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Éditeur\",\n    \"description\": \"Titre dans la navigation de la page de l'éditeur de test\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Bouton d'annulation sur la page de l'éditeur de test\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Revenir à l'original\",\n    \"description\": \"Bouton de retour à l'original sur la page de l'éditeur de test\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Réinitialiser\",\n    \"description\": \"Bouton de réinitialisation sur la page de l'éditeur de test\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Enregistrer les modifications\",\n    \"description\": \"Bouton d'enregistrement sur la page de l'éditeur de test\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Enregistrement en cours...\",\n    \"description\": \"Bouton d'enregistrement sur la page de l'éditeur de test pendant l'enregistrement en cours\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Faites glisser votre fichier audio ici\",\n    \"description\": \"Faites glisser et déposez un fichier audio sur la page de l'éditeur de test\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Ou cliquez pour parcourir\",\n    \"description\": \"Ou cliquez pour parcourir un fichier audio sur la page de l'éditeur de test\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Paramètres audio\",\n    \"description\": \"Titre des paramètres audio sur la page de l'éditeur de test\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Étiquette de volume audio sur la page de l'éditeur de test\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Appliquer\",\n    \"description\": \"Bouton de mise à jour audio sur la page de l'éditeur de tests\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Recadrer\",\n    \"description\": \"Titre de recadrage sur la page de l'éditeur de tests\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Largeur\",\n    \"description\": \"Étiquette de largeur pour les propriétés\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Hauteur\",\n    \"description\": \"Étiquette de hauteur pour les propriétés\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Gauche\",\n    \"description\": \"Étiquette de gauche pour les propriétés\"\n  },\n  \"topLabel\": {\n    \"message\": \"Haut\",\n    \"description\": \"Étiquette de haut pour les propriétés\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Faites glisser les poignées et utilisez les boutons à gauche pour éditer la section sélectionnée.\",\n    \"description\": \"Informations sur le recadrage dans l'éditeur de tests\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Recadrer la vidéo\",\n    \"description\": \"Bouton de recadrage dans l'éditeur de tests\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"En cours de recadrage...\",\n    \"description\": \"Bouton de recadrage dans l'éditeur de tests pendant le recadrage\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Supprimer la section\",\n    \"description\": \"Bouton de suppression dans l'éditeur de tests\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"En cours de suppression...\",\n    \"description\": \"Bouton de suppression dans l'éditeur de tests pendant la suppression\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Couper le son\",\n    \"description\": \"Bouton de mise en sourdine dans l'éditeur de tests\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"En cours de mise en sourdine...\",\n    \"description\": \"Bouton de mise en sourdine dans l'éditeur de tests pendant la mise en sourdine\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"En savoir plus.\",\n    \"description\": \"En savoir plus avec un point\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Étiquette d'annulation\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Rétablir\",\n    \"description\": \"Étiquette de rétablissement\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Laisser un avis\",\n    \"description\": \"Bouton de laisser un avis\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Restez informé\",\n    \"description\": \"Bouton de suivi pour les mises à jour\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Hors ligne\",\n    \"description\": \"Étiquette hors ligne\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Certaines fonctions ne sont pas disponibles\",\n    \"description\": \"Description de l'étiquette hors ligne\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Réessayer\",\n    \"description\": \"Bouton de réessai de l'étiquette hors ligne\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome a besoin d'une mise à jour\",\n    \"description\": \"Étiquette de mise à jour de Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Mettez à jour pour accéder à plus de fonctionnalités\",\n    \"description\": \"Description de l'étiquette de mise à jour de Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"En savoir plus\",\n    \"description\": \"Bouton d'en savoir plus\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Vidéo trop longue pour être traitée localement\",\n    \"description\": \"Étiquette de limite de plus de 5 minutes dans l'éditeur\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"L'édition et l'exportation au format MP4 ne sont pas disponibles\",\n    \"description\": \"Description de la limite de plus de 5 minutes dans l'éditeur\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"La vidéo est en cours de traitement...\",\n    \"description\": \"Étiquette de traitement vidéo dans l'éditeur\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Abonnez-vous pour éditer les vidéos plus rapidement\",\n    \"description\": \"Description du traitement vidéo dans l'éditeur\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Modifier\",\n    \"description\": \"Titre de modification sur la page de l'éditeur de tests\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Pas de connexion\",\n    \"description\": \"Étiquette pas de connexion\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Non disponible\",\n    \"description\": \"Étiquette non disponible\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Modifier la vidéo\",\n    \"description\": \"Étiquette de bouton de recadrage\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Recadrez ou mettez en sourdine la vidéo\",\n    \"description\": \"Étiquette de recadrage\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Préparation...\",\n    \"description\": \"Étiquette de préparation\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Recadrer\",\n    \"description\": \"Étiquette de bouton de recadrage\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Recadrez et redimensionnez la vidéo\",\n    \"description\": \"Étiquette de recadrage\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Ajouter de l'audio\",\n    \"description\": \"Étiquette de bouton d'ajout d'audio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Téléchargez votre propre audio\",\n    \"description\": \"Étiquette d'ajout d'audio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Intégrations\",\n    \"description\": \"Titre de sauvegarde sur la page de l'éditeur de tests\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Déconnexion de Google Drive\",\n    \"description\": \"Étiquette de déconnexion de Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Enregistrement...\",\n    \"description\": \"Étiquette d'enregistrement sur Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Enregistrer sur Drive\",\n    \"description\": \"Bouton d'enregistrement sur Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Enregistrez la vidéo sur Google Drive\",\n    \"description\": \"Étiquette d'enregistrement sur Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Connectez-vous et enregistrez sur Drive\",\n    \"description\": \"Étiquette de connexion à Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exporter\",\n    \"description\": \"Titre d'exportation sur la page de l'éditeur de tests\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Téléchargement...\",\n    \"description\": \"Étiquette de téléchargement\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Télécharger en tant que .webm\",\n    \"description\": \"Bouton de téléchargement WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Exportez une vidéo .webm\",\n    \"description\": \"Étiquette de téléchargement WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Télécharger en tant que .mp4\",\n    \"description\": \"Bouton de téléchargement MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Exportez une vidéo .mp4 (recommandé)\",\n    \"description\": \"Étiquette de téléchargement MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Télécharger en tant que .gif\",\n    \"description\": \"Bouton de téléchargement GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Exportez un GIF (maximum 30 secondes)\",\n    \"description\": \"Étiquette de téléchargement GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Prêt à améliorer vos enregistrements ?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Partage dans le cloud, zooms, modèles... et soutiens une dev indie qui respecte ta vie privée ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"En savoir plus sur Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Vous avez déjà un compte ? Connexion\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Partager la vidéo\",\n    \"description\": \"Bouton de partage sur la page de l'éditeur de tests\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Remplacer l'audio existant\",\n    \"description\": \"Bouton de remplacement audio dans l'éditeur\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom sur le curseur\",\n    \"description\": \"Fenêtre contextuelle de zoom sur le point\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Restez sur la page lors de l'enregistrement\",\n    \"description\": \"Fenêtre contextuelle de rester sur la page\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Avertissement si le microphone est off\",\n    \"description\": \"Fenêtre contextuelle de rappel de microphone\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Aide\",\n    \"description\": \"Bouton de fenêtre contextuelle d'aide\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Enregistrement en pause. Appuyez sur le bouton de lecture pour continuer.\",\n    \"description\": \"Titre de la boîte de dialogue d'enregistrement en pause\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity n'a pas la permission d'enregistrer votre écran\",\n    \"description\": \"Titre de la boîte de dialogue des autorisations\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Pour activer l'enregistrement de l'écran, vous devez accorder des autorisations à Screenity.\",\n    \"description\": \"Description de la boîte de dialogue des autorisations\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Activer l'enregistrement de l'écran\",\n    \"description\": \"Action de la boîte de dialogue des autorisations\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Annuler la boîte de dialogue des autorisations\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Votre microphone est désactivé\",\n    \"description\": \"Titre de la boîte de dialogue du microphone désactivé\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Pour inclure l'audio dans votre enregistrement, vous devez activer votre microphone. Voulez-vous continuer sans audio ?\",\n    \"description\": \"Description de la boîte de dialogue du microphone désactivé\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Oui, continuer\",\n    \"description\": \"Continuer la boîte de dialogue du microphone désactivé\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Annuler la boîte de dialogue du microphone désactivé\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Commencez à utiliser Screenity en trois étapes simples :\",\n    \"description\": \"Titre des étapes de configuration\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Cliquez sur l'icône \",\n    \"description\": \"Étape 1 de configuration, avant l'icône. Besoin d'un espace à la fin.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" des extensions\",\n    \"description\": \"Étape 1 de configuration, après l'icône. Besoin d'un espace au début.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Appuyez sur l'icône \",\n    \"description\": \"Étape 2 de configuration, avant l'icône. Besoin d'un espace à la fin.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" de broche\",\n    \"description\": \"Étape 2 de configuration, après l'icône. Besoin d'un espace au début.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Cliquez sur l'icône \",\n    \"description\": \"Étape 3 de configuration, avant l'icône. Besoin d'un espace à la fin.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" de Screenity pour commencer\",\n    \"description\": \"Étape 3 de configuration, après l'icône. Besoin d'un espace au début.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Super ! Vous êtes prêt\",\n    \"description\": \"Titre de la configuration terminée\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Vous pouvez maintenant commencer à enregistrer ici ou dans n'importe quel autre onglet.\",\n    \"description\": \"Description de la configuration terminée\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Cliquez ici pour dessiner\",\n    \"description\": \"Instructions pour cliquer ici\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Détendez-vous. Cliquez n'importe où pour arrêter le compte à rebours.\",\n    \"description\": \"Message de compte à rebours\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"L'édition de la vidéo peut être imprécise en raison des intervalles entre les images avec des durées courtes.\",\n    \"description\": \"Information sur l'éditeur trop petit\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"La vidéo est en cours de traitement, la lecture peut être imprécise ou interrompue.\",\n    \"description\": \"Bannière de traitement dans l'éditeur\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Le recadrage de la vidéo peut prendre un certain temps\",\n    \"description\": \"Titre de l'information de recadrage\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Abonnez-vous pour éditer les vidéos plus rapidement\",\n    \"description\": \"Description de l'information de recadrage\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microphone activé\",\n    \"description\": \"Titre de l'alerte microphone activé\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microphone désactivé\",\n    \"description\": \"Titre de l'alerte microphone désactivé\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Masquer les notifications de l'interface\",\n    \"description\": \"Bouton pour masquer les alertes de l'interface\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Ne pas afficher à nouveau\",\n    \"description\": \"Bouton Ne pas afficher à nouveau\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Masquer la barre d'outils lorsque non utilisée\",\n    \"description\": \"Bouton pour afficher la barre d'outils uniquement au survol\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Arrêter l'enregistrement\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Bienvenue dans le nouveau Screenity !\",\n    \"description\": \"Titre de l'annonce de mise à jour pour les utilisateurs existants\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Nous avons mis à jour le design et ajouté de nouvelles fonctionnalités, mais ne vous inquiétez pas, c'est toujours la même extension gratuite, privée et open source que vous connaissez et aimez.\",\n    \"description\": \"Description de l'annonce de mise à jour pour les utilisateurs existants\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"En savoir plus sur la mise à jour.\",\n    \"description\": \"Lien 'En savoir plus' de l'annonce de mise à jour pour les utilisateurs existants\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Commencer\",\n    \"description\": \"Bouton de l'annonce de mise à jour pour les utilisateurs existants\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Erreur lors du démarrage de l'enregistrement\",\n    \"description\": \"Titre du modal d'erreur de flux\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Il semble y avoir une erreur lors du démarrage de l'enregistrement. Redémarrez votre navigateur et réessayez. Si le problème persiste, veuillez nous contacter à l'adresse support@screenity.io.\",\n    \"description\": \"Description du modal d'erreur de flux\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Meilleure qualité\",\n    \"description\": \"Étiquette pour basculer vers la meilleure qualité dans la fenêtre contextuelle\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restaurer l'enregistrement précédent\",\n    \"description\": \"Bouton pour restaurer l'enregistrement précédent dans la fenêtre contextuelle\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Vous rencontrez des problèmes ?\",\n    \"description\": \"Bouton de signalement de problèmes dans l'éditeur\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Vous ne pouvez pas voir votre enregistrement ?\",\n    \"description\": \"Titre de la fenêtre modale de signalement de problèmes dans l'éditeur\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Si vous avez terminé votre enregistrement et que vous vous trouvez sur cette page sans voir votre vidéo, vous pouvez essayer de télécharger les données brutes de la vidéo si elles sont disponibles. Vous pouvez également nous contacter et soumettre un rapport de bug.\",\n    \"description\": \"Description de la fenêtre modale de signalement de problèmes dans l'éditeur\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Télécharger les données brutes de la vidéo\",\n    \"description\": \"Bouton de téléchargement dans la fenêtre modale de signalement de problèmes dans l'éditeur\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Signaler un bug\",\n    \"description\": \"Deuxième bouton dans la fenêtre modale de signalement de problèmes dans l'éditeur\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Aucun enregistrement trouvé, désolé :(\",\n    \"description\": \"Message d'alerte en cas d'absence d'enregistrement dans l'éditeur\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Limite de mémoire atteinte\",\n    \"description\": \"Titre de la limite de mémoire atteinte\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Votre appareil a épuisé sa mémoire disponible pour l'enregistrement, ce qui a entraîné l'arrêt automatique de l'enregistrement. Si vous souhaitez enregistrer plus longtemps, vous pouvez essayer de réduire la qualité de la vidéo ou de libérer de l'espace sur votre appareil.\",\n    \"description\": \"Description de la limite de mémoire atteinte\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Compris\",\n    \"description\": \"Bouton Compris\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Espace insuffisant\",\n    \"description\": \"Titre d'espace insuffisant\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity nécessite au moins 1 Go d'espace libre pour l'enregistrement. Veuillez libérer de l'espace et réessayer.\",\n    \"description\": \"Description d'espace insuffisant\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Effacer les enregistrements précédents\",\n    \"description\": \"Bouton Effacer les enregistrements précédents\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avancé\",\n    \"description\": \"Section avancée dans la page de l'éditeur en mode sécurisé\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Télécharger le fichier vidéo brut\",\n    \"description\": \"Bouton de téléchargement de l'enregistrement brut\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exporter les données d'enregistrement originales\",\n    \"description\": \"Libellé du bouton de téléchargement de l'enregistrement brut\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Obtenir de l'aide pour votre enregistrement\",\n    \"description\": \"Bouton de dépannage\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Récupérez votre vidéo et résolvez d'autres problèmes\",\n    \"description\": \"Libellé de dépannage\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Télécharger le fichier vidéo brut\",\n    \"description\": \"Titre de la fenêtre modale de l'enregistrement brut\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Le fichier vidéo brut est le fichier original enregistré par Screenity. Cette vidéo n'a pas été traitée ni modifiée, elle peut donc être très volumineuse et peut ne pas être lisible sur tous les appareils. Si vous rencontrez des problèmes avec la vidéo traitée, vous pouvez utiliser ce fichier pour récupérer votre vidéo.\",\n    \"description\": \"Description de la fenêtre modale de l'enregistrement brut\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Télécharger\",\n    \"description\": \"Bouton de la fenêtre modale de l'enregistrement brut\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Télécharger les informations système et les données vidéo pour le dépannage\",\n    \"description\": \"Titre de la fenêtre modale de dépannage\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Après le téléchargement, envoyez le fichier à support@screenity.io avec toutes les informations pertinentes. Nous utiliserons ces données pour vous aider à résoudre tout problème que vous pourriez rencontrer avec l'extension, et si possible, récupérer votre vidéo.\",\n    \"description\": \"Description de la fenêtre modale de dépannage\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Télécharger\",\n    \"description\": \"Bouton de la fenêtre modale de dépannage\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"La vidéo est trop longue pour être traitée sur votre appareil\",\n    \"description\": \"Titre de la fenêtre modale de limite de plus de 5 minutes\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Comme Screenity est complètement privé et fonctionne en local, il est limité par le matériel de votre appareil. Le traitement d'une vidéo de cette longueur prendrait trop de temps et serait très intensif en ressources. Ne vous inquiétez pas, vous pouvez toujours télécharger le fichier WEBM ou l'enregistrement brut, mais l'édition et l'exportation en MP4 ne sont pas disponibles. Cela dit, vous pouvez toujours essayer de traiter la vidéo de toute façon, si vous comprenez les risques.\",\n    \"description\": \"Description de la fenêtre modale de limite de plus de 5 minutes\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Je comprends le risque, essayer quand même\",\n    \"description\": \"Bouton de la fenêtre modale de limite de plus de 5 minutes\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"En savoir plus sur le traitement des vidéos longues.\",\n    \"description\": \"Lien 'En savoir plus' de la fenêtre modale de limite de plus de 5 minutes avec un point\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Mode de récupération\",\n    \"description\": \"Titre du mode de récupération\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Télécharger des données pour résoudre les problèmes\",\n    \"description\": \"Option de téléchargement pour résoudre les problèmes\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centre d'aide\",\n    \"description\": \"Élément de navigation pour obtenir de l'aide\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Enregistrement audio de l'ordinateur\",\n    \"description\": \"Titre de l'avertissement audio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Pour enregistrer l'audio de cette page, vous devez basculer vers '$tab$'.\",\n    \"description\": \"Description de l'avertissement audio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Zone de l'onglet\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extension non prise en charge sur l'onglet\",\n    \"description\": \"Titre de l'extension non prise en charge sur l'onglet\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity n'était pas pris en charge dans votre onglet précédent. Vous pouvez toujours commencer votre enregistrement ici et basculer vers l'onglet, mais votre caméra et votre barre d'outils ne seront pas visibles.\",\n    \"description\": \"Description de l'extension non prise en charge sur l'onglet\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configurer une sauvegarde locale de vos enregistrements Screenity\",\n    \"description\": \"Titre des sauvegardes\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Conservez une copie de tous les enregistrements que vous effectuez. Si vous ne configurez pas de sauvegardes, vos enregistrements ne seront enregistrés que lorsque vous choisirez de les télécharger.\",\n    \"description\": \"Description des sauvegardes\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Vous devrez peut-être créer un nouveau dossier d'abord si vous essayez de sauvegarder dans Téléchargements ou d'autres dossiers système.\",\n    \"description\": \"Description des sauvegardes\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Sélectionner un dossier\",\n    \"description\": \"Bouton Sélectionner un dossier\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Pas maintenant\",\n    \"description\": \"Bouton Pas maintenant\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Vos enregistrements sont sauvegardés localement\",\n    \"description\": \"Titre de l'activation des sauvegardes\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Pour éviter de demander des autorisations à chaque enregistrement, nous vous recommandons de laisser cette page ouverte.\",\n    \"description\": \"Description de l'activation des sauvegardes\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Fermer l'onglet de toute façon\",\n    \"description\": \"Bouton Fermer l'onglet de toute façon\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Arrêter toutes les sauvegardes des enregistrements\",\n    \"description\": \"Bouton Arrêter les sauvegardes\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Veuillez confirmer à nouveau l'accès à votre dossier de sauvegarde local Screenity\",\n    \"description\": \"Titre de confirmation des sauvegardes\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Nous sommes désolés, mais nous avons besoin de votre permission une fois de plus pour continuer à effectuer des sauvegardes locales de vos enregistrements. Malheureusement, nous devrons demander à chaque fois que vous fermerez cet onglet, ce qui peut arriver si vous fermez le navigateur ou redémarrez votre ordinateur.\",\n    \"description\": \"Description de la confirmation des sauvegardes\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Autoriser Screenity à continuer à effectuer des sauvegardes des enregistrements\",\n    \"description\": \"Bouton Autoriser dans la confirmation des sauvegardes\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Activer les sauvegardes des enregistrements\",\n    \"description\": \"Libellé de l'activation des sauvegardes\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Autorisation non accordée pour sauvegarder l'enregistrement\",\n    \"description\": \"Titre de l'échec de l'autorisation de sauvegarde\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Vous n'avez pas sélectionné de dossier pour sauvegarder localement vos enregistrements. Selon la version de votre navigateur ou de votre système d'exploitation, il se peut que l'on vous demande de sélectionner le dossier à chaque début d'enregistrement. Vous pouvez réessayer ou désactiver complètement les sauvegardes si vous préférez ne pas donner des autorisations de manière répétée.\",\n    \"description\": \"Description de l'échec de l'autorisation de sauvegarde\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Enregistrement audio de l'ordinateur\",\n    \"description\": \"Titre de l'avertissement audio pour macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"Sous macOS, vous ne pouvez enregistrer que l'audio de l'onglet. Sélectionnez l'option 'Onglet Chrome' et assurez-vous que 'Partager également l'audio de l'onglet' est activé.\",\n    \"description\": \"Description de l'avertissement audio pour macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Enregistrement audio de l'ordinateur\",\n    \"description\": \"Titre de l'avertissement audio pour d'autres systèmes\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Assurez-vous d'activer l'option 'Partager l'audio du système' si vous souhaitez enregistrer l'audio de l'ordinateur. Si vous ne voyez pas cette option, essayez de mettre à jour Chrome ou basculez vers l'enregistrement de l'onglet.\",\n    \"description\": \"Description de l'avertissement audio pour d'autres systèmes\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Résolution maximale\",\n    \"description\": \"Étiquette de résolution maximale dans le menu des paramètres\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"FPS maximum\",\n    \"description\": \"Étiquette de FPS maximum dans le menu des paramètres\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Pas assez de RAM\",\n    \"description\": \"Étiquette 'Pas assez de RAM' dans le menu des paramètres\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Redimensionner la fenêtre\",\n    \"description\": \"Étiquette de redimensionnement de fenêtre dans le menu des paramètres\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"L'écran est trop petit pour cette résolution\",\n    \"description\": \"Info-bulle indiquant que l'écran est trop petit dans le menu des paramètres\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Impossible d'enregistrer à cette résolution sur votre appareil\",\n    \"description\": \"Info-bulle indiquant qu'il est impossible d'enregistrer à cette résolution dans le menu des paramètres\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Réviser les autorisations\",\n    \"description\": \"Bouton de révision des autorisations\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Ajouter un autre enregistrement\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Préparation en cours...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Un instant ! Votre enregistrement est en préparation.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barre d’outils masquée. Réactivez-la depuis les options du popup.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Se connecter ou s’inscrire\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Se déconnecter\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Vous avez été déconnectée\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Limite de stockage atteinte\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Vous avez utilisé les 50 Go de stockage. Supprimez d’anciennes vidéos ou scènes pour continuer à enregistrer.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gérer le stockage\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Fermer\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Impossible de vérifier le stockage\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Un problème est survenu lors de la vérification du stockage. Veuillez vous reconnecter et réessayer.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Impossible de vérifier votre quota de stockage. Réessayez dans quelques secondes ou actualisez la page.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Réessayer\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Scène ajoutée à l’enregistrement Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Prête à enregistrer une nouvelle scène dans votre vidéo\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"L’enregistrement approche la limite de 90 minutes, il s’arrêtera bientôt\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Enregistrement arrêté — limite de 90 minutes atteinte\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"L’enregistrement d’onglet est désactivé sur cette page. L’enregistrement d’écran a été activé par défaut.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Enregistrement de la vidéo annulé\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Lien copié dans le presse-papiers\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Échec de la copie du lien\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Les plus récents\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Les plus anciens\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Toutes les vidéos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Aucune vidéo trouvée\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Chargement des vidéos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Aller au tableau de bord\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Bienvenue sur Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Sans pub. Sans suivi. Sans limites.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Enregistrez, tout simplement.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Essayez gratuitement — sans inscription !\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Envie d’en faire plus avec vos vidéos ?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Stockage cloud, partage par lien\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combinez plusieurs clips en chapitres\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zooms intelligents au clic (ou ajoutez les vôtres !)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Ajoutez des modèles, sous-titres, mockups...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Fonds et disposition de caméra personnalisés\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Débloquez des fonctionnalités\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Soutenez le développement indépendant !\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Paramètres du compte\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Assistance\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Abonnement Pro inactif\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Votre abonnement à Screenity Pro est inactif.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Réactivez-le pour retrouver l’accès.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Vos vidéos et données seront supprimées définitivement le \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gérer l’abonnement\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Vous voulez continuer avec la version gratuite ?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Se déconnecter et passer à la version gratuite\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Déconnectée\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Reconnectez-vous pour synchroniser vos enregistrements et accéder aux fonctionnalités Pro.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Se reconnecter\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Vous pouvez continuer à utiliser l’extension sans compte, mais les enregistrements ne seront pas sauvegardés ni récupérables.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continuer sans se connecter\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Mode d’enregistrement instantané\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Téléchargement immédiat, mais impossible de modifier la disposition de la caméra après le début.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Mode d’enregistrement instantané\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Ce mode enregistre tout en une seule vidéo à télécharger et partager immédiatement. Vous ne pourrez pas modifier la disposition de la caméra, mais d’autres modifications restent possibles.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Compris\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"L’enregistrement d’onglet est désactivé sur cette page\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Ajout à : \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Terminer\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Envie d’en faire plus avec vos enregistrements ?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Connectez-vous pour sauvegarder vos vidéos dans le cloud, les partager par lien et accéder à des outils de montage avancés.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Connectez-vous pour débloquer les fonctionnalités Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Soutenez la créatrice indépendante !\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Débloquez plus de fonctionnalités\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Connectez-vous pour partager (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Débloquez le stockage cloud, l’édition multi-scènes, les zooms automatiques, les sous-titres et plus encore\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity restera toujours gratuit, open source et sans pub. Pro aide à couvrir les frais de cloud et soutient une créatrice indépendante ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Ne plus afficher\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Essayer\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Mode Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Enregistrez plusieurs scènes, comme votre écran, votre caméra ou les deux, les unes après les autres. Idéal pour faire plusieurs prises, changer de vue ou découper votre enregistrement en parties. Une fois terminé, cliquez sur Terminer pour ouvrir l’éditeur avec toutes vos scènes regroupées dans une seule vidéo.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Activez Pro pour commencer\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Abonnez-vous pour accéder aux fonctionnalités Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"S’abonner à Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Vous utilisez Screenity en local ? Soutenez son développement !\",\n    \"description\": \"Titre de la bannière d’auto-hébergement dans l’éditeur\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity est développé par une créatrice indépendante. L’auto-hébergement est gratuit, mais si vous trouvez l’outil utile, pensez à soutenir avec la version Pro ❤️\",\n    \"description\": \"Description de la bannière d’auto-hébergement dans l’éditeur\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Bienvenue à nouveau sur Screenity\",\n    \"description\": \"Titre de la fenêtre popup de bienvenue pour les utilisateurs de retour\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Nouveauté : Envie d’aller plus loin avec vos enregistrements ?\",\n    \"description\": \"Titre pour la fonctionnalité de stockage cloud dans la popup de bienvenue\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro est une nouvelle plateforme optionnelle pour sauvegarder vos vidéos dans le cloud, les partager par lien, et débloquer des outils d’édition avancés, si et quand vous en avez besoin.\",\n    \"description\": \"Description de la fonctionnalité de stockage cloud dans la popup de bienvenue\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"En savoir plus sur Screenity Pro\",\n    \"description\": \"Bouton d’appel à l’action pour la fonctionnalité cloud dans la popup de bienvenue\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Bienvenue sur Screenity Pro\",\n    \"description\": \"Titre de la fenêtre de bienvenue pour Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Vous êtes prêt·e à commencer à enregistrer ! Quelques points à garder en tête :\\n\\n- La bulle de caméra peut disparaître pendant l’enregistrement — c’est normal. Elle est capturée séparément en arrière-plan, vous pourrez la repositionner comme vous le souhaitez ensuite.\\n- Les zooms automatiques ne se déclenchent que sur les clics dans les onglets Chrome, à cause d’une limitation du navigateur. Vous pouvez toujours en ajouter manuellement après l’enregistrement.\\n- Pour des enregistrements rapides avec téléchargement immédiat, essayez le Mode Instantané. Notez simplement que les options d’édition sont limitées : pas d’arrière-plans, de mises en page ou de réglages avancés.\",\n    \"description\": \"Description de la fenêtre de bienvenue pour Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Compris\",\n    \"description\": \"Bouton d’action de la fenêtre de bienvenue pour Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 échec — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Bienvenue dans l'extension Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Quelques conseils rapides avant d'enregistrer.<br/>Les zooms automatiques ne fonctionnent que pour les clics dans les <strong>onglets Chrome</strong>. Vous pourrez toujours en ajouter manuellement ensuite.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barre d'enregistrement et effets\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Utilisez cette barre pour dessiner, appliquer des effets de curseur, flouter et contrôler l'enregistrement.<br/><br/><strong>Cette barre est visible dans la vidéo</strong> si vous ne la masquez pas.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"La caméra est capturée séparément\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Votre caméra peut <strong>se masquer ou passer en PiP</strong> pendant l'enregistrement, c'est normal.<br/><br/>Elle est capturée séparément pour pouvoir être repositionnée ensuite.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Mode instantané\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Parfait pour partager rapidement avec des <strong>téléchargements illimités</strong>.<br/><br/>Les mises en page et options d'édition avancées ne sont pas disponibles dans ce mode.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Compris\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Suivant\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Retour\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"En savoir plus\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Réinitialiser l'onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Espace disque faible. Sauvegarde de l'enregistrement en cours. Tout ce qui a été capturé est en sécurité.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Enregistrement long\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Votre enregistrement est complet et en sécurité. Téléchargez en WebM ci-dessous.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Indisponible pour les longs enregistrements\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Indisponible en mode de récupération\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Édition audio trop longue\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Cette édition audio n'est pas prise en charge pour les clips de plus de 15 minutes. Votre original n'est pas modifié. Essayez d'abord de rogner le clip.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Expiration de l'édition\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"L'édition n'a pas pu être terminée à temps. Votre enregistrement original n'est pas modifié. Réessayez ou rognez le clip d'abord.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Le partage d'écran s'est arrêté. Votre enregistrement est sauvegardé. Arrêtez quand vous êtes prêt.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Un problème de traitement est survenu, mais votre enregistrement est en sécurité. Vous pouvez le télécharger ci-dessous.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Traitement de votre modification en cours. L'enregistrement original n'est pas modifié.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"La modification n'a pas pu être appliquée\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Une erreur est survenue pendant le traitement. Votre enregistrement original est en sécurité. Vous pouvez réessayer ou le télécharger tel quel.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Le partage d'écran s'est arrêté. Sauvegarde de l'enregistrement en cours. Tout ce qui a été capturé est en sécurité.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio déconnecté. Sauvegarde de l'enregistrement en cours. La vidéo capturée est en sécurité.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Votre enregistrement est sauvegardé. L'éditeur ne s'est pas ouvert. Cliquez sur l'icône Screenity pour réessayer.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"La sauvegarde prend plus de temps que prévu. Vos données sont en sécurité. L'éditeur s'ouvrira bientôt.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copier les infos de débogage\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Infos de débogage copiées\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Obtenir de l'aide\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"L'enregistrement était trop court pour être sauvegardé\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"L'enregistreur rapide a \\u00e9chou\\u00e9\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"La sortie de l'enregistreur rapide n'a pas pu \\u00eatre valid\\u00e9e sur cet appareil.\\nVous pouvez quand m\\u00eame t\\u00e9l\\u00e9charger le fichier.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"T\\u00e9l\\u00e9charger quand m\\u00eame\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Annuler\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/hi/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"स्क्रीनिटी - स्क्रीन रिकॉर्डिंग & नोट्स\",\n    \"description\": \"एक्सटेंशन का नाम\"\n  },\n  \"extDesc\": {\n    \"message\": \"सबसे बेहतरीन मुफ्त स्क्रीन रिकॉर्डर बिना किसी सीमा के। छवि कैद करें, टिप्पणी करें, जूम करें, धुंधला करें, वीडियो संपादित करें और और भी - किसी पंजीकरण की आवश्यकता नहीं है, और गोपनीयता का आदरणीय।\",\n    \"description\": \"एक्सटेंशन का विवरण\"\n  },\n  \"recordTab\": {\n    \"message\": \"रिकॉर्ड\",\n    \"description\": \"पॉपअप पर रिकॉर्ड टैब लेबल\"\n  },\n  \"videosTab\": {\n    \"message\": \"आपके वीडियो\",\n    \"description\": \"पॉपअप पर वीडियो टैब लेबल\"\n  },\n  \"screenType\": {\n    \"message\": \"स्क्रीन\",\n    \"description\": \"पॉपअप पर स्क्रीन रिकॉर्डिंग प्रकार लेबल\"\n  },\n  \"tabType\": {\n    \"message\": \"टैब क्षेत्र\",\n    \"description\": \"पॉपअप पर टैब क्षेत्र रिकॉर्डिंग प्रकार लेबल\"\n  },\n  \"cameraType\": {\n    \"message\": \"कैमरा\",\n    \"description\": \"पॉपअप पर कैमरा रिकॉर्डिंग प्रकार लेबल\"\n  },\n  \"mockupType\": {\n    \"message\": \"मॉकअप\",\n    \"description\": \"पॉपअप पर मॉकअप रिकॉर्डिंग प्रकार लेबल\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"कस्टम क्षेत्र रिकॉर्डिंग अक्षम\",\n    \"description\": \"पॉपअप के लिए 104 से पुराने क्रोम संस्करण के लिए कस्टम क्षेत्र रिकॉर्डिंग अक्षम चेतावनी\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"क्रोम संस्करण को 110+ पर अपडेट करें\",\n    \"description\": \"क्रोम संस्करण 104 से पुराने के लिए कस्टम क्षेत्र रिकॉर्डिंग अक्षम चेतावनी विवरण\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"अपडेट करें\",\n    \"description\": \"क्रोम संस्करण 104 से पुराने के लिए कस्टम क्षेत्र रिकॉर्डिंग अक्षम चेतावनी क्रिया\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"अपनी अनुमतियों की जांच करें\",\n    \"description\": \"कैमरा / माइक्रोफ़ोन अनुमतियों की चेतावनी मॉडल शीर्षक\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"लगता है कि स्क्रीनिटी कैमरा या माइक्रोफ़ोन तक पहुंच नहीं पा रहा है। कृपया पता करें कि कैमरा आइकन पर क्लिक करके पहुंच को अनुमति दें।\",\n    \"description\": \"कैमरा / माइक्रोफ़ोन अनुमतियों की चेतावनी मॉडल विवरण\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"रद्द करें\",\n    \"description\": \"कैमरा / माइक्रोफ़ोन अनुमतियों की चेतावनी मॉडल निरस्त करें बटन\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"कैमरा अनुमति दें\",\n    \"description\": \"कैमरा अनुमति दें बटन\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"माइक्रोफ़ोन अनुमति दें\",\n    \"description\": \"माइक्रोफ़ोन अनुमति दें बटन\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"बोलने के लिए दबाएं\",\n    \"description\": \"पुश टू टॉक स्विच लेबल\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"रिकॉर्ड करने के लिए एक क्षेत्र सेट करें\",\n    \"description\": \"कस्टम क्षेत्र स्विच लेबल\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"कैमरा पलटें\",\n    \"description\": \"कैमरा पलटने का स्विच लेबल\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"पृष्ठभूमि प्रभाव\",\n    \"description\": \"पृष्ठभूमि प्रभाव स्विच लेबल\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"रिकॉर्डिंग शुरू करें\",\n    \"description\": \"रिकॉर्ड बटन लेबल\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"रिकॉर्डिंग शुरू हो रही है...\",\n    \"description\": \"रिकॉर्ड बटन कार्रवाई में लेबल\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"रिकॉर्ड करने के लिए कैमरा सेट करें\",\n    \"description\": \"कैमरा मोड में होते हुए रिकॉर्ड बटन लेबल और कोई कैमरा चयन नहीं किया गया है\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"अधिक विकल्प दिखाएं\",\n    \"description\": \"अधिक विकल्प लेबल\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"सिस्टम ऑडियो शामिल करें\",\n    \"description\": \"टैब/सिस्टम ऑडियो लेबल\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"टूलबार छिपाएं\",\n    \"description\": \"टूलबार लेबल\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"गिनती शुरू करें\",\n    \"description\": \"गिनती लेबल\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"समय सीमा सेट करें\",\n    \"description\": \"अलार्म लेबल\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"ब्लर\",\n    \"description\": \"पृष्ठभूमि ब्लर लेबल\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"कोई नहीं\",\n    \"description\": \"कोई डिवाइस चयन नहीं किया गया\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"कैमरा नहीं\",\n    \"description\": \"कोई कैमरा चयन नहीं किया गया\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"माइक्रोफ़ोन नहीं\",\n    \"description\": \"कोई माइक्रोफ़ोन चयन नहीं किया गया\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"एक स्रोत चुनें\",\n    \"description\": \"स्रोत चुनने का प्लेसहोल्डर\"\n  },\n  \"offLabel\": {\n    \"message\": \"बंद\",\n    \"description\": \"ड्रॉपडाउन पर बंद लेबल\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"चौड़ाई\",\n    \"description\": \"क्षेत्र चौड़ाई लेबल\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"ऊंचाई\",\n    \"description\": \"क्षेत्र ऊंचाई लेबल\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"छवि डालने के लिए क्लिक करें\",\n    \"description\": \"नई छवि जोड़ते समय टोस्ट\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"रिकॉर्डिंग समाप्त करें\",\n    \"description\": \"रिकॉर्डिंग समाप्त करने की टूलटिप\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"रिकॉर्डिंग फिर से शुरू करें\",\n    \"description\": \"रिकॉर्डिंग फिर से शुरू करने की टूलटिप\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"रिकॉर्डिंग रोकें\",\n    \"description\": \"रिकॉर्डिंग रोकने की टूलटिप\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"रिकॉर्डिंग फिर से शुरू करें\",\n    \"description\": \"रिकॉर्डिंग टूलटिप फिर से शुरू करें\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"रिकॉर्डिंग रद्द करें\",\n    \"description\": \"रिकॉर्डिंग टूलटिप रद्द करें\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"ड्रॉइंग टूल्स टॉगल करें\",\n    \"description\": \"ड्रॉइंग टूल्स टूलटिप टॉगल करें\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"ब्लर टूल टॉगल करें\",\n    \"description\": \"ब्लर टूल टूलटिप टॉगल करें\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"कर्सर विकल्प टॉगल करें\",\n    \"description\": \"कर्सर विकल्प टूलटिप टॉगल करें\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"कैमरा बंद करें\",\n    \"description\": \"कैमरा टूलटिप बंद करें\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"कैमरा चालने के लिए\",\n    \"description\": \"कैमरा टूलटिप चालने के लिए\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"कैमरा अनुमतियाँ नहीं हैं\",\n    \"description\": \"कैमरा टूलटिप अनुमतियाँ नहीं हैं\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"माइक्रोफोन बंद करें\",\n    \"description\": \"माइक्रोफोन टूलटिप बंद करें\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"माइक्रोफोन चालने के लिए\",\n    \"description\": \"माइक्रोफोन टूलटिप चालने के लिए\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"माइक्रोफोन अनुमतियाँ नहीं हैं\",\n    \"description\": \"माइक्रोफोन टूलटिप अनुमतियाँ नहीं हैं\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"टूल चुनें\",\n    \"description\": \"टूल टूलटिप चुनें\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"पेन टूल\",\n    \"description\": \"पेन टूलटिप\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"हाइलाइटर टूल\",\n    \"description\": \"हाइलाइटर टूलटिप\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"मिटाने वाला टूल\",\n    \"description\": \"मिटाने वाला टूलटिप\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"टेक्स्ट टूल\",\n    \"description\": \"टेक्स्ट टूलटिप\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"आकार टूल\",\n    \"description\": \"आकार टूलटिप\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"तीर टूल\",\n    \"description\": \"तीर टूलटिप\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"छवि टूल\",\n    \"description\": \"छवि टूलटिप\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"पूर्ववत करें\",\n    \"description\": \"पूर्ववत करें टूलटिप\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"फिर से करें\",\n    \"description\": \"फिर से करें टूलटिप\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"कैनवास साफ़ करें\",\n    \"description\": \"कैनवास साफ़ करें टूलटिप\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"और रंग\",\n    \"description\": \"और रंग टूलटिप\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"मोटा स्ट्रोक\",\n    \"description\": \"मोटा स्ट्रोक टूलटिप\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"मध्यम स्ट्रोक\",\n    \"description\": \"मध्यम स्ट्रोक टूलटिप\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"पतला स्ट्रोक\",\n    \"description\": \"पतला स्ट्रोक टूलटिप\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"पिक्चर-इन-पिक्चर मोड टॉगल करें\",\n    \"description\": \"पिक्चर-इन-पिक्चर मोड टूलटिप\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"भराई टॉगल करें\",\n    \"description\": \"भराई टूलटिप\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"ड्रॉइंग मोड\",\n    \"description\": \"ड्रॉइंग मोड टोस्ट\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"ब्लर मोड\",\n    \"description\": \"ब्लर मोड टोस्ट\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"सभी ब्लर किए गए तत्वों को हटाएं\",\n    \"description\": \"सभी ब्लर किए गए तत्वों को हटाएं टूलटिप\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"क्लिक को हाइलाइट करें\",\n    \"description\": \"क्लिक को हाइलाइट करें टूलटिप\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"कर्सर को हाइलाइट करें\",\n    \"description\": \"कर्सर को हाइलाइट करें टूलटिप\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"कर्सर को स्पॉटलाइट करें\",\n    \"description\": \"कर्सर को स्पॉटलाइट करें टूलटिप\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"फिर से न दिखाएं\",\n    \"description\": \"अनुमतियों मॉडल डिस्मिस बटन\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"क्या आप वीडियो को पुनः प्रारंभ करना चाहते हैं?\",\n    \"description\": \"वीडियो पुनः प्रारंभ मॉडल शीर्षक\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"आपकी वर्तमान वीडियो प्रगति खो जाएगी।\",\n    \"description\": \"वीडियो पुनः प्रारंभ मॉडल विवरण\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"वीडियो पुनः प्रारंभ करें\",\n    \"description\": \"वीडियो पुनः प्रारंभ मॉडल पुनः प्रारंभ बटन\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"रिज्यूम रिकॉर्डिंग\",\n    \"description\": \"वीडियो पुनः प्रारंभ मॉडल रिज्यूम बटन\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"क्या आप वीडियो को छोड़ना चाहते हैं?\",\n    \"description\": \"वीडियो को छोड़ने का मॉडल शीर्षक\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"आपकी वर्तमान वीडियो प्रगति खो जाएगी।\",\n    \"description\": \"वीडियो को छोड़ने का मॉडल विवरण\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"वीडियो को छोड़ें\",\n    \"description\": \"वीडियो को छोड़ने का मॉडल छोड़ने बटन\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"रिज्यूम रिकॉर्डिंग\",\n    \"description\": \"वीडियो को छोड़ने का मॉडल रिज्यूम बटन\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"चुनें कि आप क्या रिकॉर्ड करना चाहते हैं\",\n    \"description\": \"रिकॉर्डिंग पृष्ठ में शीर्षक\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"रेकॉर्डिंग...\",\n    \"description\": \"रेकॉर्डिंग पृष्ठ में रेकॉर्डिंग के दौरान शीर्षक\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"इस टैब को खुला रखें। यह आपकी रेकॉर्डिंग सहेजी जाने के बाद बंद हो जाएगा।\",\n    \"description\": \"रेकॉर्डिंग पृष्ठ में विवरण\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"रेकॉर्डिंग की तैयारी...\",\n    \"description\": \"सैंडबॉक्स पृष्ठ में रेकॉर्डिंग / सहेजने के दौरान शीर्षक\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"इस टैब को खुला रखें। यह आपकी रेकॉर्डिंग तैयार होने पर अपडेट हो जाएगा।\",\n    \"description\": \"सैंडबॉक्स पृष्ठ में रेकॉर्डिंग / सहेजने के दौरान विवरण\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"संपादक\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में नेविगेशन का शीर्षक\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"रद्द करें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में रद्द करें बटन\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"मूल को पुनः लाएं\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में पुनर्स्थापित करें बटन\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"रीसेट\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में रीसेट करें बटन\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"परिवर्तन सहेजें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में सहेजें बटन\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"सहेज रहा है...\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में सहेजें बटन के दौरान सहेजें\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"आपके ऑडियो फ़ाइल को खींचें और छोड़ें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में ऑडियो फ़ाइल को खींचें और छोड़ें\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"या ब्राउज़ करने के लिए क्लिक करें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में ऑडियो फ़ाइल ब्राउज़ करने के लिए क्लिक करें\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"सेटिंग्स\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में ऑडियो सेटिंग्स शीर्षक\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"वॉल्यूम\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में ऑडियो वॉल्यूम लेबल\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"अपडेट करें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में ऑडियो को अपडेट करें बटन\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"क्रॉप\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में क्रॉप शीर्षक\"\n  },\n  \"widthLabel\": {\n    \"message\": \"चौड़ाई\",\n    \"description\": \"संपत्ति के लिए चौड़ाई लेबल\"\n  },\n  \"heightLabel\": {\n    \"message\": \"ऊंचाई\",\n    \"description\": \"संपत्ति के लिए ऊंचाई लेबल\"\n  },\n  \"leftLabel\": {\n    \"message\": \"बाएं\",\n    \"description\": \"संपत्ति के लिए बाएं लेबल\"\n  },\n  \"topLabel\": {\n    \"message\": \"ऊपर\",\n    \"description\": \"संपत्ति के लिए ऊपर लेबल\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"चुने हुए खंड को संपादित करने के लिए हैंडल को खींचें और बाएं बटन का उपयोग करें।\",\n    \"description\": \"सैंडबॉक्स संपादक में कट आपूर्ति के बारे में जानकारी\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"वीडियो को कटा जा रहा है\",\n    \"description\": \"सैंडबॉक्स संपादक में कट बटन\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"कटाई जा रही है...\",\n    \"description\": \"सैंडबॉक्स संपादक में कट बटन की कटाई के दौरान\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"खंड काटें\",\n    \"description\": \"सैंडबॉक्स संपादक में कट बटन\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"कटाई जा रही है...\",\n    \"description\": \"सैंडबॉक्स संपादक में कट बटन की कटाई के दौरान\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"ध्वनि बंद करें\",\n    \"description\": \"सैंडबॉक्स संपादक में म्यूट बटन\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"म्यूट किया जा रहा है...\",\n    \"description\": \"सैंडबॉक्स संपादक में म्यूट बटन की म्यूट करते समय\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"और जानें।\",\n    \"description\": \"एक डॉट के साथ और जानें\"\n  },\n  \"undoLabel\": {\n    \"message\": \"पूर्ववत करें\",\n    \"description\": \"पूर्ववत लेबल\"\n  },\n  \"redoLabel\": {\n    \"message\": \"फिर से करें\",\n    \"description\": \"फिर से करें लेबल\"\n  },\n  \"leaveReview\": {\n    \"message\": \"समीक्षा छोड़ें\",\n    \"description\": \"समीक्षा छोड़ें बटन\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"अपडेट्स के लिए फॉलो करें\",\n    \"description\": \"अपडेट्स के लिए फॉलो करें बटन\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"आप वर्तमान में ऑफ़लाइन हैं\",\n    \"description\": \"ऑफ़लाइन लेबल\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"कुछ सुविधाएं फिर से कनेक्ट होने तक अनुपलब्ध हैं\",\n    \"description\": \"ऑफ़लाइन लेबल विवरण\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"फिर से प्रयास करें\",\n    \"description\": \"ऑफ़लाइन लेबल पुनः प्रयास करें बटन\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"क्रोम को अपडेट करने की आवश्यकता है\",\n    \"description\": \"क्रोम अपडेट लेबल\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"और फ़ीचर्स तक पहुँचने के लिए अपडेट करें\",\n    \"description\": \"क्रोम अपडेट लेबल विवरण\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"और जानें\",\n    \"description\": \"और जानें बटन\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"वीडियो स्थानीय रूप से प्रोसेस करने के लिए बहुत लम्बा है\",\n    \"description\": \"संपादक में 5 मिनट से अधिक सीमा का लेबल\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"संपादन और MP4 निर्यात उपलब्ध नहीं है\",\n    \"description\": \"संपादक में 5 मिनट से अधिक सीमा का विवरण\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"वीडियो प्रसंस्करण हो रहा है...\",\n    \"description\": \"संपादक में वीडियो प्रसंस्करण लेबल\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"फ़ास्ट संपादन के लिए अपग्रेड करें\",\n    \"description\": \"संपादक में वीडियो प्रसंस्करण विवरण\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"संपादन करें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में संपादन शीर्षक\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"कोई कनेक्शन नहीं\",\n    \"description\": \"कोई कनेक्शन लेबल\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"उपलब्ध नहीं है\",\n    \"description\": \"उपलब्ध नहीं है लेबल\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"वीडियो संपादित करें\",\n    \"description\": \"ट्रिम लेबल बटन\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"वीडियो को ट्रिम, कट या म्यूट करें\",\n    \"description\": \"ट्रिम लेबल\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"तैयारी हो रही है...\",\n    \"description\": \"तैयारी लेबल\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"क्रॉप\",\n    \"description\": \"क्रॉप लेबल बटन\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"वीडियो को क्रॉप और आकार दें\",\n    \"description\": \"क्रॉप लेबल\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"ऑडियो जोड़ें\",\n    \"description\": \"ऑडियो लेबल बटन\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"अपना खुद का ऑडियो अपलोड करके वीडियो में जोड़ें\",\n    \"description\": \"ऑडियो लेबल\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"सहेजें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में शीर्षक सहेजें\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"ड्राइव से लॉग आउट करें\",\n    \"description\": \"Google ड्राइव से लॉग आउट लेबल\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"सहेज रहे हैं...\",\n    \"description\": \"Google ड्राइव में सहेज रहे हैं लेबल\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"ड्राइव में सहेजें\",\n    \"description\": \"Google ड्राइव में सहेजें बटन\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"वर्तमान संस्करण को Google ड्राइव में सहेजें\",\n    \"description\": \"Google ड्राइव में सहेजें लेबल\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"साइन इन करें और ड्राइव में सहेजें\",\n    \"description\": \"Google ड्राइव में साइन इन करें लेबल\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"निर्यात\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में निर्यात शीर्षक\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"डाउनलोड हो रहा है...\",\n    \"description\": \"डाउनलोड हो रहा है लेबल\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \".webm के रूप में डाउनलोड करें\",\n    \"description\": \"WEBM डाउनलोड बटन\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \".webm वीडियो निर्यात करें\",\n    \"description\": \"WEBM डाउनलोड लेबल\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \".mp4 के रूप में डाउनलोड करें\",\n    \"description\": \"MP4 डाउनलोड बटन\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \".mp4 वीडियो निर्यात करें (सिफारिश किया जाता है)\",\n    \"description\": \"MP4 डाउनलोड लेबल\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \".gif के रूप में डाउनलोड करें\",\n    \"description\": \"GIF डाउनलोड बटन\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"एक GIF निर्यात करें (अधिकतम 30 सेकंड)\",\n    \"description\": \"GIF डाउनलोड लेबल\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"क्या आप अपनी रिकॉर्डिंग बेहतर करना चाहते हैं?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"क्लाउड शेयरिंग, ज़ूम, टेम्पलेट्स पाएं... और एक प्राइवेसी-फ्रेंडली टूल को सपोर्ट करें जिसे एक इंडी डेव ने बनाया है ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Pro के बारे में जानें\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"पहले से अकाउंट है? लॉग इन करें\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"वीडियो साझा करें\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में साझा करें बटन\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"मौजूदा ऑडियो को बदलें\",\n    \"description\": \"संपादक में ऑडियो बदलें बटन\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"कर्सर पर ज़ूम करें\",\n    \"description\": \"बिंदु पॉपअप पर ज़ूम करें\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"रिकॉर्डिंग के समय पेज में रहें\",\n    \"description\": \"पेज में रहें पॉपअप\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"माइक्रोफ़ोन बंद है चेतावनी\",\n    \"description\": \"माइक्रोफ़ोन चेतावना पॉपअप\"\n  },\n  \"helpPopup\": {\n    \"message\": \"सहायता\",\n    \"description\": \"सहायता पॉपअप बटन\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"रिकॉर्डिंग रोकी गई है। फिर से आरंभ करने के लिए प्ले बटन दबाएं।\",\n    \"description\": \"रुकी हुई रिकॉर्डिंग मॉडल शीर्षक\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity को आपकी स्क्रीन रिकॉर्ड करने की अनुमति नहीं है\",\n    \"description\": \"अनुमति मॉडल शीर्षक\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"स्क्रीन रिकॉर्डिंग सक्षम करने के लिए, आपको Screenity को आपकी स्क्रीन को कैप्चर करने की अनुमति देनी होगी जब प्रोम्प्ट होता है।\",\n    \"description\": \"अनुमति मॉडल विवरण\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"स्क्रीन रिकॉर्डिंग सक्षम करें\",\n    \"description\": \"अनुमति मॉडल क्रिया\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"रद्द करें\",\n    \"description\": \"अनुमति मॉडल रद्द करें\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"आपका माइक्रोफ़ोन म्यूट है\",\n    \"description\": \"माइक्रोफ़ोन म्यूट हो गया मॉडल शीर्षक\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"आपकी रिकॉर्डिंग में ऑडियो शामिल करने के लिए, आपको अपने माइक्रोफ़ोन को अनम्यूट करना होगा। क्या आप बिना ऑडियो के जारी रखना चाहते हैं?\",\n    \"description\": \"माइक्रोफ़ोन म्यूट हो गया मॉडल विवरण\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"हाँ, जारी रखें\",\n    \"description\": \"माइक्रोफ़ोन म्यूट हो गया मॉडल जारी रखें\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"रद्द करें\",\n    \"description\": \"माइक्रोफ़ोन म्यूट हो गया मॉडल रद्द करें\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Screenity के साथ तीन सरल कदमों में शुरू हो जाइए:\",\n    \"description\": \"सेटअप कदम शीर्षक\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- क्लिक करें \",\n    \"description\": \"सेटअप कदम 1, चिन्ह के पहले। इसे आखिरी में एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" एक्सटेंशन चिन्ह\",\n    \"description\": \"सेटअप कदम 1, चिन्ह के बाद। इसे पहले एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- दबाएं \",\n    \"description\": \"सेटअप कदम 2, चिन्ह के पहले। इसे आखिरी में एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" पिन आइकन\",\n    \"description\": \"सेटअप कदम 2, चिन्ह के बाद। इसे पहले एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- क्लिक करें \",\n    \"description\": \"सेटअप कदम 3, चिन्ह के पहले। इसे आखिरी में एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity आइकन को शुरू करने के लिए\",\n    \"description\": \"सेटअप कदम 3, चिन्ह के बाद। इसे पहले एक स्पेस की आवश्यकता है।\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"बढ़िया! आपका सेटअप पूरा है\",\n    \"description\": \"सेटअप पूरा शीर्षक\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"आप अब यहां या किसी और टैब में रिकॉर्डिंग शुरू कर सकते हैं।\",\n    \"description\": \"सेटअप पूरा विवरण\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"ड्रा करने के लिए यहां क्लिक करें\",\n    \"description\": \"यहां क्लिक करने पर आग्रहण\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"आराम करें। किसी भी जगह पर क्लिक करके गिनती रोकने के लिए।\",\n    \"description\": \"गिनती संदेश\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"संवाद के बीच के अंतराल के कारण छोटे समय सीमाओं के लिए संशोधन अशुद्ध हो सकते हैं।\",\n    \"description\": \"संपादक बहुत छोटा होने के बारे में जानकारी\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"वीडियो प्रोसेस हो रहा है, प्लेबैक अशुद्ध या टूट सकता है।\",\n    \"description\": \"संपादक में प्रोसेसिंग बैनर\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"क्रॉपिंग कुछ समय लग सकता है\",\n    \"description\": \"क्रॉपिंग जानकारी शीर्षक\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"तेज प्रोसेसिंग के लिए अपग्रेड करें\",\n    \"description\": \"क्रॉपिंग जानकारी विवरण\"\n  },\n  \"micOnToast\": {\n    \"message\": \"माइक्रोफ़ोन सक्षम है\",\n    \"description\": \"माइक्रोफ़ोन सक्षम टोस्ट शीर्षक\"\n  },\n  \"micOffToast\": {\n    \"message\": \"माइक्रोफ़ोन बंद है\",\n    \"description\": \"माइक्रोफ़ोन बंद टोस्ट शीर्षक\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"UI सूचनाओं को छुपाएं\",\n    \"description\": \"UI सूचनाओं को छुपाएं बटन\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"फिर से दिखाना नहीं\",\n    \"description\": \"फिर से दिखाना नहीं बटन\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"उपयोग न करने पर टूलबार छुपाएं\",\n    \"description\": \"केवल हवाकारी पर टूलबार दिखाएं बटन\"\n  },\n  \"stopRecording\": {\n    \"message\": \"रिकॉर्डिंग बंद करें\",\n    \"description\": \"रिकॉर्डिंग स्क्रीन में बंद करें बटन\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"नए Screenity में आपका स्वागत है!\",\n    \"description\": \"मौजूदा उपयोगकर्ताओं के लिए अपडेट घोषणा शीर्षक\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"हम एक नए दिखावट और कई रोमांचक सुविधाओं का परिचय कर रहे हैं, लेकिन चिंता न करें, यह वही मुफ्त, निजी और ओपन सोर्स एक्सटेंशन है जो आप जानते और पसंद करते हैं।\",\n    \"description\": \"मौजूदा उपयोगकर्ताओं के लिए अपडेट घोषणा विवरण\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"अपडेट के बारे में और अधिक जानें।\",\n    \"description\": \"मौजूदा उपयोगकर्ताओं के लिए अपडेट घोषणा और अधिक जानें लिंक\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"शुरू हो जाओ\",\n    \"description\": \"मौजूदा उपयोगकर्ताओं के लिए अपडेट घोषणा बटन\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"रिकॉर्डिंग शुरू करने में त्रुटि\",\n    \"description\": \"स्ट्रीम त्रुटि मॉडल शीर्षक\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"लगता है कि रिकॉर्डिंग शुरू करने में त्रुटि हुई है। अपने ब्राउज़र को फिर से चालने और पुनः प्रयास करें। यदि समस्या बरकरार रहती है, कृपया हमसे संपर्क करें support@screenity.io पर।\",\n    \"description\": \"स्ट्रीम त्रुटि मॉडल विवरण\"\n  },\n  \"highestQuality\": {\n    \"message\": \"सर्वोत्तम गुणवत्ता\",\n    \"description\": \"पॉपअप में सर्वोत्तम गुणवत्ता का लेबल टॉगल करें\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"आखिरी रिकॉर्डिंग को पुनर्स्थापित करें\",\n    \"description\": \"पॉपअप में आखिरी रिकॉर्डिंग को पुनर्स्थापित करने के लिए बटन\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"कोई समस्या है?\",\n    \"description\": \"संपादक में समस्या बटन\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"अपनी रिकॉर्डिंग देख नहीं सकते?\",\n    \"description\": \"संपादक में समस्या मॉडल टाइटल\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"अगर आपने अपनी रिकॉर्डिंग पूरी की है और इस पृष्ठ पर अपने वीडियो को देखते बिना पाए हैं, तो यदि यह उपलब्ध है, आप वीडियो के रॉ डेटा को डाउनलोड करने का प्रयास कर सकते हैं। आप भी हमसे संपर्क कर सकते हैं और एक बग रिपोर्ट सबमिट कर सकते हैं।\",\n    \"description\": \"संपादक में समस्या मॉडल विवरण\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"रॉ वीडियो डेटा डाउनलोड करें\",\n    \"description\": \"संपादक में समस्या मॉडल बटन\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"बग रिपोर्ट करें\",\n    \"description\": \"संपादक में समस्या मॉडल बटन 2\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"कोई रिकॉर्डिंग नहीं मिली, दुखी हैं :(\",\n    \"description\": \"संपादक में कोई रिकॉर्डिंग नहीं मिली अलर्ट\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"मेमोरी सीमा पहुंच गई\",\n    \"description\": \"मेमोरी सीमा पहुंच गई शीर्षक\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"आपके डिवाइस की रिकॉर्डिंग के लिए उपलब्ध मेमोरी का समय समाप्त हो गया है, इसके परिणामस्वरूप रिकॉर्डिंग स्वतः बंद हो गई है। अगर आप और लंबे समय तक रिकॉर्ड करना चाहते हैं, तो वीडियो की गुणवत्ता को कम करने का प्रयास करें या अपने डिवाइस पर कुछ स्थान मुक्त करें।\",\n    \"description\": \"मेमोरी सीमा पहुंच गई विवरण\"\n  },\n  \"understoodButton\": {\n    \"message\": \"समझा\",\n    \"description\": \"समझा बटन\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"पर्याप्त स्थान नहीं\",\n    \"description\": \"पर्याप्त स्थान नहीं शीर्षक\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity को रिकॉर्ड करने के लिए कम से कम 1 जीबी फ्री स्थान की आवश्यकता है। कृपया स्थान मुक्त करें और पुनः प्रयास करें।\",\n    \"description\": \"पर्याप्त स्थान नहीं विवरण\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"पिछली रिकॉर्डिंग हटाएं\",\n    \"description\": \"पिछली रिकॉर्डिंग हटाएं बटन\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"उन्नत\",\n    \"description\": \"सैंडबॉक्स संपादक पृष्ठ में उन्नत खंड\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"कच्चा वीडियो फ़ाइल डाउनलोड करें\",\n    \"description\": \"कच्चे रिकॉर्डिंग बटन डाउनलोड करें\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"मूल रिकॉर्डिंग डेटा को निर्यात करें\",\n    \"description\": \"कच्चे रिकॉर्डिंग लेबल डाउनलोड करें\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"अपनी रिकॉर्डिंग के लिए मदद प्राप्त करें\",\n    \"description\": \"समस्या समाधान बटन\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"अपने वीडियो को पुनः प्राप्त करें और अन्य समस्याओं का समाधान करें\",\n    \"description\": \"समस्या समाधान लेबल\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"कच्चा वीडियो फ़ाइल डाउनलोड करें\",\n    \"description\": \"कच्चे रिकॉर्डिंग मॉडल शीर्षक\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"कच्चा वीडियो फ़ाइल वह मूल वीडियो फ़ाइल है जो Screenity द्वारा रिकॉर्ड की गई है। इस वीडियो को प्रोसेस या संपादित नहीं किया गया है, इसलिए यह बहुत बड़ा हो सकता है और सभी उपकरणों पर प्लेबल नहीं हो सकता है। यदि आपको प्रोसेस्ड वीडियो के साथ समस्या हो रही है, तो आप इस फ़ाइल का उपयोग अपने वीडियो को पुनः प्राप्त करने के लिए कर सकते हैं।\",\n    \"description\": \"कच्चे रिकॉर्डिंग मॉडल विवरण\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"डाउनलोड करें\",\n    \"description\": \"कच्चे रिकॉर्डिंग मॉडल बटन\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"सिस्टम जानकारी और वीडियो डेटा डाउनलोड करें और समस्या समाधान के लिए भेजें\",\n    \"description\": \"समस्या समाधान मॉडल शीर्षक\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"डाउनलोड करने के बाद, सहायता@screenity.io पर फ़ाइल भेजें और संबंधित जानकारी के साथ। हम इन डेटा का उपयोग आपकी एक्सटेंशन के साथ किसी भी समस्या को समाधान करने और आपके वीडियो को यदि संभव हो तो पुनः प्राप्त करने के लिए करेंगे।\",\n    \"description\": \"समस्या समाधान मॉडल विवरण\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"डाउनलोड करें\",\n    \"description\": \"समस्या समाधान मॉडल बटन\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"वीडियो को आपके डिवाइस में प्रोसेस करने के लिए बहुत लम्बी है\",\n    \"description\": \"5 मिनट सीमा मॉडल शीर्षक\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"क्योंकि Screenity पूरी तरह से निजी है और स्थानीय रूप से चलता है, यह आपके डिवाइस के हार्डवेयर द्वारा सीमित होता है। इस लम्बाई के वीडियो को प्रोसेस करना बहुत समय लगेगा और बहुत अधिक संसाधनों की आवश्यकता होगी। फिकर न करें, आप फिर भी WEBM फ़ाइल या रॉ रिकॉर्डिंग डाउनलोड कर सकते हैं, लेकिन संपादन और MP4 निर्यात उपलब्ध नहीं हैं। इसके बावजूद, आप फिर भी कोशिश कर सकते हैं कि वीडियो को प्रोसेस करने का प्रयास करें, अगर आप खतरों को समझते हैं।\",\n    \"description\": \"5 मिनट सीमा मॉडल विवरण\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"मैं खतरा समझता हूँ, फिर भी कोशिश करें\",\n    \"description\": \"5 मिनट सीमा मॉडल बटन\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"लंबे वीडियो को प्रोसेस करने के बारे में और जानें।\",\n    \"description\": \"5 मिनट सीमा मॉडल की अधिक जानकारी वाला लिंक\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"पुनर्प्राप्ति मोड\",\n    \"description\": \"पुनर्प्राप्ति मोड का शीर्षक\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"समस्या समाधान के लिए डेटा डाउनलोड करें\",\n    \"description\": \"समस्या समाधान के लिए डाउनलोड करने का विकल्प\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"सहायता केंद्र\",\n    \"description\": \"सहायता नेविगेशन आइटम\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"कंप्यूटर ऑडियो रिकॉर्ड करें\",\n    \"description\": \"ऑडियो चेतावनी शीर्षक\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"इस पेज से ऑडियो रिकॉर्ड करने के लिए, आपको '$tab$' पर स्विच करना होगा।\",\n    \"description\": \"ऑडियो चेतावनी विवरण\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"टैब क्षेत्र\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"एक्सटेंशन टैब पर समर्थित नहीं\",\n    \"description\": \"एक्सटेंशन टैब पर समर्थित नहीं शीर्षक\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"आपके पिछले टैब पर Screenity समर्थित नहीं था। आप अपनी रिकॉर्डिंग यहाँ पर शुरू कर सकते हैं और फिर टैब पर स्विच कर सकते हैं, लेकिन आपका कैमरा और टूलबार दिखाई नहीं देंगे।\",\n    \"description\": \"एक्सटेंशन टैब पर समर्थित नहीं शीर्षक विवरण\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"अपनी Screenity रिकॉर्डिंग्स के लिए स्थानीय बैकअप सेट अप करें\",\n    \"description\": \"बैकअप शीर्षक\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"आपकी सभी रिकॉर्डिंग्स की प्रतिलिपि रखें। यदि आप बैकअप सेट नहीं करते हैं, तो आपकी रिकॉर्डिंग्स को डाउनलोड करने के लिए ही बचाया जाएगा।\",\n    \"description\": \"बैकअप विवरण\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"यदि आप डाउनलोड्स या अन्य सिस्टम फ़ोल्डर्स में सहेजने का प्रयास कर रहे हैं, तो आपको पहले एक नया फ़ोल्डर बनाने की आवश्यकता हो सकती है।\",\n    \"description\": \"बैकअप विवरण\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"एक फ़ोल्डर चुनें\",\n    \"description\": \"फ़ोल्डर चुनने का बटन\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"अभी नहीं\",\n    \"description\": \"अब नहीं बटन\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"आपकी रिकॉर्डिंग्स स्थानीय रूप से बैकअप किए जा रहे हैं\",\n    \"description\": \"सक्रिय बैकअप शीर्षक\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"रिकॉर्ड करते समय हर बार अनुमतियों के लिए पूछा जाने को रोकने के लिए हम आपको सुझाव देते हैं कि आप इस टैब को खोले रखें।\",\n    \"description\": \"सक्रिय बैकअप विवरण\"\n  },\n  \"backupsClose\": {\n    \"message\": \"तब भी टैब बंद करें\",\n    \"description\": \"फिर भी टैब बंद करें बटन\"\n  },\n  \"backupsStop\": {\n    \"message\": \"सभी रिकॉर्डिंग्स का बैकअप बंद करें\",\n    \"description\": \"बैकअप बंद करें बटन\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"कृपया अपने स्थानीय Screenity बैकअप फ़ोल्डर तक पहुँच की पुनः पुष्टि करें\",\n    \"description\": \"बैकअप पुष्टि शीर्षक\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"हमें खेद है, लेकिन हमें आपकी रिकॉर्डिंग्स को स्थानीय रूप से बैकअप करने के लिए एक बार फिर आपकी अनुमति की आवश्यकता है। दुखद है कि हर बार यह सवाल पूछना होगा जब आप इस टैब को बंद करते हैं, जो ब्राउज़र को बंद करने या अपने कंप्यूटर को पुनरारंभ करने पर हो सकता है।\",\n    \"description\": \"बैकअप पुष्टि विवरण\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Screenity को रिकॉर्डिंग्स को बैकअप करने की अनुमति देने दें\",\n    \"description\": \"बैकअप पुष्टि अनुमति बटन\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"रिकॉर्डिंग्स का बैकअप करें\",\n    \"description\": \"बैकअप टॉगल लेबल\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"रिकॉर्डिंग की बैकअप के लिए अनुमति नहीं दी गई\",\n    \"description\": \"बैकअप अनुमति असफल शीर्षक\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"आपने अपनी रिकॉर्डिंग्स की स्थानीय बैकअप के लिए कोई फ़ोल्डर चुना नहीं है। आपके ब्राउज़र संस्करण या ऑपरेटिंग सिस्टम के आधार पर आपको हर बार रिकॉर्डिंग शुरू करने पर फ़ोल्डर का चयन करने के लिए कहा जा सकता है। आप फिर से कोशिश कर सकते हैं, या यदि आप दोहरी अनुमतियों को नहीं देना चाहते हैं, तो पूरी तरह से बैकअप को अक्षम कर सकते हैं।\",\n    \"description\": \"बैकअप अनुमति असफल विवरण\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"कंप्यूटर ऑडियो रिकॉर्ड करें\",\n    \"description\": \"ऑडियो चेतावना शीर्षक\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"macOS में, आप केवल टैब ऑडियो को रिकॉर्ड कर सकते हैं। 'भी Chrome टैब ऑडियो साझा करें' को सक्षम करने के लिए Chrome टैब विकल्प का चयन करें और सुनिश्चित करें।\",\n    \"description\": \"macOS ऑडियो चेतावना विवरण\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"कंप्यूटर ऑडियो रिकॉर्ड करें\",\n    \"description\": \"ऑडियो चेतावना शीर्षक\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"कंप्यूटर ऑडियो को रिकॉर्ड करना चाहते हैं तो 'सिस्टम ऑडियो साझा करें' विकल्प को टॉगल करने की सुनिश्चित करें। इस विकल्प को दिखाई नहीं देता है, तो कृपया Chrome को अपडेट करने का प्रयास करें, या टैब रिकॉर्डिंग पर स्विच करें।\",\n    \"description\": \"अन्य ऑडियो चेतावना विवरण\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"अधिकतम रिज़ॉल्यूशन\",\n    \"description\": \"सेटिंग्स मेनू में अधिकतम रिज़ॉल्यूशन का लेबल\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"अधिकतम FPS\",\n    \"description\": \"सेटिंग्स मेनू में अधिकतम FPS का लेबल\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"पर्याप्त रैम नहीं\",\n    \"description\": \"सेटिंग्स मेनू में 'पर्याप्त रैम नहीं' लेबल\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"विंडो का आकार बदलें\",\n    \"description\": \"सेटिंग्स मेनू में विंडो के आकार के लिए लेबल\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"इस रिज़ॉल्यूशन के लिए स्क्रीन बहुत छोटा है\",\n    \"description\": \"सेटिंग्स मेनू में स्क्रीन बहुत छोटा है टूलटिप\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"इस उपकरण पर इस रिज़ॉल्यूशन पर रिकॉर्ड नहीं किया जा सकता है\",\n    \"description\": \"सेटिंग्स मेनू में इस रिज़ॉल्यूशन पर रिकॉर्ड नहीं किया जा सकता है टूलटिप\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"अनुमतियों की समीक्षा करें\",\n    \"description\": \"अनुमतियों की समीक्षा करने का बटन\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"एक और रिकॉर्डिंग जोड़ें\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"तैयारी की जा रही है...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"थोड़ा रुकिए! आपकी रिकॉर्डिंग तैयार की जा रही है।\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"टूलबार छुपा हुआ है। पॉपअप विकल्पों में फिर से सक्षम करें।\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"लॉग इन करें या साइन अप करें\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"लॉग आउट करें\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"आप लॉग आउट हो चुके हैं\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"स्टोरेज सीमा पूरी हो गई है\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"आपने अपनी पूरी 100 GB स्टोरेज का उपयोग कर लिया है। कृपया रिकॉर्डिंग जारी रखने के लिए पुराने वीडियो या सीन हटाएं।\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"स्टोरेज प्रबंधित करें\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"बंद करें\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"स्टोरेज जांच नहीं हो सकी\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"आपकी स्टोरेज जांचने में समस्या हुई। कृपया दोबारा लॉग इन करें और रिकॉर्डिंग आज़माएं।\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"हम आपकी स्टोरेज कोटा सत्यापित नहीं कर सके। कुछ सेकंड में दोबारा प्रयास करें या पेज रीफ़्रेश करें।\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"फिर से प्रयास करें\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"सीन को Multi रिकॉर्डिंग में जोड़ा गया\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"अपने वीडियो में नया सीन रिकॉर्ड करने के लिए तैयार\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"रिकॉर्डिंग 90 मिनट की सीमा के करीब है, यह जल्द ही बंद हो जाएगी\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"90 मिनट की सीमा के कारण रिकॉर्डिंग बंद हो गई\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"इस पेज पर टैब रिकॉर्डिंग अक्षम है। स्क्रीन रिकॉर्डिंग पर स्विच किया गया।\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"वीडियो रिकॉर्डिंग रद्द कर दी गई\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"लिंक क्लिपबोर्ड पर कॉपी किया गया\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"लिंक कॉपी करने में विफल\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"नवीनतम\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"सबसे पुराने\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"सभी वीडियो\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"कोई वीडियो नहीं मिला\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"वीडियो लोड हो रहे हैं...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"डैशबोर्ड पर जाएं\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Screenity में आपका स्वागत है\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"कोई विज्ञापन नहीं। कोई ट्रैकिंग नहीं। कोई सीमा नहीं।\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"बस रिकॉर्ड दबाएँ।\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"इसे मुफ्त में इस्तेमाल करें — साइन अप की ज़रूरत नहीं!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"अपने वीडियो से और ज़्यादा करना चाहते हैं?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"क्लाउड स्टोरेज, लिंक के साथ वीडियो साझा करें\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"कई क्लिप को एक कहानी में जोड़ें\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"क्लिक पर स्मार्ट ज़ूम (या अपना जोड़ें!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"टेम्पलेट, कैप्शन, लेआउट जोड़ें...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"कस्टम बैकग्राउंड और मॉकअप\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 अतिरिक्त सुविधाएँ अनलॉक करें\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ एक सोलो इंडी मेकर के विकास को सपोर्ट करें!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"अकाउंट सेटिंग्स\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"सहायता\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"आपकी Pro सदस्यता सक्रिय नहीं है\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"आपकी Screenity Pro सदस्यता सक्रिय नहीं है।\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"कृपया एक्सेस फिर से शुरू करने के लिए पुनः सक्रिय करें।\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"आपके वीडियो और डेटा को स्थायी रूप से हटाया जाएगा \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"अपनी सदस्यता प्रबंधित करें\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"क्या आप मुफ्त संस्करण का उपयोग जारी रखना चाहते हैं?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"लॉग आउट करें और डाउनग्रेड करें\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"आप लॉग आउट हो चुके हैं\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"अपनी रिकॉर्डिंग को अकाउंट से सिंक रखने और प्रीमियम फीचर्स का उपयोग जारी रखने के लिए, दोबारा लॉग इन करना होगा।\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"दोबारा लॉग इन करें\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"आप बिना अकाउंट के एक्सटेंशन का उपयोग कर सकते हैं — लेकिन आपकी रिकॉर्डिंग सेव नहीं होगी और बाद में पुनः प्राप्त नहीं की जा सकेगी।\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"लॉगिन के बिना जारी रखें\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"तत्काल रिकॉर्डिंग मोड\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"तत्काल डाउनलोड, लेकिन कैमरा और लेआउट बाद में संपादन योग्य नहीं होंगे।\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"तत्काल रिकॉर्डिंग मोड\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"यह सब कुछ एक वीडियो में रिकॉर्ड करता है ताकि तुरंत डाउनलोड और साझा किया जा सके। आप बाद में कैमरा लेआउट नहीं बदल सकेंगी, लेकिन अन्य संपादन संभव हैं।\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"समझ गई\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"इस पेज पर टैब रिकॉर्डिंग अक्षम है\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"इसमें जोड़ रहे हैं: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"समाप्त करें\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"अपनी रिकॉर्डिंग से और अधिक करना चाहती हैं?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"अपने वीडियो क्लाउड में सेव करने, लिंक के साथ साझा करने और एडवांस एडिटिंग फीचर्स एक्सेस करने के लिए साइन इन करें।\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"पेड फीचर्स अनलॉक करने के लिए साइन इन करें\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"एक सोलो इंडी मेकर के विकास को सपोर्ट करें\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"और अधिक फीचर्स अनलॉक करें\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"साझा करने के लिए साइन इन करें (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"क्लाउड शेयरिंग, मल्टी-सीन एडिटिंग, ऑटो ज़ूम, कैप्शन और बहुत कुछ अनलॉक करें\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity हमेशा मुफ्त, ओपन-सोर्स और विज्ञापन मुक्त रहेगा। Pro क्लाउड और इन्फ्रास्ट्रक्चर लागत को कवर करता है और एक सोलो इंडी डेव का समर्थन करता है! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"फिर से न दिखाएं\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"आज़माएं\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"मल्टी मोड\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"एक के बाद एक कई सीन रिकॉर्ड करें — जैसे आपकी स्क्रीन, कैमरा या दोनों। यह कई टेक लेने, व्यू बदलने या रिकॉर्डिंग को हिस्सों में बाँटने के लिए बेहतरीन है। जब आप समाप्त कर लें, तो सभी सीन को एक ही वीडियो में जोड़ने के लिए 'Finish' पर क्लिक करें।\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"शुरू करने के लिए Pro सक्रिय करें\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Pro सुविधाओं का उपयोग करने के लिए सदस्यता लें।\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Pro सदस्यता लें\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"क्या आप लोकली Screenity का इस्तेमाल कर रहे हैं? भविष्य के विकास में मदद करें!\",\n    \"description\": \"एडिटर में सेल्फ-होस्ट बैनर का शीर्षक\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity एक अकेली इंडी डेवलपर द्वारा बनाया गया है। सेल्फ-होस्टिंग पूरी तरह मुफ्त है, लेकिन अगर यह आपके काम आया हो, तो Pro के ज़रिए समर्थन देने पर विचार करें ❤️\",\n    \"description\": \"एडिटर में सेल्फ-होस्ट बैनर का विवरण\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"स्क्रीनिटी में वापस स्वागत है\",\n    \"description\": \"वापसी करने वाले उपयोगकर्ताओं को दिखाए जाने वाले वेलकम पॉपअप का शीर्षक\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ नया: अपनी रिकॉर्डिंग को अगले स्तर पर ले जाएं\",\n    \"description\": \"वेलकम पॉपअप में क्लाउड स्टोरेज फीचर का शीर्षक\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro एक वैकल्पिक नया प्लेटफ़ॉर्म है जहाँ आप अपने वीडियो क्लाउड में सेव कर सकते हैं, लिंक के ज़रिए शेयर कर सकते हैं, और ज़रूरत पड़ने पर एडवांस्ड एडिटिंग टूल्स का इस्तेमाल कर सकते हैं।\",\n    \"description\": \"वेलकम पॉपअप में क्लाउड स्टोरेज फीचर का विवरण\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Screenity Pro के बारे में और जानें\",\n    \"description\": \"वेलकम पॉपअप में क्लाउड फीचर के लिए कॉल-टू-एक्शन बटन\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Screenity Pro में आपका स्वागत है\",\n    \"description\": \"Pro वेलकम मोडल का शीर्षक\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"रिकॉर्डिंग शुरू करने के लिए सब कुछ तैयार है! कुछ बातों का ध्यान रखें:\\n\\n- रिकॉर्डिंग के दौरान कैमरा बबल गायब हो सकता है, यह सामान्य है। इसे बैकग्राउंड में अलग से कैप्चर किया जाता है ताकि आप बाद में इसे कहीं भी रख सकें।\\n- ज़ूम केवल Chrome टैब्स में क्लिक करने पर अपने आप बनते हैं — यह ब्राउज़र की सीमा है। आप रिकॉर्डिंग के बाद मैन्युअली ज़ूम जोड़ सकते हैं।\\n- त्वरित रिकॉर्डिंग और तुरंत डाउनलोड के लिए Instant Mode का उपयोग करें। ध्यान रखें कि इसमें एडिटिंग के विकल्प सीमित होते हैं: कोई बैकग्राउंड्स, लेआउट्स या एडवांस टूल्स नहीं।\",\n    \"description\": \"Pro वेलकम मोडल का विवरण\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"ठीक है\",\n    \"description\": \"Pro वेलकम मोडल का एक्शन बटन\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 विफल — ऑफ\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Screenity Pro एक्सटेंशन में आपका स्वागत है\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"रिकॉर्डिंग शुरू करने से पहले कुछ ज़रूरी टिप्स।<br/>ऑटो-ज़ूम केवल <strong>Chrome टैब्स</strong> के अंदर किए गए क्लिक पर काम करता है। बाद में आप ज़ूम मैन्युअली जोड़ सकते हैं।\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"रिकॉर्डिंग टूलबार और इफेक्ट्स\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"इस टूलबार से ड्रॉ करें, कर्सर इफेक्ट्स लगाएँ, ब्लर करें और रिकॉर्डिंग कंट्रोल करें।<br/><br/><strong>यह टूलबार वीडियो में दिखेगा</strong> अगर आप इसे छिपाएँगे नहीं।\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"कैमरा अलग से कैप्चर होता है\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"रिकॉर्डिंग के दौरान आपका कैमरा <strong>छिप सकता है या PiP में जा सकता है</strong>, यह सामान्य है।<br/><br/>इसे अलग से कैप्चर किया जाता है ताकि आप बाद में इसकी पोज़िशन बदल सकें।\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"इंस्टेंट मोड\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"तेज़ शेयरिंग और <strong>अनलिमिटेड डाउनलोड्स</strong> के लिए सबसे अच्छा।<br/><br/>इस मोड में एडवांस लेआउट और एडिटर विकल्प उपलब्ध नहीं हैं।\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"ठीक है\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"आगे\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"वापस\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"और जानें\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"ऑनबोर्डिंग रीसेट करें\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"डिस्क में कम जगह। आपकी रिकॉर्डिंग सहेजी जा रही है। अब तक कैप्चर किया गया सब कुछ सुरक्षित है।\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"लंबी रिकॉर्डिंग\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"आपकी रिकॉर्डिंग पूर्ण और सुरक्षित है। नीचे WebM के रूप में डाउनलोड करें।\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"लंबी रिकॉर्डिंग के लिए अनुपलब्ध\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"पुनर्प्राप्ति मोड में अनुपलब्ध\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"ऑडियो संपादन बहुत लंबा\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"यह ऑडियो संपादन 15 मिनट से अधिक के क्लिप के लिए समर्थित नहीं है। आपका मूल अपरिवर्तित है। पहले क्लिप को ट्रिम करने का प्रयास करें।\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"संपादन का समय समाप्त\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"संपादन समय पर पूरा नहीं हुआ। आपकी मूल रिकॉर्डिंग अपरिवर्तित है। फिर से प्रयास करें या पहले क्लिप ट्रिम करें।\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"स्क्रीन शेयरिंग बंद हो गई। आपकी रिकॉर्डिंग सहेजी गई है। जब तैयार हों तब रोकें।\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"प्रोसेसिंग में कोई समस्या थी, लेकिन आपकी रिकॉर्डिंग सुरक्षित है। आप इसे नीचे डाउनलोड कर सकते हैं।\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"आपका संपादन प्रोसेस हो रहा है। आपकी मूल रिकॉर्डिंग अपरिवर्तित है।\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"संपादन लागू नहीं हो सका\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"प्रोसेसिंग के दौरान कुछ गलत हो गया। आपकी मूल रिकॉर्डिंग सुरक्षित है। आप फिर से कोशिश कर सकते हैं या इसे वैसे ही डाउनलोड कर सकते हैं।\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"स्क्रीन शेयरिंग बंद हो गई। आपकी रिकॉर्डिंग सहेजी जा रही है। अब तक कैप्चर किया गया सब कुछ सुरक्षित है।\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"ऑडियो डिस्कनेक्ट हो गया। आपकी रिकॉर्डिंग सहेजी जा रही है। अब तक कैप्चर किया गया वीडियो सुरक्षित है।\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"आपकी रिकॉर्डिंग सहेजी गई है। एडिटर नहीं खुला। फिर से कोशिश करने के लिए Screenity आइकन पर क्लिक करें।\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"सहेजने में अपेक्षा से अधिक समय लग रहा है। आपका डेटा सुरक्षित है। एडिटर जल्द खुलेगा।\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"डीबग जानकारी कॉपी करें\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"डीबग जानकारी कॉपी हो गई\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"सहायता प्राप्त करें\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"रिकॉर्डिंग सेव करने के लिए बहुत छोटी थी\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"तेज़ रिकॉर्डर विफल हुआ\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"इस डिवाइस पर तेज़ रिकॉर्डर का आउटपुट सत्यापित नहीं हो सका।\\nआप फिर भी फ़ाइल डाउनलोड कर सकते हैं।\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"फिर भी डाउनलोड करें\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"रद्द करें\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/id/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Perekam Layar & Alat Anotasi\",\n    \"description\": \"Nama ekstensi\"\n  },\n  \"extDesc\": {\n    \"message\": \"Perekam layar terbaik gratis tanpa batasan. Tangkap, beri anotasi, zoom, kaburkan, sunting video, dan banyak lagi - tanpa perlu masuk, & ramah privasi.\",\n    \"description\": \"Deskripsi ekstensi\"\n  },\n  \"recordTab\": {\n    \"message\": \"Rekam\",\n    \"description\": \"Label tab Rekam pada popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Video Anda\",\n    \"description\": \"Label tab Video pada popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Layar\",\n    \"description\": \"Label jenis perekaman layar pada popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Area tab\",\n    \"description\": \"Label jenis perekaman area tab pada popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Kamera\",\n    \"description\": \"Label jenis perekaman kamera pada popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Label jenis perekaman mockup pada popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Perekaman area kustom dinonaktifkan\",\n    \"description\": \"Peringatan perekaman area kustom dinonaktifkan pada popup untuk versi Chrome yang lebih lama dari 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Perbarui versi Chrome ke 110+\",\n    \"description\": \"Deskripsi peringatan perekaman area kustom dinonaktifkan pada popup untuk versi Chrome yang lebih lama dari 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Perbarui\",\n    \"description\": \"Tindakan peringatan perekaman area kustom dinonaktifkan pada popup untuk versi Chrome yang lebih lama dari 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Periksa izin Anda\",\n    \"description\": \"Judul modal peringatan izin kamera/mikrofon\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Sepertinya Screenity tidak dapat mengakses kamera atau mikrofon Anda. Pastikan untuk mengizinkan akses dengan mengklik ikon kamera di bilah alamat.\",\n    \"description\": \"Deskripsi modal peringatan izin kamera/mikrofon\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Abaikan\",\n    \"description\": \"Tombol abaikan modal peringatan izin kamera/mikrofon\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Izinkan akses kamera\",\n    \"description\": \"Tombol izinkan akses kamera\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Izinkan akses mikrofon\",\n    \"description\": \"Tombol izinkan akses mikrofon\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Tekan untuk berbicara\",\n    \"description\": \"Label sakelar Tekan untuk berbicara\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Atur area untuk direkam\",\n    \"description\": \"Label sakelar area kustom\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Putar kamera\",\n    \"description\": \"Label sakelar putar kamera\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efek latar belakang\",\n    \"description\": \"Label sakelar efek latar belakang\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Mulai merekam\",\n    \"description\": \"Label tombol rekam\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Memulai merekam...\",\n    \"description\": \"Label tombol rekam dalam proses\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Atur kamera untuk merekam\",\n    \"description\": \"Label tombol rekam saat dalam mode kamera dan tidak ada kamera yang dipilih\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Tampilkan opsi lebih lanjut\",\n    \"description\": \"Label tampilkan opsi lebih lanjut\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Sertakan audio sistem\",\n    \"description\": \"Label audio sistem/tab\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Sembunyikan toolbar\",\n    \"description\": \"Label sembunyikan toolbar\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Hitung mundur\",\n    \"description\": \"Label hitung mundur\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Atur batas waktu\",\n    \"description\": \"Label alarm\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Blur\",\n    \"description\": \"Label latar belakang blur\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Tidak ada\",\n    \"description\": \"Tidak ada perangkat yang dipilih\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Tidak ada kamera\",\n    \"description\": \"Tidak ada kamera yang dipilih\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Tidak ada mikrofon\",\n    \"description\": \"Tidak ada mikrofon yang dipilih\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Pilih sumber\",\n    \"description\": \"Pilih tempat untuk sumber\"\n  },\n  \"offLabel\": {\n    \"message\": \"Mati\",\n    \"description\": \"Label Mati pada dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Lebar\",\n    \"description\": \"Label lebar daerah\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Tinggi\",\n    \"description\": \"Label tinggi daerah\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Klik untuk menempatkan gambar\",\n    \"description\": \"Pemberitahuan ketika menambahkan gambar baru\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Selesaikan perekaman\",\n    \"description\": \"Petunjuk untuk menyelesaikan perekaman\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Mulai ulang perekaman\",\n    \"description\": \"Petunjuk untuk memulai ulang perekaman\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Jeda perekaman\",\n    \"description\": \"Petunjuk untuk menjeda perekaman\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Lanjutkan perekaman\",\n    \"description\": \"Petunjuk untuk melanjutkan perekaman\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Batal perekaman\",\n    \"description\": \"Petunjuk untuk membatalkan perekaman\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Hidupkan/Matikan alat gambar\",\n    \"description\": \"Petunjuk untuk mengaktifkan/menonaktifkan alat gambar\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Hidupkan/Matikan alat blur\",\n    \"description\": \"Petunjuk untuk mengaktifkan/menonaktifkan alat blur\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Hidupkan/Matikan opsi kursor\",\n    \"description\": \"Petunjuk untuk mengaktifkan/menonaktifkan opsi kursor\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Matikan kamera\",\n    \"description\": \"Petunjuk untuk mematikan kamera\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Hidupkan kamera\",\n    \"description\": \"Petunjuk untuk menghidupkan kamera\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Tidak ada izin kamera\",\n    \"description\": \"Petunjuk untuk tidak ada izin kamera\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Matikan mikrofon\",\n    \"description\": \"Petunjuk untuk mematikan mikrofon\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Hidupkan mikrofon\",\n    \"description\": \"Petunjuk untuk menghidupkan mikrofon\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Tidak ada izin mikrofon\",\n    \"description\": \"Petunjuk untuk tidak ada izin mikrofon\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Pilih alat\",\n    \"description\": \"Petunjuk untuk memilih alat\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Alat pena\",\n    \"description\": \"Petunjuk untuk alat pena\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Alat penyorot\",\n    \"description\": \"Petunjuk untuk alat penyorot\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Alat penghapus\",\n    \"description\": \"Petunjuk untuk alat penghapus\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Alat teks\",\n    \"description\": \"Petunjuk untuk alat teks\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Alat bentuk\",\n    \"description\": \"Petunjuk untuk alat bentuk\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Alat panah\",\n    \"description\": \"Petunjuk untuk alat panah\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Alat gambar\",\n    \"description\": \"Petunjuk untuk alat gambar\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Batalkan\",\n    \"description\": \"Petunjuk untuk membatalkan\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Ulangi\",\n    \"description\": \"Petunjuk untuk mengulangi\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Hapus kanvas\",\n    \"description\": \"Petunjuk untuk menghapus kanvas\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Warna lebih banyak\",\n    \"description\": \"Petunjuk untuk warna lebih banyak\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Goresan tebal\",\n    \"description\": \"Petunjuk untuk goresan tebal\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Goresan sedang\",\n    \"description\": \"Petunjuk untuk goresan sedang\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Goresan tipis\",\n    \"description\": \"Petunjuk untuk goresan tipis\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Hidupkan/Matikan mode gambar dalam gambar\",\n    \"description\": \"Petunjuk untuk mengaktifkan/menonaktifkan mode gambar dalam gambar\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Hidupkan/Matikan isian\",\n    \"description\": \"Petunjuk untuk mengaktifkan/menonaktifkan isian\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Mode menggambar\",\n    \"description\": \"Pemberitahuan tentang mode menggambar\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Mode blur\",\n    \"description\": \"Pemberitahuan tentang mode blur\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Hapus semua elemen yang diblur\",\n    \"description\": \"Petunjuk untuk menghapus semua elemen yang diblur\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Sorot klik\",\n    \"description\": \"Petunjuk untuk sorotan klik\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Sorotan kursor\",\n    \"description\": \"Petunjuk untuk sorotan kursor\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Kursor Spotlight\",\n    \"description\": \"Tooltip Kursor Spotlight\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Jangan tampilkan lagi\",\n    \"description\": \"Tombol tolak modal Izin\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Apakah Anda yakin ingin memulai ulang perekaman?\",\n    \"description\": \"Judul modal Memulai Ulang Perekaman\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Progress video Anda saat ini akan hilang.\",\n    \"description\": \"Deskripsi modal Memulai Ulang Perekaman\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Memulai ulang perekaman\",\n    \"description\": \"Tombol memulai ulang modal Memulai Ulang Perekaman\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Lanjutkan perekaman\",\n    \"description\": \"Tombol lanjutkan modal Memulai Ulang Perekaman\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Apakah Anda yakin ingin membuang perekaman?\",\n    \"description\": \"Judul modal Membuang Perekaman\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Progress video Anda saat ini akan hilang.\",\n    \"description\": \"Deskripsi modal Membuang Perekaman\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Membuang perekaman\",\n    \"description\": \"Tombol membuang modal Membuang Perekaman\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Lanjutkan perekaman\",\n    \"description\": \"Tombol lanjutkan modal Membuang Perekaman\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Pilih apa yang ingin Anda rekam\",\n    \"description\": \"Judul halaman perekaman\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Merekam...\",\n    \"description\": \"Judul halaman perekaman saat merekam\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Simpan tab ini terbuka. Ini akan ditutup setelah perekaman Anda disimpan.\",\n    \"description\": \"Deskripsi halaman perekaman\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Mempersiapkan perekaman...\",\n    \"description\": \"Judul halaman sandbox saat merekam / menyimpan\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Simpan tab ini terbuka. Ini akan diperbarui dengan perekaman Anda saat sudah siap.\",\n    \"description\": \"Deskripsi halaman sandbox saat merekam / menyimpan\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Judul navigasi halaman editor sandbox\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Batal\",\n    \"description\": \"Tombol batal di halaman editor sandbox\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Kembalikan ke asli\",\n    \"description\": \"Tombol kembali di halaman editor sandbox\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Atur ulang\",\n    \"description\": \"Tombol atur ulang di halaman editor sandbox\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Simpan perubahan\",\n    \"description\": \"Tombol simpan di halaman editor sandbox\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Menyimpan...\",\n    \"description\": \"Tombol simpan di halaman editor sandbox saat menyimpan\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Seret dan lepaskan file audio Anda\",\n    \"description\": \"Seret dan lepaskan file audio di halaman editor sandbox\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Atau klik untuk mencari\",\n    \"description\": \"Atau klik untuk mencari file audio di halaman editor sandbox\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Pengaturan\",\n    \"description\": \"Judul pengaturan audio di halaman editor sandbox\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Label volume audio di halaman editor sandbox\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Perbarui\",\n    \"description\": \"Tombol perbarui audio di halaman editor sandbox\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Potong\",\n    \"description\": \"Judul potong di halaman editor sandbox\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Lebar\",\n    \"description\": \"Label lebar untuk properti\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Tinggi\",\n    \"description\": \"Label tinggi untuk properti\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Kiri\",\n    \"description\": \"Label kiri untuk properti\"\n  },\n  \"topLabel\": {\n    \"message\": \"Atas\",\n    \"description\": \"Label atas untuk properti\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Seret pegangan dan gunakan tombol di sebelah kiri untuk mengedit bagian yang dipilih.\",\n    \"description\": \"Info tentang pemangkasan di halaman editor sandbox\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Potong video\",\n    \"description\": \"Tombol potong di halaman editor sandbox\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Memotong...\",\n    \"description\": \"Tombol potong di halaman editor sandbox saat memotong\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Potong bagian\",\n    \"description\": \"Tombol potong bagian di halaman editor sandbox\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Memotong...\",\n    \"description\": \"Tombol potong bagian di halaman editor sandbox saat memotong\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Bisukan audio\",\n    \"description\": \"Tombol bisu di halaman editor sandbox\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Mematikan...\",\n    \"description\": \"Tombol bisu di halaman editor sandbox saat mematikan\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Pelajari lebih lanjut.\",\n    \"description\": \"Pelajari lebih lanjut dengan titik\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Urungkan\",\n    \"description\": \"Label urungkan\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Ulangi\",\n    \"description\": \"Label ulangi\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Tinggalkan ulasan\",\n    \"description\": \"Tombol tinggalkan ulasan\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Ikuti untuk pembaruan\",\n    \"description\": \"Tombol Ikuti untuk pembaruan\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Anda saat ini offline\",\n    \"description\": \"Label Offline\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Beberapa fitur tidak tersedia sampai Anda terhubung kembali\",\n    \"description\": \"Deskripsi label Offline\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Coba lagi\",\n    \"description\": \"Tombol Coba lagi label Offline\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome perlu diperbarui\",\n    \"description\": \"Label Perbarui Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Perbarui untuk mengakses lebih banyak fitur\",\n    \"description\": \"Deskripsi label Perbarui Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Pelajari lebih lanjut\",\n    \"description\": \"Tombol Pelajari lebih lanjut\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video terlalu panjang untuk diproses secara lokal\",\n    \"description\": \"Label batas lebih dari 5 menit dalam editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Pengeditan dan ekspor ke format MP4 tidak tersedia\",\n    \"description\": \"Deskripsi batas lebih dari 5 menit dalam editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Video sedang diproses...\",\n    \"description\": \"Label Proses video di editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Upgrade untuk pengeditan yang lebih cepat\",\n    \"description\": \"Deskripsi Proses video di editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Edit\",\n    \"description\": \"Judul Edit di halaman editor Sandbox\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Tidak ada koneksi\",\n    \"description\": \"Label Tidak ada koneksi\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Tidak tersedia\",\n    \"description\": \"Label Tidak tersedia\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Edit video\",\n    \"description\": \"Tombol Potong di editor\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Potong, potong, atau matikan suara video\",\n    \"description\": \"Label Potong\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Menyiapkan...\",\n    \"description\": \"Label Persiapan\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Potong\",\n    \"description\": \"Tombol Potong label\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Potong dan ubah ukuran video\",\n    \"description\": \"Label Potong\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Tambahkan audio\",\n    \"description\": \"Tombol Tambahkan audio label\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Unggah audio Anda sendiri untuk ditambahkan ke video\",\n    \"description\": \"Label Tambahkan audio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Simpan\",\n    \"description\": \"Judul Simpan di halaman editor Sandbox\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Keluar dari Drive\",\n    \"description\": \"Label Keluar dari Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Menyimpan...\",\n    \"description\": \"Label Menyimpan ke Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Simpan ke Drive\",\n    \"description\": \"Tombol Simpan ke Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Simpan versi saat ini ke Google Drive\",\n    \"description\": \"Label Simpan ke Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Masuk dan simpan ke Drive\",\n    \"description\": \"Masuk ke Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Ekspor\",\n    \"description\": \"Judul Ekspor di halaman editor Sandbox\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Mengunduh...\",\n    \"description\": \"Label Mengunduh\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Unduh sebagai .webm\",\n    \"description\": \"Tombol Unduh WEBM label\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Ekspor video .webm\",\n    \"description\": \"Label Unduh WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Unduh sebagai .mp4\",\n    \"description\": \"Tombol Unduh MP4 label\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Ekspor video .mp4 (direkomendasikan)\",\n    \"description\": \"Label Unduh MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Unduh sebagai .gif\",\n    \"description\": \"Tombol Unduh GIF label\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Ekspor GIF (maksimal 30 detik)\",\n    \"description\": \"Label Unduh GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Siap tingkatkan rekamanmu?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Bagikan ke cloud, gunakan zoom, template... dan dukung alat ramah privasi buatan seorang developer indie! ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Pelajari tentang Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Sudah punya akun? Masuk\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Bagikan video\",\n    \"description\": \"Tombol Bagikan di halaman editor Sandbox\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Ganti audio yang ada\",\n    \"description\": \"Tombol Ganti audio di editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Perbesar ke kursor\",\n    \"description\": \"Popup Perbesar ke titik\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Tetap di halaman saat merekam\",\n    \"description\": \"Popup Tetap di halaman\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Peringatan mikrofon mati\",\n    \"description\": \"Popup Pengingat mikrofon\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Bantuan\",\n    \"description\": \"Tombol Popup Bantuan\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Perekaman dijeda. Tekan tombol putar untuk melanjutkan.\",\n    \"description\": \"Judul modal Perekaman dijeda\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity tidak memiliki izin untuk merekam layar Anda\",\n    \"description\": \"Judul modal Izin\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Untuk mengaktifkan perekaman layar, Anda harus memberikan izin kepada Screenity untuk menangkap layar Anda ketika diminta.\",\n    \"description\": \"Deskripsi modal Izin\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Aktifkan perekaman layar\",\n    \"description\": \"Aksi modal Izin\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Batal\",\n    \"description\": \"Batal modal Izin\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Mikrofon Anda dimatikan\",\n    \"description\": \"Judul modal Mikrofon dimatikan\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Untuk menyertakan audio dalam perekaman Anda, Anda perlu mengaktifkan kembali mikrofon Anda. Apakah Anda ingin melanjutkan tanpa audio?\",\n    \"description\": \"Deskripsi modal Mikrofon dimatikan\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Ya, lanjutkan\",\n    \"description\": \"Lanjutkan modal Mikrofon dimatikan\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Batal\",\n    \"description\": \"Batal modal Mikrofon dimatikan\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Mulailah dengan Screenity dalam tiga langkah sederhana:\",\n    \"description\": \"Judul langkah pengaturan\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Klik ikon \",\n    \"description\": \"Langkah pengaturan 1, sebelum ikon. Diperlukan spasi di akhir.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" ekstensi\",\n    \"description\": \"Langkah pengaturan 1, setelah ikon. Diperlukan spasi di awal.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Tekan ikon \",\n    \"description\": \"Langkah pengaturan 2, sebelum ikon. Diperlukan spasi di akhir.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" pin\",\n    \"description\": \"Langkah pengaturan 2, setelah ikon. Diperlukan spasi di awal.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Klik ikon \",\n    \"description\": \"Langkah pengaturan 3, sebelum ikon. Diperlukan spasi di akhir.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity untuk memulai\",\n    \"description\": \"Langkah pengaturan 3, setelah ikon. Diperlukan spasi di awal.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Keren! Anda sudah siap\",\n    \"description\": \"Judul pengaturan selesai\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Anda sekarang dapat mulai merekam di sini atau di tab lain.\",\n    \"description\": \"Deskripsi pengaturan selesai\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Klik di sini untuk menggambar\",\n    \"description\": \"Panduan Klik di sini\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Tenang. Klik di mana saja untuk menghentikan hitungan mundur.\",\n    \"description\": \"Pesan hitungan mundur\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Edit mungkin tidak akurat untuk rentang waktu singkat karena interval antara bingkai.\",\n    \"description\": \"Info tentang editor yang terlalu kecil\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Video sedang diproses, pemutaran mungkin tidak akurat atau terputus.\",\n    \"description\": \"Banner pemrosesan di editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Penggunaan alat potong mungkin memerlukan waktu\",\n    \"description\": \"Judul info potong\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Upgrade untuk pemrosesan yang lebih cepat\",\n    \"description\": \"Deskripsi info potong\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Mikrofon diaktifkan\",\n    \"description\": \"Judul toast Mikrofon diaktifkan\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Mikrofon dimatikan\",\n    \"description\": \"Judul toast Mikrofon dimatikan\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Sembunyikan notifikasi UI\",\n    \"description\": \"Tombol Sembunyikan notifikasi UI\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Jangan tampilkan lagi\",\n    \"description\": \"Tombol Jangan tampilkan lagi\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Sembunyikan toolbar saat tidak digunakan\",\n    \"description\": \"Tombol Hanya tampilkan toolbar saat dihover\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Hentikan perekaman\",\n    \"description\": \"Tombol Hentikan perekaman di layar perekaman\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Selamat datang di Screenity yang baru!\",\n    \"description\": \"Judul pengumuman pembaruan untuk pengguna yang sudah ada\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Kami memperkenalkan tampilan baru dan banyak fitur menarik, tetapi jangan khawatir, ini masih merupakan perluasan gratis, pribadi, dan open source yang sama yang Anda kenal dan cintai.\",\n    \"description\": \"Deskripsi pengumuman pembaruan untuk pengguna yang sudah ada\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Pelajari lebih lanjut tentang pembaruan.\",\n    \"description\": \"Tautan pelajari lebih lanjut pengumuman pembaruan untuk pengguna yang sudah ada\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Mulai sekarang\",\n    \"description\": \"Tombol pengumuman pembaruan untuk pengguna yang sudah ada\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Kesalahan saat memulai perekaman\",\n    \"description\": \"Judul modal kesalahan stream\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Sepertinya ada kesalahan saat memulai perekaman. Restart browser Anda dan coba lagi. Jika masalah masih berlanjut, silakan hubungi kami di support@screenity.io.\",\n    \"description\": \"Deskripsi modal kesalahan stream\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Kualitas Tertinggi\",\n    \"description\": \"Label untuk mengganti kualitas tertinggi di popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Kembalikan Rekaman Terakhir\",\n    \"description\": \"Tombol untuk mengembalikan rekaman terakhir di popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Mengalami masalah?\",\n    \"description\": \"Tombol masalah di editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Tidak dapat melihat rekaman Anda?\",\n    \"description\": \"Judul modal masalah di editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Jika Anda telah menyelesaikan rekaman Anda dan mendapati diri Anda berada di halaman ini tanpa melihat video Anda, Anda dapat mencoba mengunduh data video mentah jika tersedia. Anda juga dapat menghubungi kami dan mengirimkan laporan bug.\",\n    \"description\": \"Deskripsi modal masalah di editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Unduh data video mentah\",\n    \"description\": \"Tombol modal masalah di editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Laporkan bug\",\n    \"description\": \"Tombol kedua dalam modal masalah di editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Tidak ada rekaman yang ditemukan, maaf :(\",\n    \"description\": \"Peringatan tidak ada rekaman ditemukan di editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Batas Memori Tercapai\",\n    \"description\": \"Judul Batas Memori Tercapai\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Perangkat Anda telah kehabisan memori untuk merekam, sehingga perekaman telah berhenti secara otomatis. Jika Anda ingin merekam lebih lama, Anda dapat mencoba mengurangi kualitas video atau membersihkan beberapa ruang pada perangkat Anda.\",\n    \"description\": \"Deskripsi Batas Memori Tercapai\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Dimengerti\",\n    \"description\": \"Tombol Dimengerti\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Ruang Tidak Cukup\",\n    \"description\": \"Judul Ruang Tidak Cukup\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity memerlukan setidaknya 1 GB ruang kosong untuk merekam. Silakan mengosongkan beberapa ruang pada perangkat Anda dan coba lagi.\",\n    \"description\": \"Deskripsi Ruang Tidak Cukup\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Hapus Rekaman Sebelumnya\",\n    \"description\": \"Tombol Hapus Rekaman Sebelumnya\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Lanjutan\",\n    \"description\": \"Bagian Lanjutan pada halaman editor sandbox\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Unduh berkas video mentah\",\n    \"description\": \"Tombol Unduh berkas perekaman mentah\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Ekspor data perekaman asli\",\n    \"description\": \"Label Unduh berkas perekaman mentah\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Dapatkan bantuan untuk perekaman Anda\",\n    \"description\": \"Tombol Penyelesaian Masalah\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Pulihkan video Anda dan selesaikan masalah lainnya\",\n    \"description\": \"Label Penyelesaian Masalah\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Unduh berkas video mentah\",\n    \"description\": \"Judul modal berkas perekaman mentah\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Berkas video mentah adalah berkas video asli yang direkam oleh Screenity. Video ini tidak diproses atau diedit, sehingga mungkin sangat besar dan mungkin tidak dapat diputar di semua perangkat. Jika Anda mengalami masalah dengan video yang diproses, Anda dapat menggunakan berkas ini untuk memulihkan video Anda.\",\n    \"description\": \"Deskripsi modal berkas perekaman mentah\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Unduh\",\n    \"description\": \"Tombol modal berkas perekaman mentah\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Unduh informasi sistem dan data video untuk penyelesaian masalah\",\n    \"description\": \"Judul modal Penyelesaian Masalah\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Setelah mengunduh, kirim berkas ini ke support@screenity.io dengan informasi relevan. Kami akan menggunakan data ini untuk membantu Anda menyelesaikan masalah dengan ekstensi dan memulihkan video Anda jika memungkinkan.\",\n    \"description\": \"Deskripsi modal Penyelesaian Masalah\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Unduh\",\n    \"description\": \"Tombol modal Penyelesaian Masalah\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Video terlalu panjang untuk diproses di perangkat Anda\",\n    \"description\": \"Judul modal batas lebih dari 5 menit\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Karena Screenity sepenuhnya privat dan berjalan secara lokal, ia dibatasi oleh perangkat keras perangkat Anda. Memproses video sepanjang ini akan memakan waktu terlalu lama dan sangat intensif dalam penggunaan sumber daya. Jangan khawatir, Anda masih dapat mengunduh file WEBM atau rekaman mentah, tetapi pengeditan dan ekspor ke MP4 tidak tersedia. Namun demikian, Anda masih dapat mencoba memproses video ini jika Anda memahami risikonya.\",\n    \"description\": \"Deskripsi modal batas lebih dari 5 menit\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Saya mengerti risikonya, coba saja\",\n    \"description\": \"Tombol modal batas lebih dari 5 menit\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Pelajari lebih lanjut tentang pengolahan video panjang.\",\n    \"description\": \"Tautan 'Pelajari lebih lanjut' dalam modal batas lebih dari 5 menit dengan titik\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Mode Pemulihan\",\n    \"description\": \"Judul Mode Pemulihan\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Unduh data untuk perbaikan masalah\",\n    \"description\": \"Pilihan unduhan untuk perbaikan masalah\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Pusat Bantuan\",\n    \"description\": \"Elemen Navigasi Bantuan\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Merekam Audio dari Halaman\",\n    \"description\": \"Judul Peringatan Audio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Untuk merekam audio dari halaman ini, Anda harus menggunakan opsi '$tab$'.\",\n    \"description\": \"Deskripsi Peringatan Audio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Area Tab\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Ekstensi Tidak Didukung di Halaman\",\n    \"description\": \"Judul Ekstensi Tidak Didukung di Tab\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity tidak mendukung di tab sebelumnya Anda. Meskipun begitu, Anda masih bisa memulai perekaman di sini dan kemudian beralih ke tab lain, tetapi kamera dan toolbar Anda tidak akan terlihat.\",\n    \"description\": \"Deskripsi Ekstensi Tidak Didukung di Tab\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Konfigurasi Cadangan Lokal untuk Rekaman Screenity Anda\",\n    \"description\": \"Judul Cadangan\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Simpan salinan dari semua rekaman yang Anda buat. Jika Anda tidak mengatur cadangan, rekaman Anda hanya akan disimpan saat Anda memutuskan untuk mengunduhnya.\",\n    \"description\": \"Deskripsi Cadangan\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Anda mungkin perlu membuat folder baru terlebih dahulu jika Anda mencoba menyimpannya di folder seperti 'Unduhan' atau folder sistem lainnya.\",\n    \"description\": \"Deskripsi Cadangan\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Pilih Folder\",\n    \"description\": \"Tombol Pilih Folder\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Tidak Sekarang\",\n    \"description\": \"Tombol Tidak Sekarang\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Rekaman Anda Sedang Disinkronkan Secara Lokal\",\n    \"description\": \"Judul Cadangan Aktif\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Untuk menghindari permintaan izin setiap kali Anda merekam, kami sarankan Anda menjaga tab ini tetap terbuka.\",\n    \"description\": \"Deskripsi Cadangan Aktif\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Tutup Tab Anyway\",\n    \"description\": \"Tombol Tutup Tab Anyway\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Hentikan Semua Cadangan Rekaman\",\n    \"description\": \"Tombol Hentikan Cadangan\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Mohon Konfirmasi Kembali Akses ke Folder Cadangan Lokal Screenity Anda\",\n    \"description\": \"Judul Konfirmasi Cadangan\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Maaf, tetapi kami perlu izin Anda sekali lagi untuk terus membuat salinan lokal dari rekaman Anda. Sayangnya, kami akan perlu bertanya setiap kali Anda menutup tab ini, yang bisa terjadi jika Anda menutup browser atau me-restart komputer Anda.\",\n    \"description\": \"Deskripsi Konfirmasi Cadangan\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Izinkan Screenity untuk Terus Membuat Cadangan Rekaman\",\n    \"description\": \"Tombol Izinkan dalam Konfirmasi Cadangan\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Membuat Cadangan Rekaman\",\n    \"description\": \"Label Aktifkan/Nonaktifkan Cadangan\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Izin Tidak Diberikan untuk Membuat Cadangan Rekaman\",\n    \"description\": \"Judul Gagal Izin Cadangan\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Anda tidak memilih folder untuk membuat salinan lokal dari rekaman Anda. Bergantung pada versi browser atau sistem operasi Anda, Anda mungkin diminta untuk memilih folder setiap kali Anda mulai merekam. Anda dapat mencoba lagi atau menonaktifkan cadangan sepenuhnya jika Anda tidak ingin memberikan izin berkali-kali.\",\n    \"description\": \"Deskripsi Gagal Izin Cadangan\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Merekam Audio dari Komputer\",\n    \"description\": \"Judul Peringatan Audio untuk macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"Di macOS, Anda hanya dapat merekam audio dari tab. Pilih opsi 'Tab Chrome' dan pastikan 'Bagikan juga audio tab' diaktifkan.\",\n    \"description\": \"Deskripsi Peringatan Audio untuk macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Merekam Audio dari Komputer\",\n    \"description\": \"Judul Peringatan Audio untuk Sistem Lainnya\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Pastikan Anda mengaktifkan opsi 'Bagikan audio sistem' jika Anda ingin merekam audio dari komputer. Jika Anda tidak melihat opsi ini, coba perbarui Chrome atau beralih ke opsi merekam tab.\",\n    \"description\": \"Deskripsi Peringatan Audio untuk Sistem Lainnya\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Resolusi Maksimum\",\n    \"description\": \"Label resolusi maksimum dalam menu pengaturan\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"FPS Maksimum\",\n    \"description\": \"Label FPS maksimum dalam menu pengaturan\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Tidak cukup RAM\",\n    \"description\": \"Label 'Tidak cukup RAM' dalam menu pengaturan\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Ubah Ukuran Jendela\",\n    \"description\": \"Label Ubah Ukuran Jendela di menu pengaturan\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Layar terlalu kecil untuk resolusi ini\",\n    \"description\": \"Tooltip Layar terlalu kecil di menu pengaturan\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Tidak dapat merekam pada resolusi ini di perangkat Anda\",\n    \"description\": \"Tooltip Resolusi maksimum di menu pengaturan\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Ulasan Izin\",\n    \"description\": \"Tombol Ulasan Izin\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Tambah rekaman lain\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Sedang menyiapkan...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Tunggu sebentar! Rekamanmu sedang disiapkan.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Toolbar disembunyikan. Aktifkan kembali di pengaturan popup.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Masuk atau daftar\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Keluar\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Kamu telah keluar\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Batas penyimpanan tercapai\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Kamu telah menggunakan seluruh 100 GB penyimpanan. Hapus video atau adegan lama untuk melanjutkan merekam.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Kelola penyimpanan\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Tutup\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Gagal memeriksa penyimpanan\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Terjadi masalah saat memeriksa penyimpanan. Silakan masuk kembali dan coba rekam lagi.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Kami tidak dapat memverifikasi kuota penyimpananmu. Coba lagi beberapa detik lagi atau muat ulang halaman.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Coba lagi\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Adegan ditambahkan ke multi-recording\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Siap merekam adegan baru dalam videomu\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Rekaman mendekati batas 90 menit, akan segera berhenti\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Rekaman dihentikan karena batas 90 menit\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Rekaman tab dinonaktifkan di halaman ini. Beralih ke rekaman layar.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Rekaman video dibatalkan\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Tautan disalin ke clipboard\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Gagal menyalin tautan\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Terbaru\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Terlama\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Semua video\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Tidak ada video ditemukan\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Memuat video...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Ke dashboard\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Selamat datang di Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Tanpa iklan. Tanpa pelacakan. Tanpa batas.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Langsung rekam.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Gunakan gratis — tanpa perlu daftar!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Ingin melakukan lebih banyak dengan videomu?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Penyimpanan cloud, bagikan video dengan tautan\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Gabungkan beberapa klip jadi satu cerita\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zoom pintar saat klik (atau tambahkan sendiri!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Tambahkan template, teks, tata letak...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Latar belakang dan mockup khusus\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Buka fitur tambahan\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Dukung pengembangan oleh pembuat indie tunggal!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Pengaturan akun\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Dukungan\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Langganan Pro kamu tidak aktif\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Langganan Screenity Pro kamu tidak aktif.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Aktifkan kembali untuk melanjutkan akses.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Video dan datamu akan dihapus secara permanen pada \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Kelola langgananmu\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Ingin tetap menggunakan versi gratis?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Keluar dan turunkan versi\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Kamu telah keluar\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Untuk menyinkronkan rekamanmu dan mengakses fitur premium, kamu perlu masuk kembali.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Masuk kembali\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Kamu tetap bisa menggunakan ekstensi tanpa akun - tapi rekamanmu tidak akan disimpan dan tidak bisa dipulihkan.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Lanjut tanpa login\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Mode rekam instan\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Unduh instan, tapi kamera dan tata letak tidak bisa diedit nanti.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Mode rekam instan\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Semua direkam dalam satu video untuk unduhan dan berbagi instan. Tata letak kamera tidak bisa diubah nanti, tapi pengeditan lainnya tetap bisa.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Mengerti\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Rekaman tab dinonaktifkan di halaman ini\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Menambahkan ke: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Selesai\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Ingin melakukan lebih banyak dengan rekamanmu?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Masuk untuk menyimpan video ke cloud, berbagi dengan tautan, dan akses fitur editing lanjutan.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Masuk untuk membuka fitur berbayar\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Dukung pengembangan oleh pembuat indie tunggal\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Buka lebih banyak fitur\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Masuk untuk berbagi (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Buka berbagi cloud, editing multi-adegan, zoom otomatis, teks & lainnya\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity akan selalu gratis, open-source, dan tanpa iklan. Pro membantu menutupi biaya cloud & infrastruktur, serta mendukung pengembangan oleh developer indie tunggal! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Jangan tampilkan lagi\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Coba sekarang\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Mode Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Rekam beberapa adegan—layar, kamera, atau keduanya—secara berurutan. Cocok untuk mengambil beberapa ulangan, mengganti tampilan, atau membagi rekaman menjadi beberapa bagian. Setelah selesai, klik Selesai untuk membuka editor dengan semua adegan digabungkan dalam satu video.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Aktifkan Pro untuk memulai\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Berlangganan untuk mengakses fitur Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Langganan Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Menikmati Screenity secara lokal? Bantu dukung pengembangannya!\",\n    \"description\": \"Judul banner self-host di editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity dibuat oleh seorang pengembang indie perempuan. Hosting sendiri gratis, tapi jika kamu merasa terbantu, pertimbangkan untuk mendukung lewat Pro ❤️\",\n    \"description\": \"Deskripsi banner self-host di editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Selamat datang kembali di Screenity\",\n    \"description\": \"Judul pop-up sambutan yang ditampilkan untuk pengguna yang kembali\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Baru: Tingkatkan rekamanmu ke level berikutnya\",\n    \"description\": \"Judul fitur penyimpanan cloud di pop-up sambutan\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro adalah platform opsional baru untuk menyimpan video ke cloud, membagikannya lewat tautan, dan membuka alat pengeditan lanjutan — jika dan saat kamu membutuhkannya.\",\n    \"description\": \"Deskripsi fitur penyimpanan cloud di pop-up sambutan\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Pelajari lebih lanjut tentang Screenity Pro\",\n    \"description\": \"Tombol ajakan bertindak untuk fitur penyimpanan cloud di pop-up sambutan\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Selamat datang di Screenity Pro\",\n    \"description\": \"Judul modal sambutan Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Kamu siap untuk mulai merekam! Beberapa hal yang perlu diingat:\\n\\n- Gelembung kamera mungkin menghilang saat merekam, ini normal. Kamera direkam secara terpisah di latar belakang, jadi kamu bisa memposisikannya sesuka hati nanti.\\n- Zoom hanya dibuat otomatis saat kamu mengeklik di tab Chrome — ini adalah batasan dari browser. Kamu tetap bisa menambahkan zoom secara manual setelah merekam.\\n- Untuk rekaman cepat dengan unduhan instan, coba Mode Instan. Tapi ingat, pilihan pengeditannya terbatas: tidak ada latar belakang, tata letak, atau penyesuaian lanjutan.\",\n    \"description\": \"Deskripsi modal sambutan Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Mengerti\",\n    \"description\": \"Tombol aksi di modal sambutan Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 gagal — mati\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Selamat datang di ekstensi Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Beberapa tips cepat sebelum mulai merekam.<br/>Auto-zoom hanya bekerja untuk klik di dalam <strong>tab Chrome</strong>. Kamu tetap bisa menambahkan zoom secara manual nanti.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Toolbar rekaman & efek\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Gunakan toolbar ini untuk menggambar, efek kursor, blur, dan kontrol rekaman.<br/><br/><strong>Toolbar ini ikut terekam di video</strong> jika tidak disembunyikan.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Kamera direkam secara terpisah\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Saat merekam, kamera bisa <strong>sembunyi atau pindah ke PiP</strong>, itu normal.<br/><br/>Kamera direkam terpisah supaya bisa kamu atur posisinya nanti.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Mode instan\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Cocok untuk berbagi cepat dengan <strong>unduhan tanpa batas</strong>.<br/><br/>Opsi tata letak dan editor lanjutan tidak tersedia di mode ini.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Mengerti\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Lanjut\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Kembali\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Pelajari lebih lanjut\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Reset onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Ruang disk rendah. Menyimpan rekaman Anda. Semua yang direkam sejauh ini aman.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Rekaman panjang\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Rekaman Anda lengkap dan aman. Unduh sebagai WebM di bawah.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Tidak tersedia untuk rekaman panjang\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Tidak tersedia dalam mode pemulihan\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Pengeditan audio terlalu panjang\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Pengeditan audio ini tidak didukung untuk klip lebih dari 15 menit. File asli Anda tidak berubah. Coba potong klip terlebih dahulu.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Pengeditan habis waktu\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Pengeditan tidak selesai tepat waktu. Rekaman asli Anda tidak berubah. Coba lagi atau potong klip terlebih dahulu.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Berbagi layar berhenti. Rekaman Anda tersimpan. Hentikan saat Anda siap.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Terjadi masalah pemrosesan, tetapi rekaman Anda aman. Anda dapat mengunduhnya di bawah.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Memproses pengeditan Anda. Rekaman asli tidak berubah.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Pengeditan tidak dapat diterapkan\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Terjadi kesalahan saat pemrosesan. Rekaman asli Anda aman. Anda dapat mencoba lagi atau mengunduhnya apa adanya.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Berbagi layar berhenti. Menyimpan rekaman Anda. Semua yang direkam sejauh ini aman.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio terputus. Menyimpan rekaman Anda. Video yang direkam sejauh ini aman.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Rekaman Anda tersimpan. Editor tidak terbuka. Klik ikon Screenity untuk mencoba lagi.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Penyimpanan memakan waktu lebih lama dari yang diharapkan. Data Anda aman. Editor akan segera terbuka.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Salin info debug\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info debug disalin\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Dapatkan bantuan\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Rekaman terlalu singkat untuk disimpan\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Perekam cepat gagal\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"Output perekam cepat tidak dapat divalidasi di perangkat ini.\\nAnda tetap dapat mengunduh filenya.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Unduh saja\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Batal\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/it/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Registra lo schermo e annota\",\n    \"description\": \"Nome dell'estensione\"\n  },\n  \"extDesc\": {\n    \"message\": \"La migliore registrazione dello schermo gratuita per Chrome senza limiti. Cattura, disegna, ingrandisci, sfoca, modifica video e altro ancora, senza la necessità di registrazione e completamente privata.\",\n    \"description\": \"Descrizione dell'estensione\"\n  },\n  \"recordTab\": {\n    \"message\": \"Registra\",\n    \"description\": \"Etichetta della scheda Registra nel menu a discesa\"\n  },\n  \"videosTab\": {\n    \"message\": \"Tus videos\",\n    \"description\": \"Etichetta della scheda I tuoi video nel menu a discesa\"\n  },\n  \"screenType\": {\n    \"message\": \"Schermo\",\n    \"description\": \"Etichetta del tipo di registrazione dello schermo nel menu a discesa\"\n  },\n  \"tabType\": {\n    \"message\": \"Area\",\n    \"description\": \"Etichetta del tipo di registrazione dell'area della scheda nel menu a discesa\"\n  },\n  \"cameraType\": {\n    \"message\": \"Fotocamera\",\n    \"description\": \"Etichetta del tipo di registrazione della fotocamera nel menu a discesa\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etichetta del tipo di registrazione del mockup nel menu a discesa\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Registrazione area personalizzata disabilitata\",\n    \"description\": \"Titolo del messaggio di avviso per la registrazione dell'area personalizzata disabilitata nelle versioni di Chrome precedenti alla 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Aggiorna Chrome alla versione 110+\",\n    \"description\": \"Descrizione del messaggio di avviso per la registrazione dell'area personalizzata disabilitata nelle versioni di Chrome precedenti alla 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Aggiorna\",\n    \"description\": \"Azione del messaggio di avviso per la registrazione dell'area personalizzata disabilitata nelle versioni di Chrome precedenti alla 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Verifica i tuoi permessi\",\n    \"description\": \"Titolo del modulo di avviso dei permessi per fotocamera/microfono\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Sembra che Screenity non possa accedere alla tua fotocamera o al tuo microfono. Consenti l'accesso facendo clic sull'icona della fotocamera nella barra degli indirizzi.\",\n    \"description\": \"Descrizione del modulo di avviso dei permessi per fotocamera/microfono\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Pulsante di annullamento del modulo di avviso dei permessi per fotocamera/microfono\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Consenti accesso alla fotocamera\",\n    \"description\": \"Pulsante per consentire l'accesso alla fotocamera\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Consenti accesso al microfono\",\n    \"description\": \"Pulsante per consentire l'accesso al microfono\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Premi per parlare\",\n    \"description\": \"Etichetta dell'interruttore di premi per parlare\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Imposta un'area di registrazione\",\n    \"description\": \"Etichetta dell'interruttore dell'area personalizzata\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Rovescia la fotocamera\",\n    \"description\": \"Etichetta dell'interruttore per rovesciare la fotocamera\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Effetti di sfondo\",\n    \"description\": \"Etichetta dell'interruttore per gli effetti di sfondo\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Avvia la registrazione\",\n    \"description\": \"Etichetta del pulsante di registrazione\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Seleziona una fotocamera per registrare\",\n    \"description\": \"Etichetta del pulsante di registrazione quando è in modalità fotocamera e non è stata selezionata alcuna fotocamera\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Registrazione in corso...\",\n    \"description\": \"Etichetta del pulsante di registrazione in corso\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Mostra più opzioni\",\n    \"description\": \"Etichetta per mostrare più opzioni\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Includi l'audio di sistema\",\n    \"description\": \"Etichetta audio di sistema/etichetta tab\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Nascondi la barra degli strumenti\",\n    \"description\": \"Etichetta per nascondere la barra degli strumenti\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Conto alla rovescia\",\n    \"description\": \"Etichetta del conto alla rovescia\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Imposta il limite di tempo\",\n    \"description\": \"Etichetta dell'allarme\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Sfocatura\",\n    \"description\": \"Etichetta per il tipo di sfocatura dello sfondo\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Nessuno\",\n    \"description\": \"Nessun dispositivo selezionato\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Nessuna fotocamera\",\n    \"description\": \"Nessuna fotocamera selezionata\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Nessun microfono\",\n    \"description\": \"Nessun microfono selezionato\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Seleziona una sorgente\",\n    \"description\": \"Segnaposto per la selezione della sorgente\"\n  },\n  \"offLabel\": {\n    \"message\": \"Off\",\n    \"description\": \"Etichetta per disattivare nel menu a discesa\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Larghezza\",\n    \"description\": \"Etichetta per la larghezza della regione\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Altezza\",\n    \"description\": \"Etichetta per l'altezza della regione\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Clicca per inserire l'immagine\",\n    \"description\": \"Messaggio a comparsa al momento dell'inserimento di una nuova immagine\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Termina la registrazione\",\n    \"description\": \"Suggerimento per terminare la registrazione\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Ricomincia la registrazione\",\n    \"description\": \"Suggerimento per ricominciare la registrazione\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Mettila in pausa\",\n    \"description\": \"Suggerimento per mettere in pausa la registrazione\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Riprendi la registrazione\",\n    \"description\": \"Suggerimento per riprendere la registrazione\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Annulla la registrazione\",\n    \"description\": \"Suggerimento per annullare la registrazione\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Strumenti di disegno\",\n    \"description\": \"Suggerimento per attivare/disattivare gli strumenti di disegno\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Strumento di sfocatura\",\n    \"description\": \"Suggerimento per attivare/disattivare lo strumento di sfocatura\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Opzioni del cursore\",\n    \"description\": \"Suggerimento per attivare/disattivare le opzioni del cursore\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Disattiva la fotocamera\",\n    \"description\": \"Suggerimento per disattivare la fotocamera\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Attiva la fotocamera\",\n    \"description\": \"Suggerimento per attivare la fotocamera\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Nessun permesso per la fotocamera\",\n    \"description\": \"Suggerimento per l'assenza di permessi per la fotocamera\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Disattiva il microfono\",\n    \"description\": \"Suggerimento per disattivare il microfono\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Attiva il microfono\",\n    \"description\": \"Suggerimento per attivare il microfono\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Nessun permesso per il microfono\",\n    \"description\": \"Suggerimento per l'assenza di permessi per il microfono\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Strumento di selezione\",\n    \"description\": \"Suggerimento per selezionare uno strumento\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Penna\",\n    \"description\": \"Suggerimento per lo strumento penna\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Evidenziatore\",\n    \"description\": \"Suggerimento per lo strumento evidenziatore\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Cancella\",\n    \"description\": \"Suggerimento per lo strumento cancella\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Testo\",\n    \"description\": \"Suggerimento per lo strumento testo\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Forme\",\n    \"description\": \"Suggerimento per lo strumento forme\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Freccia\",\n    \"description\": \"Suggerimento per lo strumento freccia\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Immagini\",\n    \"description\": \"Suggerimento per lo strumento immagini\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Suggerimento per annullare\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Rifare\",\n    \"description\": \"Suggerimento per rifare\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Cancella tela\",\n    \"description\": \"Suggerimento per cancellare la tela\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Più colori\",\n    \"description\": \"Suggerimento per più colori\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Pennello spesso\",\n    \"description\": \"Suggerimento per pennello spesso\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Pennello medio\",\n    \"description\": \"Suggerimento per pennello medio\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Pennello sottile\",\n    \"description\": \"Suggerimento per pennello sottile\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Modalità Picture-in-Picture (PIP)\",\n    \"description\": \"Suggerimento per attivare la modalità Picture-in-Picture (PIP)\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Attiva riempimento\",\n    \"description\": \"Suggerimento per attivare il riempimento\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Modalità disegno\",\n    \"description\": \"Messaggio di avviso per la modalità disegno\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Modalità sfocatura\",\n    \"description\": \"Messaggio di avviso per la modalità sfocatura\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Cancella tutti gli elementi sfocati\",\n    \"description\": \"Suggerimento per cancellare tutti gli elementi sfocati\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Evidenzia clic\",\n    \"description\": \"Suggerimento per evidenziare clic\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Evidenzia cursore\",\n    \"description\": \"Suggerimento per evidenziare il cursore\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Punto focale sul cursore\",\n    \"description\": \"Suggerimento per il punto focale sul cursore\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Non mostrare più\",\n    \"description\": \"Pulsante per non mostrare più nel modalità di autorizzazioni\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Sei sicuro di voler riavviare la registrazione?\",\n    \"description\": \"Titolo del modalità di riavvio registrazione\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Perderai tutto il tuo progresso.\",\n    \"description\": \"Descrizione del modalità di riavvio registrazione\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Riavvia registrazione\",\n    \"description\": \"Pulsante per riavviare la registrazione nel modalità\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Riprendi registrazione\",\n    \"description\": \"Pulsante per riprendere la registrazione nel modalità\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Sei sicuro di voler scartare la registrazione?\",\n    \"description\": \"Titolo del modalità di scarto registrazione\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Perderai tutto il tuo progresso.\",\n    \"description\": \"Descrizione del modalità di scarto registrazione\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Scarta registrazione\",\n    \"description\": \"Pulsante per scartare la registrazione nel modalità\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Riprendi registrazione\",\n    \"description\": \"Pulsante per riprendere la registrazione nel modalità\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Scegli cosa registrare\",\n    \"description\": \"Titolo nella pagina di registrazione\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Registrazione in corso...\",\n    \"description\": \"Titolo nella pagina di registrazione durante la registrazione\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Tieni aperta questa scheda. Si chiuderà una volta che il tuo video sarà salvato.\",\n    \"description\": \"Descrizione nella pagina di registrazione\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparazione registrazione...\",\n    \"description\": \"Titolo nella pagina di registrazione mentre si sta registrando/salvando\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Lascia aperta questa scheda. Si aggiornerà con il tuo video una volta pronto.\",\n    \"description\": \"Descrizione nella pagina di registrazione mentre si sta registrando/salvando\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Titolo nella navigazione della pagina dell'editor di prova\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Pulsante per annullare nella pagina dell'editor di prova\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Ripristina originale\",\n    \"description\": \"Pulsante per ripristinare nella pagina dell'editor di prova\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Reimposta\",\n    \"description\": \"Pulsante per reimpostare nella pagina dell'editor di prova\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Salva modifiche\",\n    \"description\": \"Pulsante per salvare nella pagina dell'editor di prova\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Salvataggio...\",\n    \"description\": \"Pulsante per salvare nella pagina dell'editor di prova mentre si sta salvando\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Trascina il tuo file audio\",\n    \"description\": \"Trascina e rilascia il file audio nella pagina dell'editor di prova\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"O fai clic per cercare\",\n    \"description\": \"O fai clic per cercare il file audio nella pagina dell'editor di prova\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Impostazioni audio\",\n    \"description\": \"Titolo delle impostazioni audio nella pagina dell'editor di prova\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Etichetta del volume audio nella pagina dell'editor di prova\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Applica\",\n    \"description\": \"Pulsante di aggiornamento audio nella pagina dell'editor di test\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Taglia\",\n    \"description\": \"Titolo del ritaglio nella pagina dell'editor di test\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Larghezza\",\n    \"description\": \"Etichetta larghezza per le proprietà\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Altezza\",\n    \"description\": \"Etichetta altezza per le proprietà\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Sinistra\",\n    \"description\": \"Etichetta sinistra per le proprietà\"\n  },\n  \"topLabel\": {\n    \"message\": \"In alto\",\n    \"description\": \"Etichetta in alto per le proprietà\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Trascina le maniglie e usa i pulsanti a sinistra per modificare la sezione selezionata.\",\n    \"description\": \"Informazioni sul ritaglio nell'editor di test\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Taglia video\",\n    \"description\": \"Pulsante di ritaglio nell'editor di test\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Tagliando...\",\n    \"description\": \"Pulsante di ritaglio nell'editor di test durante il ritaglio\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Rimuovi sezione\",\n    \"description\": \"Pulsante di taglio nell'editor di test\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Rimozione...\",\n    \"description\": \"Pulsante di taglio nell'editor di test durante il taglio\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Silenzia audio\",\n    \"description\": \"Pulsante di silenziamento nell'editor di test\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Silenzio...\",\n    \"description\": \"Pulsante di silenziamento nell'editor di test durante il silenziamento\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Scopri di più.\",\n    \"description\": \"Scopri di più con un punto\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Etichetta annulla\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Ripeti\",\n    \"description\": \"Etichetta ripeti\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Lascia una recensione\",\n    \"description\": \"Pulsante per lasciare una recensione\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Rimani informato\",\n    \"description\": \"Pulsante per seguire per gli aggiornamenti\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Offline\",\n    \"description\": \"Etichetta offline\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Alcune funzioni non sono disponibili \",\n    \"description\": \"Descrizione dell'etichetta offline\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Riprova\",\n    \"description\": \"Pulsante per riprovare dell'etichetta offline\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome deve essere aggiornato\",\n    \"description\": \"Etichetta per aggiornare Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Aggiorna per accedere a più funzionalità\",\n    \"description\": \"Descrizione dell'etichetta per aggiornare Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Scopri di più\",\n    \"description\": \"Pulsante per saperne di più\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video troppo lungo per essere elaborato localmente\",\n    \"description\": \"Etichetta di limite superiore a 5 minuti nell'editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Modifica ed esportazione in formato MP4 non disponibili\",\n    \"description\": \"Descrizione del limite superiore a 5 minuti nell'editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Il video è in fase di elaborazione...\",\n    \"description\": \"Etichetta per l'elaborazione video nell'editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Abbonati per modificare video più rapidamente\",\n    \"description\": \"Descrizione dell'elaborazione video nell'editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Modifica\",\n    \"description\": \"Titolo della modifica nella pagina dell'editor di test\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Nessuna connessione\",\n    \"description\": \"Etichetta nessuna connessione\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Non disponibile\",\n    \"description\": \"Etichetta non disponibile\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Modifica video\",\n    \"description\": \"Etichetta del pulsante di ritaglio\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Taglia o silenzia il video\",\n    \"description\": \"Etichetta del ritaglio\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Preparazione...\",\n    \"description\": \"Etichetta preparazione\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Taglia\",\n    \"description\": \"Etichetta del pulsante di ritaglio\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Taglia e ridimensiona il video\",\n    \"description\": \"Etichetta del ritaglio\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Aggiungi audio\",\n    \"description\": \"Etichetta del pulsante per aggiungere audio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Carica il tuo audio per aggiungerlo al video\",\n    \"description\": \"Etichetta per aggiungere audio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Integrazioni\",\n    \"description\": \"Titolo del salvataggio nella pagina dell'editor di test\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Esci da Google Drive\",\n    \"description\": \"Etichetta esci da Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Salvataggio...\",\n    \"description\": \"Etichetta salvataggio su Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Salva su Drive\",\n    \"description\": \"Pulsante di salvataggio su Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Salva la versione attuale su Google Drive\",\n    \"description\": \"Etichetta per il salvataggio su Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Accedi e salva su Drive\",\n    \"description\": \"Etichetta per l'accesso a Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Esporta\",\n    \"description\": \"Titolo dell'esportazione nella pagina dell'editor di prova\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Scaricando...\",\n    \"description\": \"Etichetta di download\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Scarica come .webm\",\n    \"description\": \"Pulsante di download WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Esporta un video .webm\",\n    \"description\": \"Etichetta di download WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Scarica come .mp4\",\n    \"description\": \"Pulsante di download MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Esporta un video .mp4 (consigliato)\",\n    \"description\": \"Etichetta di download MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Scarica come .gif\",\n    \"description\": \"Pulsante di download GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Esporta un GIF (massimo 30 secondi)\",\n    \"description\": \"Etichetta di download GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Pronto a migliorare le tue registrazioni?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Condividi nel cloud, usa zoom e modelli... e supporta una dev indie che rispetta la tua privacy ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Scopri Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Hai già un account? Accedi\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Condividi video\",\n    \"description\": \"Pulsante di condivisione nella pagina dell'editor di prova\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Sostituisci l'audio esistente\",\n    \"description\": \"Pulsante di sostituzione dell'audio nell'editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom sul cursore\",\n    \"description\": \"Popup di zoom sul punto\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Rimani nella pagina durante la registrazione\",\n    \"description\": \"Popup di permanenza nella pagina\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Mostra un avviso se il microfono è spento\",\n    \"description\": \"Popup di promemoria del microfono\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Aiuto\",\n    \"description\": \"Pulsante di popup di aiuto\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Registrazione in pausa. Premi il pulsante di riproduzione per continuare.\",\n    \"description\": \"Titolo del modalità di registrazione in pausa\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity non ha il permesso di registrare il tuo schermo\",\n    \"description\": \"Titolo del modalità di autorizzazioni\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Per abilitare la registrazione dello schermo, devi concedere le autorizzazioni a Screenity.\",\n    \"description\": \"Descrizione del modalità di autorizzazioni\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Abilita la registrazione dello schermo\",\n    \"description\": \"Azione del modalità di autorizzazioni\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Annulla del modalità di autorizzazioni\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Il tuo microfono è spento\",\n    \"description\": \"Titolo del modalità di microfono spento\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Per includere l'audio nella tua registrazione, devi attivare il tuo microfono. Vuoi continuare senza audio?\",\n    \"description\": \"Descrizione del modalità di microfono spento\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Sì, continua\",\n    \"description\": \"Continua del modalità di microfono spento\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Annulla del modalità di microfono spento\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Inizia a usare Screenity con tre semplici passaggi:\",\n    \"description\": \"Titolo dei passaggi di configurazione\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Fai clic sull'icona \",\n    \"description\": \"Passaggio 1 di configurazione, prima dell'icona. Richiede uno spazio alla fine.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" delle estensioni\",\n    \"description\": \"Passaggio 1 di configurazione, dopo l'icona. Richiede uno spazio all'inizio.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Premi l'icona \",\n    \"description\": \"Passaggio 2 di configurazione, prima dell'icona. Richiede uno spazio alla fine.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" del pin\",\n    \"description\": \"Passaggio 2 di configurazione, dopo l'icona. Richiede uno spazio all'inizio.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Fai clic sull'icona di Screenity\",\n    \"description\": \"Passaggio 3 di configurazione, prima dell'icona. Richiede uno spazio alla fine.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" per iniziare\",\n    \"description\": \"Passaggio 3 di configurazione, dopo l'icona. Richiede uno spazio all'inizio.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Fantastico! Sei pronto!\",\n    \"description\": \"Titolo del completamento della configurazione\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Ora puoi iniziare a registrare qui o in qualsiasi altra scheda.\",\n    \"description\": \"Descrizione del completamento della configurazione\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Clicca qui per disegnare\",\n    \"description\": \"Istruzioni per fare clic qui\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Rilassati. Fai clic ovunque per interrompere il conto alla rovescia.\",\n    \"description\": \"Messaggio del conto alla rovescia\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"L'editing del video potrebbe essere impreciso a causa degli intervalli tra i frame con brevi durate.\",\n    \"description\": \"Informazioni sull'editor troppo piccolo\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Il video è in fase di elaborazione, la riproduzione potrebbe essere imprecisa o interrotta.\",\n    \"description\": \"Banner di elaborazione nell'editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Il ritaglio del video potrebbe richiedere del tempo\",\n    \"description\": \"Titolo delle informazioni sul ritaglio\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Iscriviti per modificare i video più velocemente\",\n    \"description\": \"Descrizione delle informazioni sul ritaglio\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microfono attivato\",\n    \"description\": \"Titolo del messaggio del microfono attivato\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microfono disattivato\",\n    \"description\": \"Titolo del messaggio del microfono disattivato\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Nascondi le notifiche dell'interfaccia\",\n    \"description\": \"Pulsante per nascondere gli avvisi dell'interfaccia\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Non mostrare più\",\n    \"description\": \"Pulsante di non mostrare più\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Nascondi i controlli quando non li usi\",\n    \"description\": \"Pulsante per mostrare la barra degli strumenti solo passando il cursore\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Arresta la registrazione\",\n    \"description\": \"Pulsante per interrompere la registrazione nella schermata di registrazione\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Benvenuto nella nuova versione di Screenity!\",\n    \"description\": \"Titolo dell'annuncio di aggiornamento per gli utenti esistenti\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Abbiamo aggiornato il design e sviluppato nuove funzionalità, ma non preoccuparti, rimane comunque l'estensione gratuita, privata e open source di sempre.\",\n    \"description\": \"Descrizione dell'annuncio di aggiornamento per gli utenti esistenti\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Scopri di più sull'aggiornamento.\",\n    \"description\": \"Link 'Scopri di più' dell'annuncio di aggiornamento per gli utenti esistenti\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Inizia\",\n    \"description\": \"Pulsante dell'annuncio di aggiornamento per gli utenti esistenti\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Errore durante l'avvio della registrazione\",\n    \"description\": \"Titolo del modalità di errore di streaming\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Si è verificato un errore durante l'avvio della registrazione. Riavvia il tuo browser e riprova. Se il problema persiste, contattaci all'indirizzo support@screenity.io.\",\n    \"description\": \"Descrizione del modalità di errore di streaming\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Qualità più alta\",\n    \"description\": \"Etichetta per attivare la qualità più alta nella finestra popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Ripristina l'ultima registrazione\",\n    \"description\": \"Pulsante per ripristinare l'ultima registrazione nella finestra popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Riscontri problemi?\",\n    \"description\": \"Pulsante dei problemi nell'editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Non riesci a vedere la tua registrazione?\",\n    \"description\": \"Titolo del modal dei problemi nell'editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Se hai completato la tua registrazione e ti trovi su questa pagina senza vedere il tuo video, puoi tentare di scaricare i dati video grezzi se sono disponibili. Puoi anche metterti in contatto e inviare una segnalazione di errore.\",\n    \"description\": \"Descrizione del modal dei problemi nell'editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Scarica dati video grezzi\",\n    \"description\": \"Pulsante nel modal dei problemi nell'editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Segnala un errore\",\n    \"description\": \"Secondo pulsante nel modal dei problemi nell'editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Nessuna registrazione trovata, mi dispiace :(\",\n    \"description\": \"Avviso di nessuna registrazione trovata nell'editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Limite di memoria raggiunto\",\n    \"description\": \"Titolo del limite di memoria raggiunto\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Il tuo dispositivo ha esaurito la memoria disponibile per la registrazione, causando l'interruzione automatica della registrazione. Se desideri registrare per un periodo più lungo, puoi provare a ridurre la qualità del video o a liberare spazio sul tuo dispositivo.\",\n    \"description\": \"Descrizione del limite di memoria raggiunto\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Capito\",\n    \"description\": \"Pulsante Capito\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Spazio insufficiente\",\n    \"description\": \"Titolo di spazio insufficiente\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity richiede almeno 1 GB di spazio libero per la registrazione. Per favore, libera spazio e riprova.\",\n    \"description\": \"Descrizione di spazio insufficiente\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Cancella le registrazioni precedenti\",\n    \"description\": \"Pulsante Cancella le registrazioni precedenti\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avanzato\",\n    \"description\": \"Sezione avanzata nella pagina dell'editor nell'ambiente protetto\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Scarica file video grezzo\",\n    \"description\": \"Pulsante di download registrazione grezza\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Esporta i dati di registrazione originali\",\n    \"description\": \"Etichetta del pulsante di download registrazione grezza\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Ottieni assistenza per la tua registrazione\",\n    \"description\": \"Pulsante di risoluzione dei problemi\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recupera il tuo video e risolvi altri problemi\",\n    \"description\": \"Etichetta di risoluzione dei problemi\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Scarica file video grezzo\",\n    \"description\": \"Titolo del modulo registrazione grezza\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Il file video grezzo è il file originale registrato da Screenity. Questo video non è stato elaborato o modificato, quindi potrebbe essere molto grande e potrebbe non essere riproducibile su tutti i dispositivi. Se stai riscontrando problemi con il video elaborato, puoi utilizzare questo file per recuperare il tuo video.\",\n    \"description\": \"Descrizione del modulo registrazione grezza\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Scarica\",\n    \"description\": \"Pulsante del modulo registrazione grezza\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Scarica informazioni di sistema e dati video per la risoluzione dei problemi\",\n    \"description\": \"Titolo del modulo di risoluzione dei problemi\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Dopo il download, invia il file a support@screenity.io con qualsiasi informazione pertinente. Utilizzeremo questi dati per aiutarti a risolvere eventuali problemi che riscontri con l'estensione e, se possibile, recuperare il tuo video.\",\n    \"description\": \"Descrizione del modulo di risoluzione dei problemi\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Scarica\",\n    \"description\": \"Pulsante del modulo di risoluzione dei problemi\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Modalità di recupero\",\n    \"description\": \"Titolo della modalità di recupero\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Scarica dati per risolvere problemi\",\n    \"description\": \"Opzione di scaricamento per risolvere problemi\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centro assistenza\",\n    \"description\": \"Elemento di navigazione per il centro assistenza\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Registra l'audio del computer\",\n    \"description\": \"Titolo dell'avviso audio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Per registrare l'audio da questa pagina, devi passare a '$tab$'.\",\n    \"description\": \"Descrizione dell'avviso audio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Area della scheda\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Estensione non supportata sulla scheda\",\n    \"description\": \"Titolo dell'estensione non supportata sulla scheda\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity non è stato supportato nella tua scheda precedente. Puoi comunque iniziare la registrazione qui e passare alla scheda, ma la tua fotocamera e la barra degli strumenti non saranno visibili.\",\n    \"description\": \"Descrizione dell'estensione non supportata sulla scheda\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configura un backup locale per le registrazioni di Screenity\",\n    \"description\": \"Titolo dei backup\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Tieni una copia di tutte le registrazioni che fai. Se non configuri i backup, le tue registrazioni verranno salvate solo quando decidi di scaricarle.\",\n    \"description\": \"Descrizione dei backup\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Potresti dover creare una nuova cartella prima se stai cercando di salvare in Download o in altre cartelle di sistema.\",\n    \"description\": \"Descrizione dei backup\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Seleziona una cartella\",\n    \"description\": \"Pulsante Seleziona una cartella\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Non ora\",\n    \"description\": \"Pulsante Non ora\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Le tue registrazioni sono salvate localmente\",\n    \"description\": \"Titolo dei backup attivi\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Per evitare di richiedere autorizzazioni ogni volta che registri, ti consigliamo di lasciare aperta questa scheda.\",\n    \"description\": \"Descrizione dei backup attivi\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Chiudi comunque la scheda\",\n    \"description\": \"Pulsante Chiudi comunque la scheda\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Arresta tutti i backup delle registrazioni\",\n    \"description\": \"Pulsante Arresta i backup\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Conferma nuovamente l'accesso alla cartella di backup locale di Screenity\",\n    \"description\": \"Titolo della conferma dei backup\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Ci dispiace, ma abbiamo bisogno della tua autorizzazione ancora una volta per continuare a fare backup locali delle tue registrazioni. Sfortunatamente, dovremo chiedere ogni volta che chiudi questa scheda, il che può accadere se chiudi il browser o riavvii il computer.\",\n    \"description\": \"Descrizione della conferma dei backup\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Consenti a Screenity di continuare a fare backup delle registrazioni\",\n    \"description\": \"Pulsante Consenti nella conferma dei backup\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Attiva i backup delle registrazioni\",\n    \"description\": \"Etichetta Attiva i backup\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permesso non concesso per il backup della registrazione\",\n    \"description\": \"Titolo del fallimento del permesso di backup\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Non hai selezionato una cartella per il backup locale delle registrazioni. A seconda della versione del tuo browser o del sistema operativo, potrebbe essere richiesto di selezionare la cartella ogni volta che inizi a registrare. Puoi riprovare o disabilitare completamente i backup se preferisci non concedere autorizzazioni ripetutamente.\",\n    \"description\": \"Descrizione del fallimento del permesso di backup\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Registra l'audio del computer\",\n    \"description\": \"Titolo dell'avviso audio per macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"Su macOS, puoi registrare solo l'audio della scheda. Seleziona l'opzione 'Scheda Chrome' e assicurati che 'Condividi anche l'audio della scheda' sia abilitato.\",\n    \"description\": \"Descrizione dell'avviso audio per macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Registra l'audio del computer\",\n    \"description\": \"Titolo dell'avviso audio per altri sistemi\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Assicurati di attivare l'opzione 'Condividi l'audio del sistema' se desideri registrare l'audio del computer. Se non vedi questa opzione, prova a aggiornare Chrome o passa alla registrazione della scheda.\",\n    \"description\": \"Descrizione dell'avviso audio per altri sistemi\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Risoluzione massima\",\n    \"description\": \"Etichetta risoluzione massima nel menu delle impostazioni\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"FPS massimi\",\n    \"description\": \"Etichetta FPS massimi nel menu delle impostazioni\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"RAM insufficiente\",\n    \"description\": \"Etichetta 'RAM insufficiente' nel menu delle impostazioni\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Ridimensiona finestra\",\n    \"description\": \"Etichetta per il ridimensionamento della finestra nel menu delle impostazioni\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Lo schermo è troppo piccolo per questa risoluzione\",\n    \"description\": \"Tooltip che indica che lo schermo è troppo piccolo nel menu delle impostazioni\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Impossibile registrare a questa risoluzione sul tuo dispositivo\",\n    \"description\": \"Tooltip che indica che non è possibile registrare a questa risoluzione nel menu delle impostazioni\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Rivedi le autorizzazioni\",\n    \"description\": \"Pulsante per la revisione delle autorizzazioni\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Aggiungi un'altra registrazione\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Preparando tutto...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Attendi un momento, stiamo preparando la tua registrazione.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barra degli strumenti nascosta. Riattivala dalle opzioni.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Accedi o registrati\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Disconnettiti\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Sessione terminata\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Hai raggiunto il limite di archiviazione\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Hai usato 100 GB di archiviazione. Elimina vecchi video o scene per continuare a registrare.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gestisci archiviazione\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Chiudi\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Impossibile verificare l’archiviazione\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"C'è stato un problema nella verifica dello spazio. Accedi di nuovo e riprova.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Impossibile verificare la tua quota di archiviazione. Riprova tra qualche secondo o aggiorna la pagina.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Riprova\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Scena aggiunta alla registrazione Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Pronta per registrare una nuova scena nel tuo video\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"La registrazione si sta avvicinando al limite di 90 minuti, verrà interrotta a breve\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Registrazione interrotta per aver raggiunto il limite di 90 minuti\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"La registrazione delle schede è disattivata su questa pagina. Registrazione schermo attivata per impostazione predefinita.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Registrazione del video annullata\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Link copiato negli appunti\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Impossibile copiare il link\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Più recenti\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Più vecchi\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Tutti i video\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Nessun video trovato\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Caricamento video...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Vai al pannello\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Benvenuta su Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Niente pubblicità. Niente tracciamento. Nessun limite.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Registra, semplicemente.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Usalo gratis — senza registrazione!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Vuoi fare di più con i tuoi video?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Archiviazione cloud, condividi con un link\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combina più clip come capitoli di una storia\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zoom intelligenti con un clic (o aggiungi i tuoi!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Aggiungi template, sottotitoli, mockup...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Sfondi e layout fotocamera personalizzati\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Sblocca funzioni extra\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Supporta lo sviluppo indipendente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Impostazioni account\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Supporto\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Abbonamento Pro inattivo\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Il tuo abbonamento a Screenity Pro è inattivo.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Riattivalo per accedere di nuovo.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"I tuoi video e dati verranno eliminati definitivamente il \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gestisci abbonamento\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Vuoi continuare a usare la versione gratuita?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Disconnettiti e passa alla versione gratuita\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Sessione terminata\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Per mantenere le registrazioni sincronizzate e accedere alle funzioni Pro, accedi di nuovo.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Accedi di nuovo\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Puoi continuare a usare l’estensione senza account, ma le registrazioni non verranno salvate né recuperate in seguito.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continua senza accedere\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Modalità registrazione istantanea\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Download immediato, ma non potrai modificare la disposizione della fotocamera dopo l'inizio.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Modalità registrazione istantanea\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Questa modalità registra tutto in un singolo video da scaricare e condividere subito. Non potrai modificare il layout della fotocamera, ma potrai comunque fare altre modifiche.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Capito\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"La registrazione delle schede è disattivata su questa pagina\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Aggiungendo a: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Termina\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Vuoi fare di più con le tue registrazioni?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Accedi per salvare i video sul cloud, condividerli con un link e accedere a funzioni di editing avanzate.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Accedi per sbloccare le funzioni Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Supporta la sviluppatrice indipendente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Sblocca più funzioni\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Accedi per condividere (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Sblocca cloud, editing multi-scena, zoom automatici, sottotitoli e altro ancora\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity sarà sempre gratuito, open source e senza pubblicità. Pro aiuta a coprire i costi del cloud e sostiene una sviluppatrice indipendente ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Non mostrare più\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Provalo\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Modalità Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Registra più scene, come lo schermo, la fotocamera o entrambi, una dopo l’altra. È perfetto per fare più riprese, cambiare inquadratura o suddividere la registrazione in parti. Quando hai finito, clicca su Fine per aprire l’editor con tutte le scene unite in un unico video.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Attiva Pro per iniziare\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Abbonati per accedere alle funzionalità Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Abbonati a Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Usi Screenity in locale? Aiuta a supportarne lo sviluppo!\",\n    \"description\": \"Titolo del banner per l’auto-hosting nell’editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity è sviluppato da un’indie dev. L’auto-hosting è gratuito, ma se ti è utile, considera di supportarla con la versione Pro ❤️\",\n    \"description\": \"Descrizione del banner per l’auto-hosting nell’editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Bentornato su Screenity\",\n    \"description\": \"Titolo del popup di bentornato mostrato agli utenti di ritorno\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Novità: Vuoi fare di più con le tue registrazioni?\",\n    \"description\": \"Titolo per la funzione di salvataggio su cloud nel popup di bentornato\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro è una nuova piattaforma opzionale per salvare i tuoi video nel cloud, condividerli tramite link e accedere a strumenti avanzati di editing, se e quando ti servono.\",\n    \"description\": \"Descrizione della funzione di salvataggio su cloud nel popup di bentornato\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Scopri di più su Screenity Pro\",\n    \"description\": \"Pulsante di call to action per la funzione di salvataggio su cloud nel popup di bentornato\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Benvenuto su Screenity Pro\",\n    \"description\": \"Titolo della finestra di benvenuto per Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Sei pronto per iniziare a registrare! Alcune cose da tenere a mente:\\n\\n- La bolla della fotocamera potrebbe scomparire durante la registrazione: è normale. Viene acquisita separatamente in background, così potrai posizionarla come preferisci in un secondo momento.\\n- Gli zoom vengono creati automaticamente solo sui clic all’interno delle schede di Chrome, a causa di una limitazione del browser. Puoi sempre aggiungere altri zoom manualmente dopo la registrazione.\\n- Per registrazioni rapide con download immediato, prova la Modalità Istantanea. Tieni presente che le opzioni di modifica sono limitate: niente sfondi, layout o regolazioni avanzate.\",\n    \"description\": \"Descrizione della finestra di benvenuto per Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Ho capito\",\n    \"description\": \"Pulsante di conferma della finestra di benvenuto per Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 errore — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Benvenuto nell'estensione Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Ecco qualche consiglio rapido prima di registrare.<br/>Gli zoom automatici funzionano solo con clic nelle <strong>schede di Chrome</strong>. Puoi comunque aggiungere zoom manualmente in seguito.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barra di registrazione ed effetti\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Usa questa barra per disegnare, applicare effetti cursore, sfocare e controllare la registrazione.<br/><br/><strong>Questa barra comparirà nel video</strong> se non la nascondi.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"La camera viene acquisita separatamente\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Durante la registrazione la camera può <strong>nascondersi o passare in PiP</strong>, è normale.<br/><br/>Viene acquisita separatamente così puoi riposizionarla dopo.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Modalità istantanea\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Perfetta per condividere velocemente con <strong>download illimitati</strong>.<br/><br/>In questa modalità non sono disponibili layout e opzioni di editing avanzate.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Capito\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Avanti\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Indietro\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Scopri di più\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Reimposta onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Poco spazio su disco. Salvataggio della registrazione in corso. Tutto ciò che è stato catturato è al sicuro.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Registrazione lunga\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"La registrazione è completa e al sicuro. Scaricala in formato WebM qui sotto.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Non disponibile per registrazioni lunghe\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Non disponibile in modalità di recupero\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Modifica audio troppo lunga\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Questa modifica audio non è supportata per clip oltre 15 minuti. L'originale non è stato modificato. Prova prima a tagliare il clip.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Modifica scaduta\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"La modifica non è stata completata in tempo. La registrazione originale non è stata modificata. Riprova o taglia il clip prima.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"La condivisione dello schermo si è interrotta. La registrazione è stata salvata. Interrompi quando sei pronto.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Si è verificato un problema di elaborazione, ma la registrazione è al sicuro. Puoi scaricarla qui sotto.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Elaborazione della modifica in corso. La registrazione originale non è stata modificata.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Impossibile applicare la modifica\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Qualcosa è andato storto durante l'elaborazione. La registrazione originale è al sicuro. Puoi riprovare o scaricarla così com'è.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"La condivisione dello schermo si è interrotta. Salvataggio della registrazione in corso. Tutto ciò che è stato catturato è al sicuro.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Audio disconnesso. Salvataggio della registrazione in corso. Il video catturato è al sicuro.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"La registrazione è stata salvata. L'editor non si è aperto. Clicca sull'icona di Screenity per riprovare.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Il salvataggio sta richiedendo più tempo del previsto. I tuoi dati sono al sicuro. L'editor si aprirà a breve.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copia info di debug\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info di debug copiate\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Ottieni aiuto\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"La registrazione era troppo breve per essere salvata\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Registratore veloce non riuscito\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"L'output del registratore veloce non ha potuto essere convalidato su questo dispositivo.\\nPuoi comunque scaricare il file.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Scarica comunque\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Annulla\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/ko/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - 화면 녹화 프로그램 & 주석 도구\",\n    \"description\": \"Extension name\"\n  },\n  \"extDesc\": {\n    \"message\": \"제한 없이 사용할 수 있는 최고의 무료 화면 녹화기입니다. 로그인이 필요하지 않으며 개인정보 보호에 친화적으로 화면을 녹화하고 주석을 추가하고 확대/축소하며 흐림 효과를 적용하고 비디오를 편집하세요.\",\n    \"description\": \"Extension description\"\n  },\n  \"recordTab\": {\n    \"message\": \"녹화\",\n    \"description\": \"Record tab label on popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"귀하의 동영상\",\n    \"description\": \"Videos tab label on popup\"\n  },\n  \"screenType\": {\n    \"message\": \"화면\",\n    \"description\": \"Screen recording type label on popup\"\n  },\n  \"tabType\": {\n    \"message\": \"탭 영역\",\n    \"description\": \"Tab area recording type label on popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"카메라\",\n    \"description\": \"Camera recording type label on popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"모의실행\",\n    \"description\": \"Mockup recording type label on popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"사용자 정의 영역 녹화 비활성화됨\",\n    \"description\": \"Custom area recording disabled warning on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Chrome 버전을 104로 업데이트하세요.\",\n    \"description\": \"Custom area recording disabled warning description on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"업데이트\",\n    \"description\": \"Custom area recording disabled warning action on popup for Chrome version older than 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"권한 확인\",\n    \"description\": \"Camera / microphone permissions warning modal title\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"스크리니티가 카메라 또는 마이크에 액세스할 수 없는 것으로 보입니다. 주소 표시 줄의 카메라 아이콘을 클릭하여 액세스를 허용하세요.\",\n    \"description\": \"Camera / microphone permissions warning modal description\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"닫기\",\n    \"description\": \"Camera / microphone permissions warning modal dismiss button\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"카메라 액세스 허용\",\n    \"description\": \"Allow camera access button\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"마이크 액세스 허용\",\n    \"description\": \"Allow microphone access button\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"푸시 투 토크\",\n    \"description\": \"Push to talk switch label\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"녹화할 영역 설정\",\n    \"description\": \"Custom area switch label\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"카메라 뒤집기\",\n    \"description\": \"Flip camera switch label\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"배경 효과\",\n    \"description\": \"Background effects switch label\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"녹화 시작\",\n    \"description\": \"Record button label\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"녹화 시작 중...\",\n    \"description\": \"Record button in progress label\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"녹화할 카메라를 설정하세요\",\n    \"description\": \"Record button label when in camera mode and no camera is selected\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"더 많은 옵션 표시\",\n    \"description\": \"Show more options label\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"시스템 오디오 포함\",\n    \"description\": \"탭/시스템 오디오 레이블\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"툴바 숨기기\",\n    \"description\": \"Hide toolbar label\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"카운트다운\",\n    \"description\": \"Countdown label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"시간 제한 설정\",\n    \"description\": \"Alarm label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"흐림\",\n    \"description\": \"Blur background label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"없음\",\n    \"description\": \"No device selected\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"카메라 없음\",\n    \"description\": \"No camera selected\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"마이크 없음\",\n    \"description\": \"No microphone selected\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"소스 선택\",\n    \"description\": \"Select source placeholder\"\n  },\n  \"offLabel\": {\n    \"message\": \"끄기\",\n    \"description\": \"Off label on dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"너비\",\n    \"description\": \"Region width label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"높이\",\n    \"description\": \"Region height label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"이미지 추가하려면 클릭\",\n    \"description\": \"Toast that shows when adding a new image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"녹화 종료\",\n    \"description\": \"Finish recording tooltip\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"녹화 재시작\",\n    \"description\": \"Restart recording tooltip\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"녹화 일시 중지\",\n    \"description\": \"Pause recording tooltip\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"녹화 재개\",\n    \"description\": \"Resume recording tooltip\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"녹화 취소\",\n    \"description\": \"Cancel recording tooltip\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"그리기 도구 전환\",\n    \"description\": \"Toggle drawing tools tooltip\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"흐리게 만들기 도구 전환\",\n    \"description\": \"Toggle blur tool tooltip\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"커서 옵션 전환\",\n    \"description\": \"Toggle cursor options tooltip\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"카메라 비활성화\",\n    \"description\": \"Disable camera tooltip\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"카메라 활성화\",\n    \"description\": \"Enable camera tooltip\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"카메라 권한 없음\",\n    \"description\": \"No camera permissions tooltip\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"마이크 비활성화\",\n    \"description\": \"Disable microphone tooltip\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"마이크 활성화\",\n    \"description\": \"Enable microphone tooltip\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"마이크 권한 없음\",\n    \"description\": \"No microphone permissions tooltip\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"도구 선택\",\n    \"description\": \"Select tool tooltip\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"펜 도구\",\n    \"description\": \"Pen tool tooltip\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"형광펜 도구\",\n    \"description\": \"Highlighter tool tooltip\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"지우개 도구\",\n    \"description\": \"Eraser tool tooltip\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"텍스트 도구\",\n    \"description\": \"Text tool tooltip\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"도형 도구\",\n    \"description\": \"Shape tool tooltip\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"화살표 도구\",\n    \"description\": \"Arrow tool tooltip\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"이미지 도구\",\n    \"description\": \"Image tool tooltip\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"되돌리기\",\n    \"description\": \"Undo tooltip\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"다시 실행\",\n    \"description\": \"Redo tooltip\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"캔버스 지우기\",\n    \"description\": \"Clear canvas tooltip\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"더 많은 색상\",\n    \"description\": \"More colors tooltip\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"굵은 선\",\n    \"description\": \"Thick stroke tooltip\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"중간 선\",\n    \"description\": \"Medium stroke tooltip\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"얇은 선\",\n    \"description\": \"Thin stroke tooltip\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"화면 내 화면 모드 전환\",\n    \"description\": \"Toggle picture-in-picture mode tooltip\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"채우기 전환\",\n    \"description\": \"Toggle fill tooltip\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"그리기 모드\",\n    \"description\": \"Drawing mode toast\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"흐리게 만들기 모드\",\n    \"description\": \"Blur mode toast\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"모든 흐린 요소 지우기\",\n    \"description\": \"Clear all blurred elements tooltip\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"클릭 강조\",\n    \"description\": \"Highlight clicks tooltip\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"커서 강조\",\n    \"description\": \"Highlight cursor tooltip\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"커서 하이라이트\",\n    \"description\": \"Spotlight cursor tooltip\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"다시 표시하지 않음\",\n    \"description\": \"Permissions modal dismiss button\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"녹화를 다시 시작하시겠습니까?\",\n    \"description\": \"Restart recording modal title\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"현재 비디오 진행 상황이 손실됩니다.\",\n    \"description\": \"Restart recording modal description\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"녹화 다시 시작\",\n    \"description\": \"Restart recording modal restart button\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"녹화 재개\",\n    \"description\": \"Restart recording modal resume button\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"녹화를 폐기하시겠습니까?\",\n    \"description\": \"Discard recording modal title\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"현재 비디오 진행 상황이 손실됩니다.\",\n    \"description\": \"Discard recording modal description\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"녹화 폐기\",\n    \"description\": \"Discard recording modal discard button\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"녹화 재개\",\n    \"description\": \"Discard recording modal resume button\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"녹화할 항목 선택\",\n    \"description\": \"Title in recording page\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"녹화 중...\",\n    \"description\": \"Title in recording page while recording\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"이 탭을 열어둡니다. 녹화가 저장되면 자동으로 닫힙니다.\",\n    \"description\": \"Description in recording page\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"녹화 준비 중...\",\n    \"description\": \"Title in sandbox page while recording / saving\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"이 탭을 열어둡니다. 녹화가 준비되면 업데이트됩니다.\",\n    \"description\": \"Description in sandbox page while recording / saving\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"편집기\",\n    \"description\": \"Title in navigation of the sandbox editor page\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"취소\",\n    \"description\": \"Cancel button in sandbox editor page\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"원래대로 되돌리기\",\n    \"description\": \"Revert button in sandbox editor page\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"초기화\",\n    \"description\": \"Reset button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"변경 사항 저장\",\n    \"description\": \"Save button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"저장 중...\",\n    \"description\": \"Save button in sandbox editor page while saving\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"오디오 파일을 끌어다 놓거나 클릭하여 업로드하세요.\",\n    \"description\": \"Drag and drop audio file in sandbox editor page\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"또는 클릭하여 찾아보기\",\n    \"description\": \"Or click to browse audio file in sandbox editor page\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"설정\",\n    \"description\": \"Audio settings title in sandbox editor page\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"볼륨\",\n    \"description\": \"Audio volume label in sandbox editor page\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"업데이트\",\n    \"description\": \"Update audio button in sandbox editor page\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"자르기\",\n    \"description\": \"Crop title in sandbox editor page\"\n  },\n  \"widthLabel\": {\n    \"message\": \"폭\",\n    \"description\": \"Width label for properties\"\n  },\n  \"heightLabel\": {\n    \"message\": \"높이\",\n    \"description\": \"Height label for properties\"\n  },\n  \"leftLabel\": {\n    \"message\": \"왼쪽\",\n    \"description\": \"Left label for properties\"\n  },\n  \"topLabel\": {\n    \"message\": \"위\",\n    \"description\": \"Top label for properties\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"핸들을 드래그하고 왼쪽의 버튼을 사용하여 선택한 부분을 편집하세요.\",\n    \"description\": \"Info about trimming in sandbox editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"비디오 자르기\",\n    \"description\": \"Trim button in sandbox editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"자르기 중...\",\n    \"description\": \"Trim button in sandbox editor while trimming\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"섹션 자르기\",\n    \"description\": \"Cut button in sandbox editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"잘라내는 중...\",\n    \"description\": \"Cut button in sandbox editor while cutting\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"오디오 음소거\",\n    \"description\": \"Mute button in sandbox editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"음소거 중...\",\n    \"description\": \"Mute button in sandbox editor while muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"더 알아보기.\",\n    \"description\": \"Learn more with a dot\"\n  },\n  \"undoLabel\": {\n    \"message\": \"되돌리기\",\n    \"description\": \"Undo label\"\n  },\n  \"redoLabel\": {\n    \"message\": \"다시 실행\",\n    \"description\": \"Redo label\"\n  },\n  \"leaveReview\": {\n    \"message\": \"리뷰를 남겨주세요\",\n    \"description\": \"Leave a review button\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"업데이트 받기\",\n    \"description\": \"Follow for updates button\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"현재 오프라인 상태입니다\",\n    \"description\": \"Offline label\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"일부 기능은 다시 연결할 때까지 사용할 수 없습니다\",\n    \"description\": \"Offline label description\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"다시 시도\",\n    \"description\": \"Offline label try again button\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome을 업데이트해야 합니다\",\n    \"description\": \"Update Chrome label\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"더 많은 기능에 액세스하려면 업데이트하세요\",\n    \"description\": \"Update Chrome label description\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"더 알아보기\",\n    \"description\": \"Learn more button\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"로컬에서 처리하기에 비디오가 너무 길어요\",\n    \"description\": \"편집기 내에서 5분 이상 제한 레이블\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"편집 및 MP4 형식으로 내보내기를 사용할 수 없어요\",\n    \"description\": \"편집기 내에서 5분 이상 제한 설명\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"비디오 처리 중...\",\n    \"description\": \"Video processing label in editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"업그레이드하여 더 빠른 편집\",\n    \"description\": \"Video processing description in editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"편집\",\n    \"description\": \"Edit title in sandbox editor page\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"연결 없음\",\n    \"description\": \"No connection label\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"사용 불가\",\n    \"description\": \"Not available label\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"비디오 편집\",\n    \"description\": \"Trim label button\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"비디오 자르기, 자르기 또는 음소거\",\n    \"description\": \"Trim label\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"준비 중...\",\n    \"description\": \"Preparing label\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"자르기\",\n    \"description\": \"Crop label button\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"비디오 자르기 및 크기 조정\",\n    \"description\": \"Crop label\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"오디오 추가\",\n    \"description\": \"Add audio label button\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"비디오에 추가할 자체 오디오 업로드\",\n    \"description\": \"Add audio label\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"저장\",\n    \"description\": \"Save title in sandbox editor page\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Drive에서 로그아웃\",\n    \"description\": \"Sign out of Google Drive label\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"저장 중...\",\n    \"description\": \"Saving to Google Drive label\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Drive에 저장\",\n    \"description\": \"Save to Google Drive button\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"현재 버전을 Google Drive에 저장\",\n    \"description\": \"Save to Google Drive label\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"로그인하고 Drive에 저장\",\n    \"description\": \"Sign in to Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"내보내기\",\n    \"description\": \"Export title in sandbox editor page\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"다운로드 중...\",\n    \"description\": \"Downloading label\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \".webm으로 다운로드\",\n    \"description\": \"Download WEBM button\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \".webm 비디오 내보내기\",\n    \"description\": \"Download WEBM label\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \".mp4으로 다운로드\",\n    \"description\": \"Download MP4 button\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \".mp4 비디오 내보내기 (권장)\",\n    \"description\": \"Download MP4 label\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \".gif으로 다운로드\",\n    \"description\": \"Download GIF button\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"GIF 내보내기 (최대 30초)\",\n    \"description\": \"Download GIF label\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"녹화를 한 단계 업그레이드해 볼까요?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"클라우드 공유, 줌, 템플릿 제공… 그리고 프라이버시를 지키는 여성 인디 개발자를 응원해 주세요! ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Pro 자세히 알아보기\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"이미 계정이 있으신가요? 로그인\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"비디오 공유\",\n    \"description\": \"Share button in sandbox editor page\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"기존 오디오 교체\",\n    \"description\": \"Replace audio button in editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"커서로 확대\",\n    \"description\": \"Zoom to point popup\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"녹화할 때 페이지에 머무르기\",\n    \"description\": \"Stay in page popup\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"마이크 꺼짐 경고\",\n    \"description\": \"Mic reminder popup\"\n  },\n  \"helpPopup\": {\n    \"message\": \"도움말\",\n    \"description\": \"Help popup button\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"녹화 일시 중지. 계속하려면 재생 버튼을 누르세요.\",\n    \"description\": \"Paused recording modal title\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity가 화면 녹화 권한이 없습니다\",\n    \"description\": \"Permission modal title\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"화면 녹화를 사용하려면 Screenity에게 화면 캡처 권한을 부여해야 합니다.\",\n    \"description\": \"Permission modal description\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"화면 녹화 활성화\",\n    \"description\": \"Permission modal action\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"취소\",\n    \"description\": \"Permission modal cancel\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"마이크가 음소거되었습니다\",\n    \"description\": \"Microphone muted modal title\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"녹음에 오디오를 포함하려면 마이크를 음소거 해제해야 합니다. 오디오 없이 계속하시겠습니까?\",\n    \"description\": \"Microphone muted modal description\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"네, 계속\",\n    \"description\": \"Microphone muted modal continue\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"취소\",\n    \"description\": \"Microphone muted modal cancel\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Screenity로 세 가지 간단한 단계로 시작하세요:\",\n    \"description\": \"Set up steps title\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- 클릭하세요 \",\n    \"description\": \"Set up step 1, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" 확장 아이콘\",\n    \"description\": \"Set up step 1, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- 누르세요 \",\n    \"description\": \"Set up step 2, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" 고정된 아이콘\",\n    \"description\": \"Set up step 2, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- 클릭하세요 \",\n    \"description\": \"Set up step 3, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity 아이콘을 시작하려면\",\n    \"description\": \"Set up step 3, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"멋져요! 이제 모든 준비가 끝났습니다\",\n    \"description\": \"Set up complete title\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"이제 여기에서 녹화를 시작하거나 다른 탭에서 녹화를 시작할 수 있습니다.\",\n    \"description\": \"Set up complete description\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"여기를 클릭하여 그리기\",\n    \"description\": \"Click here onboarding\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"편안하세요. 카운트다운을 중지하려면 어디든 클릭하세요.\",\n    \"description\": \"Countdown message\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"프레임 간 간격 때문에 짧은 시간 범위에 대한 편집이 부정확할 수 있습니다.\",\n    \"description\": \"Info about the editor being too small\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"비디오 처리 중이며 재생이 부정확하거나 중단될 수 있습니다.\",\n    \"description\": \"Processing banner in editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"자르기에는 시간이 걸릴 수 있습니다\",\n    \"description\": \"Cropping info title\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"업그레이드하여 더 빠른 처리\",\n    \"description\": \"Cropping info description\"\n  },\n  \"micOnToast\": {\n    \"message\": \"마이크 활성화됨\",\n    \"description\": \"Microphone enabled toast title\"\n  },\n  \"micOffToast\": {\n    \"message\": \"마이크 비활성화됨\",\n    \"description\": \"Microphone disabled toast title\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"UI 알림 숨기기\",\n    \"description\": \"Hide UI alerts button\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"다시 표시하지 않음\",\n    \"description\": \"Don't show again button\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"사용하지 않을 때 툴바 숨기기\",\n    \"description\": \"Only show toolbar on hover button\"\n  },\n  \"stopRecording\": {\n    \"message\": \"녹화 중지\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"새로운 Screenity에 오신 것을 환영합니다!\",\n    \"description\": \"기존 사용자를 위한 업데이트 공지 제목\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"새로운 디자인과 다양한 기능을 소개합니다. 걱정 마세요. 여전히 무료, 개인 정보 보호, 오픈 소스 확장 프로그램입니다.\",\n    \"description\": \"기존 사용자를 위한 업데이트 공지 설명\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"업데이트에 대한 자세한 정보 알아보기.\",\n    \"description\": \"기존 사용자를 위한 업데이트 공지 자세히 알아보기 링크\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"시작하기\",\n    \"description\": \"기존 사용자를 위한 업데이트 공지 버튼\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"녹화 시작 중 오류 발생\",\n    \"description\": \"스트림 오류 모달 제목\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"녹화를 시작하는 중에 오류가 발생한 것 같습니다. 브라우저를 다시 시작하고 다시 시도해보세요. 문제가 계속되면 support@screenity.io로 문의해주세요.\",\n    \"description\": \"스트림 오류 모달 설명\"\n  },\n  \"highestQuality\": {\n    \"message\": \"최상 품질\",\n    \"description\": \"팝업에서 최상 품질 레이블 토글\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"최근 녹음 복원\",\n    \"description\": \"팝업에서 최근 녹음 복원 버튼\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"문제가 있나요?\",\n    \"description\": \"에디터 내 문제 버튼\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"녹화물이 보이지 않나요?\",\n    \"description\": \"에디터 내 문제 모달 제목\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"녹화를 완료하고 비디오를 보지 못한 채 이 페이지에 남아 있다면, 사용 가능한 경우 원시 비디오 데이터를 다운로드하거나 버그 보고서를 제출할 수 있습니다.\",\n    \"description\": \"에디터 내 문제 모달 설명\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"원시 비디오 데이터 다운로드\",\n    \"description\": \"에디터 내 문제 모달 버튼\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"버그 신고\",\n    \"description\": \"에디터 내 문제 모달 두 번째 버튼\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"녹화된 것을 찾을 수 없습니다, 죄송합니다 :(\",\n    \"description\": \"에디터 내 녹화된 것을 찾을 수 없음 경고\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"메모리 한도 도달\",\n    \"description\": \"메모리 한도 도달 제목\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"귀하의 장치는 녹화를 위한 사용 가능한 메모리를 모두 소진하여 녹화가 자동으로 중지되었습니다. 더 긴 시간 동안 녹화하려면 비디오 품질을 낮추거나 장치의 저장 공간을 확보해보십시오.\",\n    \"description\": \"메모리 한도 도달 설명\"\n  },\n  \"understoodButton\": {\n    \"message\": \"이해함\",\n    \"description\": \"이해함 버튼\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"공간 부족\",\n    \"description\": \"공간 부족 제목\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity는 녹화를 위해 최소 1GB의 빈 공간이 필요합니다. 장치의 공간을 확보하고 다시 시도하십시오.\",\n    \"description\": \"공간 부족 설명\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"이전 녹화 삭제\",\n    \"description\": \"이전 녹화 삭제 버튼\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"고급 설정\",\n    \"description\": \"샌드박스 에디터 페이지의 고급 설정 섹션\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"원시 비디오 파일 다운로드\",\n    \"description\": \"원시 녹화 파일 다운로드 버튼\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"원본 녹화 데이터 내보내기\",\n    \"description\": \"원시 녹화 다운로드 레이블\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"녹화에 대한 도움말 받기\",\n    \"description\": \"문제 해결 버튼\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"비디오를 복구하고 다른 문제 해결하기\",\n    \"description\": \"문제 해결 레이블\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"원시 비디오 파일 다운로드\",\n    \"description\": \"원시 녹화 모달 제목\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"원시 비디오 파일은 Screenity에 의해 녹화된 원본 비디오 파일입니다. 이 비디오는 처리되거나 편집되지 않았으므로 매우 크며 모든 장치에서 재생되지 않을 수 있습니다. 처리된 비디오에 문제가 있는 경우이 파일을 사용하여 비디오를 복구할 수 있습니다.\",\n    \"description\": \"원시 녹화 모달 설명\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"다운로드\",\n    \"description\": \"원시 녹화 모달 버튼\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"시스템 정보 및 비디오 데이터 다운로드 및 문제 해결 전송\",\n    \"description\": \"문제 해결 모달 제목\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"다운로드 후 해당 파일을 support@screenity.io로 보내 주십시오. 우리는 이 데이터를 사용하여 확장 프로그램과 관련된 문제를 해결하고 가능한 경우 비디오를 복구하는 데 도움을 드리겠습니다.\",\n    \"description\": \"문제 해결 모달 설명\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"다운로드\",\n    \"description\": \"문제 해결 모달 버튼\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"비디오가 디바이스에서 처리하기에 너무 길어요\",\n    \"description\": \"5분 이상 제한 모달 제목\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Screenity는 완전히 개인적이며 로컬에서 실행되므로 디바이스의 하드웨어에 제한됩니다. 이 길이의 비디오를 처리하는 데 시간이 오래 걸리고 많은 리소스를 필요로 할 것입니다. 걱정하지 마세요. 여전히 WEBM 파일이나 원시 녹화를 다운로드할 수 있지만 편집 및 MP4 내보내기는 사용할 수 없습니다. 그럼에도 불구하고, 이 비디오를 처리해 보려고 할 수 있습니다. 단, 위험을 이해하는 경우에만 시도하십시오.\",\n    \"description\": \"5분 이상 제한 모달 설명\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"위험을 이해하고 시도해 보겠습니다\",\n    \"description\": \"5분 이상 제한 모달 버튼\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"긴 비디오 처리에 대해 자세히 알아보기\",\n    \"description\": \"5분 이상 제한 모달에서 자세히 알아보기 링크와 점이 포함된 설명\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"복구 모드\",\n    \"description\": \"복구 모드 제목\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"문제 해결을 위한 데이터 다운로드\",\n    \"description\": \"문제 해결을 위한 다운로드 옵션\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"도움말 센터\",\n    \"description\": \"도움말 탐색 항목\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"페이지 오디오 녹음\",\n    \"description\": \"오디오 경고 제목\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"이 페이지의 오디오를 녹음하려면 '$tab$' 옵션을 사용해야 합니다.\",\n    \"description\": \"오디오 경고 설명\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"탭 영역\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"페이지에서 지원되지 않는 확장 프로그램\",\n    \"description\": \"탭에서 지원되지 않는 확장 프로그램 제목\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity는 이전 탭에서 지원되지 않았습니다. 그럼에도 불구하고 여기에서 녹음을 시작한 다음 다른 탭으로 전환할 수 있지만 카메라와 도구 모음은 표시되지 않습니다.\",\n    \"description\": \"탭에서 지원되지 않는 확장 프로그램 설명\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Screenity 녹음을 위한 로컬 백업 설정\",\n    \"description\": \"백업 제목\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"생성한 모든 녹음의 사본을 유지하세요. 백업을 설정하지 않으면 녹음은 다운로드하기로 결정할 때만 저장됩니다.\",\n    \"description\": \"백업 설명\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"시스템 다운로드 폴더와 같은 폴더에 저장하려면 먼저 새 폴더를 만들어야 할 수 있습니다.\",\n    \"description\": \"백업 설명\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"폴더 선택\",\n    \"description\": \"폴더 선택 버튼\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"지금은 아님\",\n    \"description\": \"지금 아님 버튼\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"녹음이 로컬로 동기화됨\",\n    \"description\": \"활성화된 백업 제목\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"녹음할 때마다 권한을 요청하지 않으려면 이 탭을 열어두는 것을 권장합니다.\",\n    \"description\": \"활성화된 백업 설명\"\n  },\n  \"backupsClose\": {\n    \"message\": \"그래도 탭 닫기\",\n    \"description\": \"그래도 탭 닫기 버튼\"\n  },\n  \"backupsStop\": {\n    \"message\": \"모든 녹음의 백업 중지\",\n    \"description\": \"백업 중지 버튼\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Screenity 로컬 백업 폴더에 다시 액세스 권한을 확인하세요.\",\n    \"description\": \"백업 확인 제목\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"죄송하지만 녹음의 로컬 사본을 계속 만들기 위해 다시 권한이 필요합니다. 불행하게도 이 탭을 닫을 때마다 묻게 될 것이며, 이는 브라우저를 닫거나 컴퓨터를 다시 시작하는 경우에 발생할 수 있습니다.\",\n    \"description\": \"백업 확인 설명\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Screenity에게 녹음 백업 권한 부여\",\n    \"description\": \"백업 확인에서 허용 버튼\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"녹음 백업 생성\",\n    \"description\": \"녹음 백업 전환 레이블\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"녹음 백업 권한 부여 실패\",\n    \"description\": \"녹음 백업 권한 부여 실패 제목\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"녹음의 로컬 사본을 만들기 위한 폴더를 선택하지 않았습니다. 브라우저 또는 운영 체제 버전에 따라 녹음을 시작할 때마다 폴더를 선택하라는 메시지가 나올 수 있습니다. 다시 시도하거나 권한을 반복적으로 부여하지 않으려면 백업을 완전히 비활성화할 수 있습니다.\",\n    \"description\": \"녹음 백업 권한 부여 실패 설명\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"컴퓨터 오디오 녹음\",\n    \"description\": \"macOS용 오디오 경고 제목\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"macOS에서는 탭에서만 오디오를 녹음할 수 있습니다. 'Chrome 탭' 옵션을 선택하고 '탭 오디오도 공유'가 활성화되어 있는지 확인하세요.\",\n    \"description\": \"macOS용 오디오 경고 설명\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"컴퓨터 오디오 녹음\",\n    \"description\": \"기타 시스템용 오디오 경고 제목\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"컴퓨터 오디오를 녹음하려면 '시스템 오디오 공유' 옵션을 활성화하세요. 이 옵션을 보지 못하는 경우 Chrome을 업데이트하거나 탭 녹음 옵션으로 전환해 보세요.\",\n    \"description\": \"기타 시스템용 오디오 경고 설명\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"최대 해상도\",\n    \"description\": \"설정 메뉴에서 최대 해상도 레이블\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"최대 FPS\",\n    \"description\": \"설정 메뉴에서 최대 FPS 레이블\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"RAM 부족\",\n    \"description\": \"설정 메뉴에서 'RAM 부족' 라벨\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"창 크기 조절\",\n    \"description\": \"설정 메뉴의 창 크기 조절 레이블\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"이 화면은 이 해상도로 조절하기에 너무 작습니다\",\n    \"description\": \"설정 메뉴의 화면 너무 작음 툴팁\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"귀하의 장치에서 이 해상도로 녹화할 수 없습니다\",\n    \"description\": \"설정 메뉴의 최대 해상도 툴팁\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"권한 검토\",\n    \"description\": \"권한 검토 버튼\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"녹화 추가\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"준비 중...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"잠시만 기다려 주세요. 녹화를 준비하고 있어요.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"툴바가 숨겨졌습니다. 팝업 옵션에서 다시 활성화하세요.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"로그인 또는 가입\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"로그아웃\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"로그아웃되었습니다\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"저장 공간 한도 도달\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"100GB 저장 공간을 모두 사용하셨습니다. 계속 녹화하려면 이전 영상이나 장면을 삭제해 주세요.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"저장 공간 관리\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"닫기\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"저장 공간 확인 실패\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"저장 공간을 확인하는 중 문제가 발생했습니다. 다시 로그인하고 녹화를 시도해 주세요.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"저장 공간을 확인할 수 없습니다. 잠시 후 다시 시도하거나 페이지를 새로고침해 주세요.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"다시 시도\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"장면이 Multi 녹화에 추가되었습니다\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"새 장면을 녹화할 준비가 되었습니다\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"녹화가 90분 제한에 가까워지고 있어 곧 종료됩니다\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"90분 녹화 제한으로 녹화가 종료되었습니다\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"이 페이지에서는 탭 녹화가 비활성화되어 있어 화면 녹화로 전환되었습니다.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"영상 녹화가 취소되었습니다\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"링크가 클립보드에 복사되었습니다\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"링크 복사에 실패했습니다\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"최신순\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"오래된순\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"모든 영상\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"영상이 없습니다\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"영상을 불러오는 중...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"대시보드로 이동\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Screenity에 오신 것을 환영합니다\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"광고 없음. 추적 없음. 제한 없음.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"그냥 녹화 버튼만 누르세요.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"가입 없이 무료로 사용해보세요!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"영상으로 더 많은 걸 해보고 싶으신가요?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"클라우드 저장소, 링크로 공유\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"여러 클립을 하나의 스토리로 결합\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"클릭 시 스마트 줌 (또는 직접 추가!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"템플릿, 자막, 레이아웃 추가...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"맞춤형 배경 및 목업\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 추가 기능 잠금 해제\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ 1인 인디 개발자를 응원해 주세요!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"계정 설정\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"지원\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Pro 구독이 비활성화되었습니다\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Screenity Pro 구독이 비활성화되어 있습니다.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"접근을 재개하려면 구독을 다시 활성화해 주세요.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"다음 날짜에 영상과 데이터가 영구적으로 삭제됩니다: \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"구독 관리\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"무료 버전을 계속 사용하시겠어요?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"로그아웃하고 무료 버전으로 전환\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"로그아웃되었습니다\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"녹화를 계정과 동기화하고 프리미엄 기능을 사용하려면 다시 로그인해야 합니다.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"다시 로그인\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"계정 없이도 확장 기능을 계속 사용할 수 있지만, 녹화는 저장되지 않으며 나중에 복구할 수 없습니다.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"로그인 없이 계속하기\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"즉시 녹화 모드\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"즉시 다운로드 가능하지만, 카메라 및 레이아웃은 나중에 편집할 수 없습니다.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"즉시 녹화 모드\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"이 모드는 모든 것을 하나의 영상으로 녹화하여 즉시 다운로드 및 공유할 수 있게 해줍니다. 나중에 카메라 레이아웃은 변경할 수 없지만, 다른 편집은 가능합니다.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"확인\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"이 페이지에서는 탭 녹화가 비활성화되어 있습니다\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"추가 중: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"완료\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"녹화로 더 많은 것을 해보고 싶으신가요?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"영상을 클라우드에 저장하고 링크로 공유하며 고급 편집 기능을 사용하려면 로그인하세요.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"유료 기능 잠금 해제를 위해 로그인\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"1인 인디 개발자를 응원해 주세요\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"더 많은 기능 잠금 해제\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"공유하려면 로그인 (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"클라우드 공유, 멀티 장면 편집, 자동 줌, 자막 등 기능 잠금 해제\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity는 항상 무료, 오픈소스, 광고 없이 제공됩니다. Pro는 클라우드 및 인프라 비용을 지원하며, 1인 개발자의 개발을 돕습니다! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"다시 표시하지 않기\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"체험해 보기\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"멀티 모드\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"화면, 카메라 또는 둘 다를 순차적으로 녹화하세요. 여러 번 촬영하거나 시점을 바꾸거나 녹화를 여러 부분으로 나누기에 좋습니다. 완료되면 '완료'를 클릭하여 모든 장면이 하나의 영상으로 결합된 편집기를 열 수 있습니다.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Pro를 활성화하여 시작하세요\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Pro 기능에 접근하려면 구독해 주세요.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Pro 구독하기\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"로컬에서 Screenity를 잘 사용하고 계신가요? 개발을 응원해 주세요!\",\n    \"description\": \"에디터에서의 셀프 호스트 배너 제목\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity는 여성 1인 개발자가 만든 프로젝트입니다. 셀프 호스팅은 무료지만, 도움이 되셨다면 Pro로 후원해 주세요 ❤️\",\n    \"description\": \"에디터에서의 셀프 호스트 배너 설명\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Screenity에 다시 오신 걸 환영해요\",\n    \"description\": \"다시 방문한 사용자에게 보여지는 환영 팝업의 제목\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ 새로워졌어요: 녹화 그 이상을 원하시나요?\",\n    \"description\": \"환영 팝업에 표시되는 클라우드 저장 기능의 제목\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro는 동영상을 클라우드에 저장하고, 링크로 공유하며, 고급 편집 도구를 사용할 수 있는 선택적인 새로운 플랫폼이에요. 필요할 때 언제든 사용할 수 있어요.\",\n    \"description\": \"환영 팝업에 표시되는 클라우드 저장 기능 설명\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Screenity Pro 자세히 알아보기\",\n    \"description\": \"환영 팝업의 클라우드 저장 기능에 대한 클릭 유도 문구\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Screenity Pro에 오신 것을 환영합니다\",\n    \"description\": \"Pro 환영 모달 제목\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"이제 녹화를 시작할 준비가 완료되었습니다! 몇 가지 참고 사항:\\n\\n- 녹화 중에는 카메라 버블이 사라질 수 있지만 정상입니다. 카메라는 배경에서 별도로 캡처되므로 나중에 원하는 위치로 조정할 수 있습니다.\\n- Chrome 탭 내에서 클릭할 때만 자동으로 줌이 생성됩니다. 이는 브라우저의 제한 사항입니다. 녹화 후 수동으로 줌을 추가할 수 있습니다.\\n- 즉시 다운로드가 가능한 빠른 녹화를 원하신다면, 인스턴트 모드를 사용해 보세요. 단, 이 모드에서는 배경, 레이아웃, 고급 설정 등의 편집 기능이 제한됩니다.\",\n    \"description\": \"Pro 환영 모달 설명\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"알겠어요\",\n    \"description\": \"Pro 환영 모달 확인 버튼\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 실패 — 꺼짐\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Screenity Pro 확장 프로그램에 오신 것을 환영합니다\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"녹화를 시작하기 전에 빠른 팁을 확인하세요.<br/>자동 줌은 <strong>Chrome 탭</strong> 안에서의 클릭에만 동작합니다. 이후에 수동으로 줌을 추가할 수 있습니다.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"녹화 툴바 및 효과\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"이 툴바에서 그리기, 커서 효과, 블러, 녹화 제어를 할 수 있습니다.<br/><br/><strong>툴바를 숨기지 않으면 영상에 함께 녹화됩니다</strong>.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"카메라는 별도로 캡처됩니다\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"녹화 중 카메라가 <strong>숨겨지거나 PiP로 이동</strong>할 수 있으며 정상 동작입니다.<br/><br/>카메라는 별도로 캡처되므로 나중에 원하는 위치로 배치할 수 있습니다.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"인스턴트 모드\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"빠른 공유와 <strong>무제한 다운로드</strong>에 가장 적합합니다.<br/><br/>이 모드에서는 고급 레이아웃/편집 옵션을 사용할 수 없습니다.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"확인\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"다음\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"뒤로\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"자세히 보기\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"온보딩 다시 시작\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"디스크 공간이 부족합니다. 녹화를 저장하고 있어요. 지금까지 캡처된 내용은 안전합니다.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"긴 녹화\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"녹화가 완료되었고 안전합니다. 아래에서 WebM으로 다운로드하세요.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"긴 녹화에는 사용할 수 없어요\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"복구 모드에서 사용할 수 없어요\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"오디오 편집이 너무 깁니다\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"이 오디오 편집은 15분이 넘는 클립에는 지원되지 않습니다. 원본은 변경되지 않았습니다. 먼저 클립을 다듬어 보세요.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"편집 시간이 초과되었습니다\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"편집이 제시간에 완료되지 않았습니다. 원본 녹화는 변경되지 않았습니다. 다시 시도하거나 먼저 클립을 다듬어 보세요.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"화면 공유가 중단되었어요. 녹화는 저장되었습니다. 준비되면 중지하세요.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"처리 중 문제가 있었지만 녹화는 안전해요. 아래에서 다운로드할 수 있습니다.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"편집을 처리하고 있어요. 원본 녹화는 변경되지 않았습니다.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"편집을 적용할 수 없어요\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"처리 중 문제가 발생했어요. 원본 녹화는 안전합니다. 다시 시도하거나 그대로 다운로드할 수 있어요.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"화면 공유가 중단되었어요. 녹화를 저장하고 있습니다. 지금까지 캡처된 내용은 안전합니다.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"오디오가 연결 해제되었어요. 녹화를 저장하고 있습니다. 지금까지 캡처된 영상은 안전합니다.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"녹화가 저장되었어요. 편집기가 열리지 않았습니다. Screenity 아이콘을 클릭해서 다시 시도하세요.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"저장이 예상보다 오래 걸리고 있어요. 데이터는 안전합니다. 편집기가 곧 열릴 거예요.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"디버그 정보 복사\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"디버그 정보가 복사되었어요\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"도움 받기\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"녹화 시간이 너무 짧아 저장할 수 없습니다\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"빠른 녹화기 실패\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"이 기기에서 빠른 녹화기 출력을 검증할 수 없습니다.\\n파일을 그대로 다운로드할 수 있습니다.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"그래도 다운로드\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"취소\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/pl/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Nagrywarka & Edytor adnotacji\",\n    \"description\": \"Nazwa rozszerzenia\"\n  },\n  \"extDesc\": {\n    \"message\": \"Najlepszy darmowy rejestrator ekranu bez ograniczeń. Przechwytuj, adnotuj, przybliżaj, rozmywaj, edytuj filmy i wiele więcej - bez konieczności logowania i przyjazny dla prywatności.\",\n    \"description\": \"Opis rozszerzenia\"\n  },\n  \"recordTab\": {\n    \"message\": \"Nagrywaj\",\n    \"description\": \"Etykieta zakładki Nagrywaj na okienku popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Twoje filmy\",\n    \"description\": \"Etykieta zakładki Twoje filmy na okienku popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Ekran\",\n    \"description\": \"Etykieta rodzaju nagrywania ekranu na okienku popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Obszar karty\",\n    \"description\": \"Etykieta rodzaju nagrywania obszaru karty na okienku popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Kamera\",\n    \"description\": \"Etykieta rodzaju nagrywania kamery na okienku popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etykieta rodzaju nagrywania mockup na okienku popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Nagrywanie niestandardowego obszaru wyłączone\",\n    \"description\": \"Ostrzeżenie o wyłączonym nagrywaniu niestandardowego obszaru na okienku popup dla wersji Chrome starszych niż 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Zaktualizuj wersję Chrome do 110+\",\n    \"description\": \"Opis ostrzeżenia o wyłączonym nagrywaniu niestandardowego obszaru na okienku popup dla wersji Chrome starszych niż 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Zaktualizuj\",\n    \"description\": \"Działanie w ostrzeżeniu o wyłączonym nagrywaniu niestandardowego obszaru na okienku popup dla wersji Chrome starszych niż 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Sprawdź swoje uprawnienia\",\n    \"description\": \"Tytuł modalnego okienka ostrzeżenia o uprawnieniach kamery / mikrofonu\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Wygląda na to, że Screenity nie ma dostępu do twojej kamery lub mikrofonu. Upewnij się, że zezwoliłeś na dostęp, klikając ikonę kamery w pasku adresu.\",\n    \"description\": \"Opis modalnego okienka ostrzeżenia o uprawnieniach kamery / mikrofonu\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Odrzuć\",\n    \"description\": \"Przycisk odrzucenia w modalnym okienku ostrzeżenia o uprawnieniach kamery / mikrofonu\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Zezwól na dostęp do kamery\",\n    \"description\": \"Przycisk zezwalający na dostęp do kamery\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Zezwól na dostęp do mikrofonu\",\n    \"description\": \"Przycisk zezwalający na dostęp do mikrofonu\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Naciśnij, aby mówić\",\n    \"description\": \"Etykieta przełącznika Naciśnij, aby mówić\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Ustaw obszar do nagrywania\",\n    \"description\": \"Etykieta przełącznika Niestandardowy obszar\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Obróć kamerę\",\n    \"description\": \"Etykieta przełącznika Obróć kamerę\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efekty tła\",\n    \"description\": \"Etykieta przełącznika efektów tła\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Rozpocznij nagrywanie\",\n    \"description\": \"Etykieta przycisku nagrywania\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Rozpoczynanie nagrywania...\",\n    \"description\": \"Etykieta przycisku nagrywania w trakcie\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Wybierz kamerę do nagrywania\",\n    \"description\": \"Etykieta przycisku nagrywania w trybie kamery, gdy nie wybrano kamery\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Pokaż więcej opcji\",\n    \"description\": \"Etykieta przycisku Pokaż więcej opcji\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Uwzględnij dźwięk systemowy\",\n    \"description\": \"Etykieta dźwięku systemowego/karta\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Ukryj pasek narzędzi\",\n    \"description\": \"Etykieta ukrywania paska narzędzi\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Odliczanie\",\n    \"description\": \"Etykieta odliczania\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Ustaw limit czasu\",\n    \"description\": \"Etykieta alarmu\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Rozmycie\",\n    \"description\": \"Etykieta rozmywania tła\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Brak\",\n    \"description\": \"Brak wybranej urządzenia\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Brak kamery\",\n    \"description\": \"Brak wybranej kamery\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Brak mikrofonu\",\n    \"description\": \"Brak wybranego mikrofonu\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Wybierz źródło\",\n    \"description\": \"Etykieta miejsca na wybór źródła\"\n  },\n  \"offLabel\": {\n    \"message\": \"Wyłączone\",\n    \"description\": \"Etykieta wyłączenia w rozwijanej liście\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Szerokość\",\n    \"description\": \"Etykieta szerokości regionu\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Wysokość\",\n    \"description\": \"Etykieta wysokości regionu\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Kliknij, aby umieścić obraz\",\n    \"description\": \"Powiadomienie toast, które pojawia się podczas dodawania nowego obrazu\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Zakończ nagrywanie\",\n    \"description\": \"Podpowiedź Zakończ nagrywanie\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Rozpocznij nagrywanie ponownie\",\n    \"description\": \"Podpowiedź Rozpocznij nagrywanie ponownie\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Wstrzymaj nagrywanie\",\n    \"description\": \"Podpowiedź Wstrzymaj nagrywanie\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Wznów nagrywanie\",\n    \"description\": \"Podpowiedź Wznów nagrywanie\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Anuluj nagrywanie\",\n    \"description\": \"Podpowiedź Anuluj nagrywanie\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Przełącz narzędzia do rysowania\",\n    \"description\": \"Podpowiedź Przełącz narzędzia do rysowania\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Przełącz narzędzia do rozmywania\",\n    \"description\": \"Podpowiedź Przełącz narzędzia do rozmywania\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Przełącz opcje kursora\",\n    \"description\": \"Podpowiedź Przełącz opcje kursora\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Wyłącz kamerę\",\n    \"description\": \"Podpowiedź Wyłącz kamerę\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Włącz kamerę\",\n    \"description\": \"Podpowiedź Włącz kamerę\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Brak uprawnień do kamery\",\n    \"description\": \"Podpowiedź Brak uprawnień do kamery\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Wyłącz mikrofon\",\n    \"description\": \"Podpowiedź Wyłącz mikrofon\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Włącz mikrofon\",\n    \"description\": \"Podpowiedź Włącz mikrofon\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Brak uprawnień do mikrofonu\",\n    \"description\": \"Podpowiedź Brak uprawnień do mikrofonu\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Wybierz narzędzie\",\n    \"description\": \"Podpowiedź Wybierz narzędzie\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Narzędzie pióro\",\n    \"description\": \"Podpowiedź Narzędzie pióro\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Narzędzie zakreślania\",\n    \"description\": \"Podpowiedź Narzędzie zakreślania\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Narzędzie gumki\",\n    \"description\": \"Podpowiedź Narzędzie gumki\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Narzędzie tekstowe\",\n    \"description\": \"Podpowiedź narzędzia tekstowego\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Narzędzie kształtów\",\n    \"description\": \"Podpowiedź narzędzia kształtów\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Narzędzie strzałki\",\n    \"description\": \"Podpowiedź narzędzia strzałki\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Narzędzie obrazu\",\n    \"description\": \"Podpowiedź narzędzia obrazu\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Cofnij\",\n    \"description\": \"Podpowiedź cofania\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Ponów\",\n    \"description\": \"Podpowiedź ponawiania\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Wyczyść płótno\",\n    \"description\": \"Podpowiedź wyczyszczenia płótna\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Więcej kolorów\",\n    \"description\": \"Podpowiedź więcej kolorów\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Gruba kreska\",\n    \"description\": \"Podpowiedź grubej kreski\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Średnia kreska\",\n    \"description\": \"Podpowiedź średniej kreski\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Cienka kreska\",\n    \"description\": \"Podpowiedź cienkiej kreski\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Przełącz tryb obraz w obrazie\",\n    \"description\": \"Podpowiedź przełączania trybu obraz w obrazie\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Włącz/wyłącz wypełnienie\",\n    \"description\": \"Podpowiedź włączania/wyłączania wypełnienia\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Tryb rysowania\",\n    \"description\": \"Komunikat trybu rysowania\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Tryb rozmywania\",\n    \"description\": \"Komunikat trybu rozmywania\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Wyczyść wszystkie elementy rozmyte\",\n    \"description\": \"Podpowiedź wyczyszczenia wszystkich elementów rozmytych\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Podświetl kliknięcia\",\n    \"description\": \"Podpowiedź podświetlania kliknięć\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Podświetl kursor\",\n    \"description\": \"Podpowiedź podświetlania kursora\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Reflektor na kursorze\",\n    \"description\": \"Podpowiedź reflektora na kursorze\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Nie pokazuj ponownie\",\n    \"description\": \"Przycisk odrzucenia modalu uprawnień\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Czy na pewno chcesz zrestartować nagrywanie?\",\n    \"description\": \"Tytuł modalu restartowania nagrywania\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Bieżący postęp wideo zostanie utracony.\",\n    \"description\": \"Opis modalu restartowania nagrywania\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Zrestartuj nagrywanie\",\n    \"description\": \"Przycisk restartowania nagrywania\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Wznów nagrywanie\",\n    \"description\": \"Przycisk wznowienia nagrywania\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Czy na pewno chcesz odrzucić nagranie?\",\n    \"description\": \"Tytuł modalu odrzucenia nagrania\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Bieżący postęp wideo zostanie utracony.\",\n    \"description\": \"Opis modalu odrzucenia nagrania\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Odrzuć nagranie\",\n    \"description\": \"Przycisk odrzucenia nagrania\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Wznów nagrywanie\",\n    \"description\": \"Przycisk wznowienia nagrywania\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Wybierz, co chcesz nagrać\",\n    \"description\": \"Tytuł na stronie nagrywania\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Nagrywanie...\",\n    \"description\": \"Tytuł na stronie nagrywania podczas nagrywania\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Zachowaj tę kartę otwartą. Zostanie zamknięta po zapisaniu nagrania.\",\n    \"description\": \"Opis na stronie nagrywania\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Przygotowanie nagrania...\",\n    \"description\": \"Tytuł na stronie piaskownicy podczas nagrywania/zapisywania\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Zachowaj tę kartę otwartą. Zaktualizuje się z twoim nagraniem, gdy będzie gotowe.\",\n    \"description\": \"Opis na stronie piaskownicy podczas nagrywania/zapisywania\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Edytor\",\n    \"description\": \"Tytuł w nawigacji strony edytora piaskownicy\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Anuluj\",\n    \"description\": \"Przycisk anulowania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Przywróć do oryginału\",\n    \"description\": \"Przycisk przywracania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Reset\",\n    \"description\": \"Przycisk resetowania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Zapisz zmiany\",\n    \"description\": \"Przycisk zapisywania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Zapisywanie...\",\n    \"description\": \"Przycisk zapisywania na stronie edytora piaskownicy podczas zapisywania\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Przeciągnij i upuść plik dźwiękowy\",\n    \"description\": \"Przeciągnij i upuść plik dźwiękowy na stronie edytora piaskownicy\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Lub kliknij, aby przeglądać\",\n    \"description\": \"Lub kliknij, aby przeglądać plik dźwiękowy na stronie edytora piaskownicy\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Ustawienia\",\n    \"description\": \"Tytuł ustawień audio na stronie edytora piaskownicy\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Głośność\",\n    \"description\": \"Etykieta głośności audio na stronie edytora piaskownicy\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aktualizuj\",\n    \"description\": \"Przycisk aktualizacji audio na stronie edytora piaskownicy\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Przytnij\",\n    \"description\": \"Tytuł przytnij na stronie edytora piaskownicy\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Szerokość\",\n    \"description\": \"Etykieta szerokości dla właściwości\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Wysokość\",\n    \"description\": \"Etykieta wysokości dla właściwości\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Lewo\",\n    \"description\": \"Etykieta lewa dla właściwości\"\n  },\n  \"topLabel\": {\n    \"message\": \"Góra\",\n    \"description\": \"Etykieta góra dla właściwości\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Przeciągnij uchwyty i użyj przycisków po lewej stronie, aby edytować wybrany fragment.\",\n    \"description\": \"Informacja o przycinaniu na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Przytnij wideo\",\n    \"description\": \"Przycisk przycinania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Przycinanie...\",\n    \"description\": \"Przycisk przycinania na stronie edytora piaskownicy podczas przycinania\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Wycięcie sekcji\",\n    \"description\": \"Przycisk wycinania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Wycinanie...\",\n    \"description\": \"Przycisk wycinania na stronie edytora piaskownicy podczas wycinania\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Wycisz dźwięk\",\n    \"description\": \"Przycisk wyciszania na stronie edytora piaskownicy\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Wyciszanie...\",\n    \"description\": \"Przycisk wyciszania na stronie edytora piaskownicy podczas wyciszania\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Dowiedz się więcej.\",\n    \"description\": \"Dowiedz się więcej z kropką\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Cofnij\",\n    \"description\": \"Etykieta cofnij\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Powtórz\",\n    \"description\": \"Etykieta powtórz\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Zostaw recenzję\",\n    \"description\": \"Przycisk napisz recenzję\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Śledź aktualizacje\",\n    \"description\": \"Przycisk śledź aktualizacje\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Jesteś obecnie offline\",\n    \"description\": \"Etykieta offline\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Niektóre funkcje są niedostępne do ponownego połączenia\",\n    \"description\": \"Opis etykiety offline\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Spróbuj ponownie\",\n    \"description\": \"Przycisk etykiety offline Spróbuj ponownie\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Konieczna aktualizacja Chrome'a\",\n    \"description\": \"Etykieta aktualizacji Chrome'a\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Zaktualizuj, aby uzyskać dostęp do więcej funkcji\",\n    \"description\": \"Opis etykiety aktualizacji Chrome'a\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Dowiedz się więcej\",\n    \"description\": \"Przycisk dowiedz się więcej\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Film jest zbyt długi, aby przetwarzać lokalnie\",\n    \"description\": \"Etykieta limitu powyżej 5 minut w edytorze\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Edycja i eksport do formatu MP4 są niedostępne\",\n    \"description\": \"Opis limitu powyżej 5 minut w edytorze\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Wideo jest przetwarzane...\",\n    \"description\": \"Etykieta przetwarzania wideo w edytorze\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Uaktualnij, aby przyspieszyć edycję\",\n    \"description\": \"Opis przetwarzania wideo w edytorze\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Edytuj\",\n    \"description\": \"Tytuł edytora w piaskownicy\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Brak połączenia\",\n    \"description\": \"Etykieta braku połączenia\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Niedostępne\",\n    \"description\": \"Etykieta niedostępności\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Edytuj wideo\",\n    \"description\": \"Przycisk etykiety przycinania\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Przytnij, wytnij lub wycisz wideo\",\n    \"description\": \"Etykieta przycinania\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Przygotowywanie...\",\n    \"description\": \"Etykieta przygotowywania\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Przytnij\",\n    \"description\": \"Przycisk etykiety przycinania\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Przytnij i zmień rozmiar wideo\",\n    \"description\": \"Etykieta przycinania\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Dodaj dźwięk\",\n    \"description\": \"Przycisk etykiety dodawania dźwięku\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Prześlij własny dźwięk, aby dodać go do wideo\",\n    \"description\": \"Etykieta dodawania dźwięku\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Zapisz\",\n    \"description\": \"Tytuł zapisu w edytorze w piaskownicy\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Wyloguj się z Dysku\",\n    \"description\": \"Etykieta wylogowania z Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Trwa zapisywanie...\",\n    \"description\": \"Etykieta zapisywania w Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Zapisz w Dysku\",\n    \"description\": \"Przycisk zapisywania w Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Zapisz bieżącą wersję w Google Drive\",\n    \"description\": \"Etykieta zapisywania w Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Zaloguj się i zapisz w Dysku\",\n    \"description\": \"Etykieta logowania do Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Eksportuj\",\n    \"description\": \"Tytuł eksportu w edytorze w piaskownicy\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Pobieranie...\",\n    \"description\": \"Etykieta pobierania\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Pobierz jako .webm\",\n    \"description\": \"Przycisk pobierania jako WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Eksportuj wideo jako .webm\",\n    \"description\": \"Etykieta pobierania WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Pobierz jako .mp4\",\n    \"description\": \"Przycisk pobierania jako MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Eksportuj wideo jako .mp4 (zalecane)\",\n    \"description\": \"Etykieta pobierania MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Pobierz jako .gif\",\n    \"description\": \"Przycisk pobierania jako GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Eksportuj jako GIF (maks. 30 sekund)\",\n    \"description\": \"Etykieta pobierania GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Gotowy, by ulepszyć swoje nagrania?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Udostępniaj w chmurze, używaj zoomów, szablonów... i wspieraj narzędzie stworzone przez niezależną deweloperkę, która dba o prywatność ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Dowiedz się więcej o Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Masz już konto? Zaloguj się\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Udostępnij wideo\",\n    \"description\": \"Przycisk udostępniania w edytorze w piaskownicy\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Zamień istniejący dźwięk\",\n    \"description\": \"Przycisk zamiany dźwięku w edytorze\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Powiększ do kursora\",\n    \"description\": \"Okno modalne powiększenia do punktu\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Zostań na stronie podczas nagrywania\",\n    \"description\": \"Okno modalne pozostawania na stronie\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Ostrzeżenie o wyłączonym mikrofonie\",\n    \"description\": \"Okno modalne przypomnienia o mikrofonie\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Pomoc\",\n    \"description\": \"Przycisk okna modalnego pomocy\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Nagrywanie wstrzymane. Naciśnij przycisk odtwarzania, aby wznowić.\",\n    \"description\": \"Tytuł modalnego powiadomienia o wstrzymanym nagrywaniu\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity nie ma uprawnień do nagrywania twojego ekranu\",\n    \"description\": \"Tytuł okna modalnego z uprawnieniami\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Aby włączyć nagrywanie ekranu, musisz udzielić Screenity uprawnień do przechwytywania twojego ekranu, gdy zostaniesz o to poproszony.\",\n    \"description\": \"Opis okna modalnego z uprawnieniami\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Włącz nagrywanie ekranu\",\n    \"description\": \"Akcja w oknie modalnym z uprawnieniami\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Anuluj\",\n    \"description\": \"Anuluj w oknie modalnym z uprawnieniami\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Twój mikrofon jest wyciszony\",\n    \"description\": \"Tytuł modalnego komunikatu o wyciszonym mikrofonie\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Aby uwzględnić dźwięk w nagraniu, musisz włączyć mikrofon. Czy chcesz kontynuować bez dźwięku?\",\n    \"description\": \"Opis modalnego komunikatu o wyciszonym mikrofonie\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Tak, kontynuuj\",\n    \"description\": \"Akcja w modalnym komunikacie o wyciszonym mikrofonie\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Anuluj\",\n    \"description\": \"Anuluj w modalnym komunikacie o wyciszonym mikrofonie\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Rozpocznij korzystanie z Screenity w trzech prostych krokach:\",\n    \"description\": \"Tytuł kroków konfiguracji\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Kliknij ikonę \",\n    \"description\": \"Krok 1 konfiguracji, przed ikoną. Wymaga spacji na końcu.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" rozszerzeń\",\n    \"description\": \"Krok 1 konfiguracji, po ikonie. Wymaga spacji na początku.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Naciśnij ikonę \",\n    \"description\": \"Krok 2 konfiguracji, przed ikoną. Wymaga spacji na końcu.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" pinek\",\n    \"description\": \"Krok 2 konfiguracji, po ikonie. Wymaga spacji na początku.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Kliknij ikonę \",\n    \"description\": \"Krok 3 konfiguracji, przed ikoną. Wymaga spacji na końcu.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity, aby rozpocząć\",\n    \"description\": \"Krok 3 konfiguracji, po ikonie. Wymaga spacji na początku.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Super! Wszystko gotowe\",\n    \"description\": \"Tytuł komunikatu o zakończeniu konfiguracji\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Teraz możesz rozpocząć nagrywanie tutaj lub w innej karcie.\",\n    \"description\": \"Opis komunikatu o zakończeniu konfiguracji\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Kliknij tutaj, aby rysować\",\n    \"description\": \"Komunikat o kliknięciu tutaj\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Spokojnie. Kliknij gdziekolwiek, aby zatrzymać odliczanie.\",\n    \"description\": \"Komunikat odliczania\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Edycje mogą być niedokładne dla krótkich zakresów czasowych z powodu interwałów między klatkami.\",\n    \"description\": \"Informacja o zbyt małym edytorze\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Wideo jest przetwarzane, odtwarzanie może być niedokładne lub przerwane.\",\n    \"description\": \"Baner przetwarzania w edytorze\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Przycinanie może chwilę potrwać\",\n    \"description\": \"Tytuł informacji o przycinaniu\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Uaktualnij, aby przyspieszyć przetwarzanie\",\n    \"description\": \"Opis informacji o przycinaniu\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Mikrofon włączony\",\n    \"description\": \"Tytuł modalnego komunikatu o włączonym mikrofonie\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Mikrofon wyłączony\",\n    \"description\": \"Tytuł modalnego komunikatu o wyłączonym mikrofonie\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Ukryj powiadomienia interfejsu\",\n    \"description\": \"Przycisk ukrywania alertów interfejsu\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Nie pokazuj ponownie\",\n    \"description\": \"Przycisk nie pokazuj ponownie\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Ukryj pasek narzędzi, gdy nie jest używany\",\n    \"description\": \"Pokaż pasek narzędzi tylko po najechaniu myszką\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Zatrzymaj nagrywanie\",\n    \"description\": \"Przycisk zatrzymywania nagrywania na ekranie nagrywania\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Witaj w nowym Screenity!\",\n    \"description\": \"Tytuł ogłoszenia o aktualizacji dla istniejących użytkowników\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Przedstawiamy nowy wygląd i wiele ekscytujących funkcji, ale nie martw się, to nadal ta sama darmowa, prywatna i otwarta rozszerzenie, które znasz i kochasz.\",\n    \"description\": \"Opis ogłoszenia o aktualizacji dla istniejących użytkowników\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Dowiedz się więcej o aktualizacji.\",\n    \"description\": \"Link do dowiedzenia się więcej o aktualizacji w ogłoszeniu dla istniejących użytkowników\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Rozpocznij\",\n    \"description\": \"Przycisk w ogłoszeniu o aktualizacji dla istniejących użytkowników\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Błąd podczas rozpoczynania nagrywania\",\n    \"description\": \"Tytuł okna modalnego błędu strumienia\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Wygląda na to, że wystąpił błąd podczas rozpoczynania nagrywania. Uruchom przeglądarkę ponownie i spróbuj jeszcze raz. Jeśli problem będzie się utrzymywał, skontaktuj się z nami pod adresem support@screenity.io.\",\n    \"description\": \"Opis okna modalnego błędu strumienia\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Najwyższa jakość\",\n    \"description\": \"Etykieta przełącznika na najwyższą jakość w okienku\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Przywróć ostatnie nagranie\",\n    \"description\": \"Przycisk przywracania ostatniego nagrania w okienku\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Masz problemy?\",\n    \"description\": \"Przycisk problemów w edytorze\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Nie widzisz swojego nagrania?\",\n    \"description\": \"Tytuł okna modalnego problemów w edytorze\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Jeśli ukończyłeś nagrywanie i znajdujesz się na tej stronie, nie widząc swojego filmu, możesz spróbować pobrać surowe dane wideo, jeśli są dostępne. Możesz również skontaktować się z nami i zgłosić błąd.\",\n    \"description\": \"Opis okna modalnego problemów w edytorze\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Pobierz surowe dane wideo\",\n    \"description\": \"Przycisk w oknie modalnym problemów w edytorze\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Zgłoś błąd\",\n    \"description\": \"Drugi przycisk w oknie modalnym problemów w edytorze\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Nie znaleziono nagrania, przepraszamy :(\",\n    \"description\": \"Komunikat o braku nagrania w edytorze\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Osiągnięto limit pamięci\",\n    \"description\": \"Tytuł komunikatu o osiągniętym limicie pamięci\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Twój urządzenie wyczerpało dostępną pamięć do nagrywania, co spowodowało automatyczne zakończenie nagrywania. Jeśli chcesz nagrywać dłużej, możesz spróbować zmniejszyć jakość nagrania lub zwolnić miejsce na urządzeniu.\",\n    \"description\": \"Opis komunikatu o osiągniętym limicie pamięci\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Rozumiem\",\n    \"description\": \"Przycisk Rozumiem\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Za mało miejsca\",\n    \"description\": \"Tytuł komunikatu o braku miejsca\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Aby korzystać z Screenity, potrzebujesz co najmniej 1 GB wolnej przestrzeni. Proszę zwolnij miejsce na urządzeniu i spróbuj ponownie.\",\n    \"description\": \"Opis komunikatu o braku miejsca\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Usuń poprzednie nagrania\",\n    \"description\": \"Przycisk Usuń poprzednie nagrania\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Zaawansowane\",\n    \"description\": \"Sekcja zaawansowana na stronie edytora w trybie piaskownicy\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Pobierz surowy plik wideo\",\n    \"description\": \"Przycisk pobierania surowego nagrania\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Eksportuj oryginalne dane nagrania\",\n    \"description\": \"Etykieta pobierania surowego nagrania\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Pomoc dotycząca nagrania\",\n    \"description\": \"Przycisk Rozwiązywania problemów\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Przywróć nagranie wideo i rozwiąż inne problemy\",\n    \"description\": \"Etykieta Rozwiązywania problemów\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Pobierz surowy plik wideo\",\n    \"description\": \"Tytuł okna modalnego surowego nagrania\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Surowy plik wideo to oryginalny plik wideo nagrany przez Screenity. Ten film nie został przetworzony ani zedytowany, dlatego może być bardzo duży i niekoniecznie da się go odtworzyć na wszystkich urządzeniach. Jeśli masz problemy z przetworzonym filmem, możesz użyć tego pliku do odzyskania swojego nagrania.\",\n    \"description\": \"Opis okna modalnego surowego nagrania\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Pobierz\",\n    \"description\": \"Przycisk okna modalnego surowego nagrania\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Pobierz informacje o systemie i dane wideo do rozwiązywania problemów\",\n    \"description\": \"Tytuł okna modalnego Rozwiązywania problemów\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Po pobraniu prześlij ten plik na adres support@screenity.io wraz z wszelkimi istotnymi informacjami. Będziemy używać tych danych, aby pomóc ci rozwiązać problemy z rozszerzeniem i, jeśli to możliwe, odzyskać twoje nagranie wideo.\",\n    \"description\": \"Opis okna modalnego Rozwiązywania problemów\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Pobierz\",\n    \"description\": \"Przycisk okna modalnego Rozwiązywania problemów\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Film jest zbyt długi, aby przetworzyć na tym urządzeniu\",\n    \"description\": \"Tytuł okna modalnego z limitem ponad 5 minut\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Ponieważ Screenity działa w sposób całkowicie prywatny i lokalnie, jest ograniczane przez sprzęt twojego urządzenia. Przetwarzanie filmu o tej długości zajęłoby zbyt dużo czasu i byłoby bardzo intensywne z punktu widzenia zasobów. Nie martw się, nadal możesz pobrać plik WEBM lub surowe nagranie, ale edycja i eksport do formatu MP4 nie są dostępne. Mimo to nadal możesz spróbować przetworzyć film, jeśli rozumiesz związane z tym ryzyko.\",\n    \"description\": \"Opis okna modalnego z limitem ponad 5 minut\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Rozumiem ryzyko, spróbuj mimo to\",\n    \"description\": \"Przycisk okna modalnego z limitem ponad 5 minut\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Dowiedz się więcej na temat przetwarzania długich filmów.\",\n    \"description\": \"Link „Dowiedz się więcej” w oknie modalnym z limitem ponad 5 minut z kropką\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Tryb odzyskiwania\",\n    \"description\": \"Tytuł trybu odzyskiwania\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Pobierz dane do rozwiązywania problemów\",\n    \"description\": \"Opcja pobierania do rozwiązywania problemów\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centrum Pomocy\",\n    \"description\": \"Element nawigacyjny pomocy\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Nagrywanie dźwięku z strony\",\n    \"description\": \"Tytuł ostrzeżenia dźwiękowego\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Aby nagrać dźwięk z tej strony, musisz użyć opcji '$tab$'.\",\n    \"description\": \"Opis ostrzeżenia dźwiękowego\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Obszar karty\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Rozszerzenie nieobsługiwane na tej stronie\",\n    \"description\": \"Tytuł ostrzeżenia o nieobsługiwanym rozszerzeniu na karcie\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity nie był obsługiwany na twojej poprzedniej karcie. Mimo to, możesz rozpocząć nagrywanie tutaj, a następnie przejść na inną kartę, ale kamera i pasek narzędzi nie będą widoczne.\",\n    \"description\": \"Opis ostrzeżenia o nieobsługiwanym rozszerzeniu na karcie\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Skonfiguruj lokalne kopie zapasowe dla swoich nagranek w Screenity\",\n    \"description\": \"Tytuł kopii zapasowych\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Zachowaj kopię wszystkich nagranych materiałów. Jeśli nie skonfigurujesz kopii zapasowej, twoje nagrania zostaną zapisane tylko wtedy, gdy zdecydujesz się je pobrać.\",\n    \"description\": \"Opis kopii zapasowych\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Możesz potrzebować utworzyć nowy folder najpierw, jeśli próbujesz zapisać nagrania w folderach takich jak 'Pobrane' lub inne foldery systemowe.\",\n    \"description\": \"Opis kopii zapasowych\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Wybierz folder\",\n    \"description\": \"Przycisk Wybierz folder\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Nie teraz\",\n    \"description\": \"Przycisk Nie teraz\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Twoje nagrania są lokalnie synchronizowane\",\n    \"description\": \"Tytuł aktywowanych kopii zapasowych\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Aby uniknąć prośby o uprawnienia za każdym razem, gdy nagrywasz, zalecamy pozostawienie tej karty otwartej.\",\n    \"description\": \"Opis aktywowanych kopii zapasowych\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Zamknij kartę mimo wszystko\",\n    \"description\": \"Przycisk Zamknij kartę mimo wszystko\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Zatrzymaj wszystkie kopie zapasowe nagrań\",\n    \"description\": \"Przycisk Zatrzymaj kopie zapasowe\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Proszę potwierdź dostęp do lokalnego folderu kopii zapasowych Screenity\",\n    \"description\": \"Tytuł potwierdzenia kopii zapasowych\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Przykro nam, ale potrzebujemy Twojej zgody jeszcze raz, aby kontynuować tworzenie lokalnych kopii zapasowych Twoich nagrań. Niestety będziemy musieli pytać za każdym razem, gdy zamkniesz tę kartę, co może się zdarzyć, jeśli zamkniesz przeglądarkę lub zrestartujesz komputer.\",\n    \"description\": \"Opis potwierdzenia kopii zapasowych\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Zezwól Screenity na kontynuowanie tworzenia kopii zapasowych nagrań\",\n    \"description\": \"Przycisk Zezwól w potwierdzeniu kopii zapasowych\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Tworzenie kopii zapasowych nagrań\",\n    \"description\": \"Etykieta Przełącz kopie zapasowe\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Odmowa uprawnień do tworzenia kopii zapasowych nagrania\",\n    \"description\": \"Tytuł niepowodzenia uprawnień kopii zapasowych\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Nie wybrałeś folderu do tworzenia lokalnych kopii zapasowych nagrań. W zależności od wersji przeglądarki lub systemu operacyjnego, możesz być proszony o wybranie folderu za każdym razem, gdy rozpoczniesz nagrywanie. Możesz spróbować ponownie lub całkowicie wyłączyć kopie zapasowe, jeśli wolisz nie udzielać uprawnień wielokrotnie.\",\n    \"description\": \"Opis niepowodzenia uprawnień kopii zapasowych\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Nagrywanie dźwięku z komputera\",\n    \"description\": \"Tytuł ostrzeżenia dźwiękowego dla macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"W systemie macOS możesz nagrywać dźwięk tylko z zakładek. Wybierz opcję 'Zakładka Chrome' i upewnij się, że opcja 'Udostępnij także dźwięk zakładki' jest włączona.\",\n    \"description\": \"Opis ostrzeżenia dźwiękowego dla macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Nagrywanie dźwięku z komputera\",\n    \"description\": \"Tytuł ostrzeżenia dźwiękowego dla innych systemów\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Aby nagrać dźwięk z komputera, upewnij się, że opcja 'Udostępnij dźwięk systemowy' jest włączona. Jeśli nie widzisz tej opcji, spróbuj zaktualizować Chrome lub przełączyć się na opcję nagrywania zakładek.\",\n    \"description\": \"Opis ostrzeżenia dźwiękowego dla innych systemów\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Maksymalna rozdzielczość\",\n    \"description\": \"Etykieta maksymalnej rozdzielczości w menu ustawień\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Maksymalna ilość kl./s\",\n    \"description\": \"Etykieta maksymalnej ilości kl./s w menu ustawień\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Za mało pamięci RAM\",\n    \"description\": \"Etykieta 'Za mało pamięci RAM' w menu ustawień\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Zmień rozmiar okna\",\n    \"description\": \"Etykieta opcji zmiany rozmiaru okna w menu ustawień\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Ekran jest zbyt mały, by zmienić rozmiar na tę rozdzielczość\",\n    \"description\": \"Etykieta ostrzeżenia o zbyt małym ekranie w menu ustawień\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Nie można nagrywać w tej rozdzielczości na tym urządzeniu\",\n    \"description\": \"Etykieta ostrzeżenia o maksymalnej rozdzielczości w menu ustawień\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Przejrzyj uprawnienia\",\n    \"description\": \"Etykieta przycisku przejrzenia uprawnień w oknie dialogowym uprawnień\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Dodaj kolejne nagranie\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Przygotowuję wszystko...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Chwileczkę! Nagranie jest przygotowywane.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Pasek narzędzi został ukryty. Włącz go ponownie w ustawieniach rozszerzenia.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Zaloguj się lub zarejestruj\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Wyloguj się\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Zostałaś wylogowana\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Osiągnięto limit miejsca\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Wykorzystałaś całe 100 GB miejsca. Usuń stare nagrania lub sceny, aby kontynuować.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Zarządzaj miejscem\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Zamknij\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Nie udało się sprawdzić miejsca\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Wystąpił problem z weryfikacją miejsca. Zaloguj się ponownie i spróbuj nagrać.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Nie udało się zweryfikować limitu miejsca. Spróbuj ponownie za chwilę lub odśwież stronę.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Spróbuj ponownie\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Scena dodana do nagrania Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Gotowa do nagrania nowej sceny w wideo\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Nagranie zbliża się do limitu 90 minut, wkrótce zostanie zatrzymane\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Nagranie zatrzymane po osiągnięciu limitu 90 minut\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Nagrywanie karty jest wyłączone na tej stronie. Włączono nagrywanie ekranu.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Nagranie wideo anulowane\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Link skopiowany do schowka\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Nie udało się skopiować linku\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Najnowsze\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Najstarsze\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Wszystkie nagrania\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Brak nagrań\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Ładowanie nagrań...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Przejdź do panelu\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Witamy w Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Bez reklam. Bez śledzenia. Bez limitów.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"  Po prostu kliknij nagrywaj.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Korzystaj za darmo — bez rejestracji!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Chcesz więcej z wideo?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Przechowywanie w chmurze, udostępnianie przez link\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Łączenie wielu klipów w jedną historię\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Inteligentne przybliżenia po kliknięciu (lub dodaj własne!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Dodawanie szablonów, napisów, układów...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Własne tła i mockupy\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Odblokuj dodatkowe funkcje\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Wesprzyj twórczynię — solo indie makerkę!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Ustawienia konta\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Wsparcie\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Twoja subskrypcja Pro jest nieaktywna\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Twoja subskrypcja Screenity Pro jest nieaktywna.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Aktywuj ponownie, aby odzyskać dostęp.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Twoje nagrania i dane zostaną trwale usunięte \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Zarządzaj subskrypcją\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Chcesz dalej korzystać z wersji darmowej?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Wyloguj się i przejdź na darmową wersję\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Zostałaś wylogowana\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Aby synchronizować nagrania z kontem i mieć dostęp do funkcji Pro, musisz się zalogować ponownie.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Zaloguj się ponownie\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Możesz dalej używać rozszerzenia bez konta – ale Twoje nagrania nie będą zapisywane i nie da się ich odzyskać później.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Kontynuuj bez logowania\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Tryb nagrywania natychmiastowego\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Natychmiastowe pobieranie, ale kamera i układ nie będą edytowalne później.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Tryb nagrywania natychmiastowego\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Nagranie trafia od razu do jednego wideo gotowego do pobrania i udostępnienia. Układ kamery nie będzie później edytowalny, ale inne zmiany nadal będą możliwe.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Rozumiem\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Nagrywanie karty jest wyłączone na tej stronie\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Dodawanie do: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Zakończ\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Chcesz więcej z nagrań?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Zaloguj się, aby zapisać nagrania w chmurze, udostępniać linkiem i korzystać z zaawansowanych opcji edycji.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Zaloguj się, aby odblokować funkcje Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Wesprzyj twórczynię – solo indie makerkę\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Odblokuj więcej funkcji\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Zaloguj się, aby udostępnić (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Odblokuj udostępnianie w chmurze, edycję wielu scen, auto-zoomy, napisy i więcej\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity zawsze będzie darmowe, open-source i bez reklam. Pro pokrywa koszty chmury i serwerów oraz wspiera rozwój przez solo indie devkę! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Nie pokazuj ponownie\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Wypróbuj\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Tryb Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Nagrywaj wiele scen – ekran, kamerę lub oba – jedna po drugiej. Świetne do wielu ujęć, zmiany widoku lub podzielenia nagrania na części. Gdy skończysz, kliknij Zakończ, aby otworzyć edytor z wszystkimi scenami połączonymi w jednym wideo.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Aktywuj Pro, aby rozpocząć\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Zasubskrybuj, aby uzyskać dostęp do funkcji Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Zasubskrybuj Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Korzystasz lokalnie ze Screenity? Wesprzyj jej dalszy rozwój!\",\n    \"description\": \"Tytuł banera o samodzielnym hostowaniu w edytorze\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity została stworzona przez niezależną programistkę. Samodzielne hostowanie jest darmowe, ale jeśli Ci się przydaje, rozważ wsparcie poprzez Pro ❤️\",\n    \"description\": \"Opis banera o samodzielnym hostowaniu w edytorze\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Witaj ponownie w Screenity\",\n    \"description\": \"Tytuł okienka powitalnego wyświetlanego powracającym użytkownikom\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Nowość: Potrzebujesz czegoś więcej niż tylko nagrywania?\",\n    \"description\": \"Tytuł funkcji przechowywania w chmurze w okienku powitalnym\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro to nowa, opcjonalna platforma do zapisywania nagrań w chmurze, udostępniania ich za pomocą linku i odblokowania zaawansowanych narzędzi do edycji — jeśli i kiedy ich potrzebujesz.\",\n    \"description\": \"Opis funkcji przechowywania w chmurze w okienku powitalnym\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Dowiedz się więcej o Screenity Pro\",\n    \"description\": \"Przycisk wezwania do działania dla funkcji chmury w okienku powitalnym\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Witamy w Screenity Pro\",\n    \"description\": \"Tytuł okna powitalnego Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Możesz już rozpocząć nagrywanie! Kilka rzeczy, o których warto pamiętać:\\n\\n- Bąbelek z kamerą może zniknąć podczas nagrywania – to normalne. Jest nagrywany osobno w tle, więc później możesz go dowolnie ustawić.\\n- Powiększenia są automatycznie dodawane tylko przy kliknięciach w kartach Chrome – to ograniczenie przeglądarki. Zawsze możesz dodać więcej powiększeń ręcznie po nagraniu.\\n- Jeśli chcesz szybko nagrywać i od razu pobierać nagrania, wypróbuj Tryb Błyskawiczny. Pamiętaj jednak, że oferuje on ograniczone możliwości edycji – brak tła, układów czy zaawansowanych opcji.\",\n    \"description\": \"Opis okna powitalnego Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Rozumiem\",\n    \"description\": \"Przycisk akcji w oknie powitalnym Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 błąd — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Witamy w rozszerzeniu Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Kilka szybkich wskazówek przed nagrywaniem.<br/>Automatyczne zbliżenia działają tylko po kliknięciach w <strong>kartach Chrome</strong>. Dodatkowe zbliżenia możesz dodać ręcznie później.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Pasek nagrywania i efekty\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Użyj tego paska do rysowania, efektów kursora, rozmycia i sterowania nagrywaniem.<br/><br/><strong>Ten pasek będzie widoczny na nagraniu</strong>, jeśli go nie ukryjesz.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Kamera jest nagrywana osobno\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Podczas nagrywania kamera może się <strong>ukryć lub przejść do PiP</strong> - to normalne.<br/><br/>Jest zapisywana osobno, więc później możesz ją swobodnie ustawić.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Tryb natychmiastowy\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Najlepszy do szybkiego udostępniania z <strong>nielimitowanymi pobraniami</strong>.<br/><br/>W tym trybie nie ma zaawansowanych układów ani opcji edycji.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Rozumiem\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Dalej\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Wstecz\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Dowiedz się więcej\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Zresetuj onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Mało miejsca na dysku. Zapisywanie nagrania. Wszystko, co zostało przechwycone, jest bezpieczne.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Długie nagranie\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Nagranie jest kompletne i bezpieczne. Pobierz jako WebM poniżej.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Niedostępne dla długich nagrań\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Niedostępne w trybie odzyskiwania\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Edycja dźwięku zbyt długa\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Ta edycja dźwięku nie jest obsługiwana dla klipów dłuższych niż 15 minut. Oryginał nie został zmieniony. Spróbuj najpierw przyciąć klip.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Czas edycji upłynął\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Edycja nie zakończyła się na czas. Oryginalne nagranie nie zostało zmienione. Spróbuj ponownie lub najpierw przytnij klip.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Udostępnianie ekranu zostało zatrzymane. Twoje nagranie jest zapisane. Zatrzymaj, gdy będziesz gotowy.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Wystąpił problem z przetwarzaniem, ale Twoje nagranie jest bezpieczne. Możesz je pobrać poniżej.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Przetwarzanie edycji. Oryginalne nagranie pozostaje niezmienione.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Nie udało się zastosować edycji\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Coś poszło nie tak podczas przetwarzania. Oryginalne nagranie jest bezpieczne. Możesz spróbować ponownie lub pobrać je w obecnej formie.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Udostępnianie ekranu zostało zatrzymane. Zapisywanie nagrania. Wszystko, co zostało przechwycone, jest bezpieczne.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Dźwięk został odłączony. Zapisywanie nagrania. Dotychczas przechwycone wideo jest bezpieczne.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Nagranie zostało zapisane. Edytor się nie otworzył. Kliknij ikonę Screenity, aby spróbować ponownie.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Zapisywanie trwa dłużej niż oczekiwano. Twoje dane są bezpieczne. Edytor otworzy się wkrótce.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Kopiuj info debugowania\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info debugowania skopiowane\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Uzyskaj pomoc\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Nagranie było za krótkie, aby je zapisać\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Szybki rejestrator nie powi\\u00f3d\\u0142 si\\u0119\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"Wyj\\u015bcie szybkiego rejestratora nie mog\\u0142o zosta\\u0107 zweryfikowane na tym urz\\u0105dzeniu.\\nMo\\u017cesz mimo to pobra\\u0107 plik.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Pobierz mimo to\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Anuluj\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/pt_BR/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Grave e faça anotações na tela\",\n    \"description\": \"Nome da extensão\"\n  },\n  \"extDesc\": {\n    \"message\": \"O melhor gravador de tela gratuito para o Chrome, sem limitações. Capture, faça anotações, dê zoom, desfoque, edite vídeos e muito mais - sem a necessidade de registro, totalmente privado e gratuito.\",\n    \"description\": \"Descrição da extensão\"\n  },\n  \"recordTab\": {\n    \"message\": \"Gravar\",\n    \"description\": \"Etiqueta da página de gravação no popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Seus vídeos\",\n    \"description\": \"Etiqueta da página de vídeos no popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Tela\",\n    \"description\": \"Etiqueta de tipo de gravação de tela no popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Área\",\n    \"description\": \"Etiqueta de tipo de gravação de área de página no popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Câmera\",\n    \"description\": \"Etiqueta de tipo de gravação de câmera no popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etiqueta de tipo de gravação de mockup no popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Gravação de área personalizada desativada\",\n    \"description\": \"Aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Atualize a versão do Chrome para 110+\",\n    \"description\": \"Descrição do aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Atualizar\",\n    \"description\": \"Ação do aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Verifique suas permissões\",\n    \"description\": \"Título do modal de aviso de permissões de câmera/microfone\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Parece que o Screenity não consegue acessar sua câmera ou microfone. Certifique-se de permitir o acesso clicando no ícone da câmera na barra de endereço.\",\n    \"description\": \"Descrição do modal de aviso de permissões de câmera/microfone\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Ignorar\",\n    \"description\": \"Botão de ignorar no modal de aviso de permissões de câmera/microfone\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Permitir acesso à câmera\",\n    \"description\": \"Botão de permitir acesso à câmera\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Permitir acesso ao microfone\",\n    \"description\": \"Botão de permitir acesso ao microfone\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Pressione para falar\",\n    \"description\": \"Etiqueta do interruptor pressionar para falar\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Definir uma área para gravar\",\n    \"description\": \"Etiqueta do interruptor de área personalizada\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Inverter câmera\",\n    \"description\": \"Etiqueta do interruptor de inverter câmera\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efeitos de fundo\",\n    \"description\": \"Etiqueta do interruptor de efeitos de fundo\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Iniciar gravação\",\n    \"description\": \"Etiqueta do botão de gravar\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Defina uma câmera para gravar\",\n    \"description\": \"Rótulo do botão de gravação quando no modo de câmera e nenhuma câmera está selecionada\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Iniciando gravação...\",\n    \"description\": \"Etiqueta em progresso do botão de gravar\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Mostrar mais opções\",\n    \"description\": \"Etiqueta de mostrar mais opções\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Incluir áudio do sistema\",\n    \"description\": \"Rótulo de áudio do sistema/guia\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Ocultar barra de ferramentas\",\n    \"description\": \"Etiqueta de ocultar barra de ferramentas\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Contagem decrescente\",\n    \"description\": \"Etiqueta de contagem decrescente\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Definir um limite de tempo\",\n    \"description\": \"Etiqueta de alarme\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Desfoque\",\n    \"description\": \"Etiqueta de desfoque de fundo\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Nenhum\",\n    \"description\": \"Nenhum dispositivo selecionado\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Sem câmera\",\n    \"description\": \"Nenhuma câmera selecionada\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Sem microfone\",\n    \"description\": \"Nenhum microfone selecionado\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Selecionar uma fonte\",\n    \"description\": \"Marcador de posição para selecionar fonte\"\n  },\n  \"offLabel\": {\n    \"message\": \"Desligado\",\n    \"description\": \"Etiqueta de desligado no menu suspenso\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Largura\",\n    \"description\": \"Etiqueta de largura de região\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Altura\",\n    \"description\": \"Etiqueta de altura de região\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Clique para adicionar uma imagem\",\n    \"description\": \"Brinde que aparece ao adicionar uma nova imagem\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Terminar gravação\",\n    \"description\": \"Dica para terminar a gravação\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Reiniciar gravação\",\n    \"description\": \"Dica para reiniciar a gravação\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Pausar gravação\",\n    \"description\": \"Dica para pausar a gravação\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Dica para continuar a gravação\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Cancelar gravação\",\n    \"description\": \"Dica para cancelar a gravação\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Ferramentas de desenho\",\n    \"description\": \"Dica para alternar ferramentas de desenho\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Ferramenta de desfoque\",\n    \"description\": \"Dica para alternar ferramentas de desfoque\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Opções de cursor\",\n    \"description\": \"Dica para alternar opções de cursor\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Desativar câmera\",\n    \"description\": \"Dica para desativar a câmera\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Ativar câmera\",\n    \"description\": \"Dica para ativar a câmera\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Sem permissões de câmera\",\n    \"description\": \"Dica para sem permissões de câmera\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Desativar microfone\",\n    \"description\": \"Dica para desativar o microfone\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Ativar microfone\",\n    \"description\": \"Dica para ativar o microfone\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Sem permissões de microfone\",\n    \"description\": \"Dica para sem permissões de microfone\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Selecionar ferramenta\",\n    \"description\": \"Dica para selecionar ferramenta\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Ferramenta de caneta\",\n    \"description\": \"Dica para ferramenta de caneta\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Ferramenta de marcador\",\n    \"description\": \"Dica para ferramenta de marcador\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Ferramenta de borracha\",\n    \"description\": \"Dica para ferramenta de borracha\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Ferramenta de texto\",\n    \"description\": \"Dica para ferramenta de texto\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Ferramenta de formas\",\n    \"description\": \"Dica para ferramenta de formas\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Ferramenta de seta\",\n    \"description\": \"Dica para ferramenta de seta\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Ferramenta de imagem\",\n    \"description\": \"Dica para ferramenta de imagem\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Desfazer\",\n    \"description\": \"Dica para desfazer\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Refazer\",\n    \"description\": \"Dica para refazer\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Limpar tela\",\n    \"description\": \"Dica para limpar tela\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Mais cores\",\n    \"description\": \"Dica para mais cores\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Traço grosso\",\n    \"description\": \"Dica para traço grosso\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Traço médio\",\n    \"description\": \"Dica para traço médio\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Traço fino\",\n    \"description\": \"Dica de traço fino\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Alternar modo Picture-in-Picture (PIP)\",\n    \"description\": \"Dica de alternar modo Picture-in-Picture\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Alternar preenchimento\",\n    \"description\": \"Dica de alternar preenchimento\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Modo de desenho\",\n    \"description\": \"Brinde de modo de desenho\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Modo de desfoque\",\n    \"description\": \"Brinde de modo de desfoque\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Limpar todos os elementos desfocados\",\n    \"description\": \"Dica de limpar todos os elementos desfocados\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Destacar cliques\",\n    \"description\": \"Dica de destacar cliques\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Destacar cursor\",\n    \"description\": \"Dica de destacar cursor\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Foco no cursor\",\n    \"description\": \"Dica de foco no cursor\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Botão de fechar modal de permissões\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Você tem certeza de que deseja reiniciar a gravação?\",\n    \"description\": \"Título do modal de reiniciar gravação\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Seu progresso no vídeo será perdido.\",\n    \"description\": \"Descrição do modal de reiniciar gravação\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Reiniciar gravação\",\n    \"description\": \"Botão de reiniciar gravação no modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Botão de continuar gravação no modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Você tem certeza de que deseja descartar a gravação?\",\n    \"description\": \"Título do modal de descartar gravação\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Seu progresso no vídeo será perdido.\",\n    \"description\": \"Descrição do modal de descartar gravação\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Descartar gravação\",\n    \"description\": \"Botão de descartar gravação no modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Botão de continuar gravação no modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Escolha o que deseja gravar\",\n    \"description\": \"Título na página de gravação\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Gravando...\",\n    \"description\": \"Título na página de gravação durante a gravação\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Mantenha esta guia aberta. Ela será fechada assim que sua gravação for salva.\",\n    \"description\": \"Descrição na página de gravação\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparando a gravação...\",\n    \"description\": \"Título na página de testes durante a gravação/salvamento\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Mantenha esta guia aberta. Ela será atualizada com sua gravação quando estiver pronta.\",\n    \"description\": \"Descrição na página de testes durante a gravação/salvamento\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Título na navegação da página do editor de testes\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Botão de cancelar na página do editor de testes\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Reverter para o original\",\n    \"description\": \"Botão de reverter na página do editor de testes\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Redefinir\",\n    \"description\": \"Botão de redefinir na página do editor de testes\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Salvar alterações\",\n    \"description\": \"Botão de salvar na página do editor de testes\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Salvando...\",\n    \"description\": \"Botão de salvar na página do editor de testes durante o salvamento\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Arraste seu arquivo de áudio\",\n    \"description\": \"Arraste e solte arquivo de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Ou clique para procurar\",\n    \"description\": \"Ou clique para procurar arquivo de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Configurações de áudio\",\n    \"description\": \"Título de configurações de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Rótulo de volume de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aplicar\",\n    \"description\": \"Botão de atualizar áudio na página do editor de testes\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Recortar\",\n    \"description\": \"Título de recorte na página do editor de testes\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Largura\",\n    \"description\": \"Rótulo de largura para propriedades\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Altura\",\n    \"description\": \"Rótulo de altura para propriedades\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Esquerda\",\n    \"description\": \"Rótulo de esquerda para propriedades\"\n  },\n  \"topLabel\": {\n    \"message\": \"Topo\",\n    \"description\": \"Rótulo de topo para propriedades\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Arraste as abas e use os botões à esquerda para editar a seção selecionada.\",\n    \"description\": \"Informações sobre corte na página do editor de testes\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Cortar vídeo\",\n    \"description\": \"Botão de corte na página do editor de testes\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Cortando...\",\n    \"description\": \"Botão de corte na página do editor de testes durante o corte\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Cortar seção\",\n    \"description\": \"Botão de corte na página do editor de testes\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Cortando...\",\n    \"description\": \"Botão de corte na página do editor de testes durante o corte\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Mutar áudio\",\n    \"description\": \"Botão de mutar na página do editor de testes\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Mutando...\",\n    \"description\": \"Botão de mutar na página do editor de testes durante o muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Saiba mais.\",\n    \"description\": \"Saiba mais com um ponto\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Desfazer\",\n    \"description\": \"Rótulo de desfazer\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Refazer\",\n    \"description\": \"Rótulo de refazer\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Deixar uma avaliação\",\n    \"description\": \"Botão de deixar uma avaliação\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Seguir para atualizações\",\n    \"description\": \"Botão de seguir para atualizações\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Atualmente offline\",\n    \"description\": \"Rótulo de offline\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Algumas funcionalidades estão indisponíveis até que você se reconecte\",\n    \"description\": \"Descrição do rótulo de offline\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Tentar novamente\",\n    \"description\": \"Botão de tentar novamente do rótulo de offline\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"O Chrome precisa ser atualizado\",\n    \"description\": \"Rótulo de atualizar o Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Atualize para acessar mais funcionalidades\",\n    \"description\": \"Descrição do rótulo de atualizar o Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Saiba mais\",\n    \"description\": \"Botão de saiba mais\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Vídeo muito longo para processar no seu dispositivo\",\n    \"description\": \"Etiqueta de limite de mais de 5 minutos no editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Edição e exportação em formato MP4 não estão disponíveis\",\n    \"description\": \"Descrição de limite de mais de 5 minutos no editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"O vídeo está sendo processado...\",\n    \"description\": \"Rótulo de processamento de vídeo no editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Faça upgrade para um editor mais rápido\",\n    \"description\": \"Descrição do processamento de vídeo no editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Editar\",\n    \"description\": \"Título de edição na página do editor de testes\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Sem conexão\",\n    \"description\": \"Rótulo de sem conexão\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Indisponível\",\n    \"description\": \"Rótulo de não disponível\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Editar vídeo\",\n    \"description\": \"Rótulo de botão de corte\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Remova, corte ou silencie o vídeo\",\n    \"description\": \"Rótulo de corte\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Preparando...\",\n    \"description\": \"Rótulo de preparação\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Cortar\",\n    \"description\": \"Rótulo de botão de corte\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Corte e redimensione o vídeo\",\n    \"description\": \"Rótulo de corte\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Adicionar áudio\",\n    \"description\": \"Rótulo de botão de adicionar áudio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Carregue seu próprio áudio para o vídeo\",\n    \"description\": \"Rótulo de adicionar áudio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Salvar\",\n    \"description\": \"Título de salvar na página do editor de testes\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Sair do Google Drive\",\n    \"description\": \"Rótulo de sair do Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Salvando...\",\n    \"description\": \"Rótulo de salvamento no Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Salvar no Google Drive\",\n    \"description\": \"Botão de salvar no Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Guarde a versão atual do vídeo\",\n    \"description\": \"Rótulo de salvar no Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Inicie a sessão e salve no Google Drive\",\n    \"description\": \"Rótulo de iniciar sessão no Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exportar\",\n    \"description\": \"Título de exportação na página do editor de testes\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Baixando...\",\n    \"description\": \"Rótulo de download\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Baixar como .webm\",\n    \"description\": \"Botão de baixar WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Exportar um vídeo .webm\",\n    \"description\": \"Rótulo de baixar WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Baixar como .mp4\",\n    \"description\": \"Botão de baixar MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Exportar um vídeo .mp4 (recomendado)\",\n    \"description\": \"Rótulo de baixar MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Baixar como .gif\",\n    \"description\": \"Botão de baixar GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Exportar um GIF (máximo de 30 segundos)\",\n    \"description\": \"Rótulo de baixar GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Pronto para turbinar suas gravações?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Compartilhe na nuvem, use zooms, modelos... e apoie uma dev indie que respeita sua privacidade ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Saiba mais sobre o Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Já tem conta? Faça login\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Compartilhar vídeo\",\n    \"description\": \"Botão de compartilhamento na página do editor de testes\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Substituir áudio existente\",\n    \"description\": \"Botão de substituição de áudio no editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom para o cursor\",\n    \"description\": \"Popup de zoom para o ponto\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Permaneça na página durante a gravação\",\n    \"description\": \"Popup de permanência na página\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Mostrar aviso de microfone desligado\",\n    \"description\": \"Popup de lembrete de microfone\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Ajuda\",\n    \"description\": \"Botão de popup de ajuda\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Gravação pausada. Pressione o botão de reprodução para continuar.\",\n    \"description\": \"Título do modal de gravação pausada\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"O Screenity não tem permissão para gravar a sua tela\",\n    \"description\": \"Título do modal de permissões\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Para permitir a gravação de tela, você deve conceder permissões ao Screenity.\",\n    \"description\": \"Descrição do modal de permissões\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Ativar gravação de tela\",\n    \"description\": \"Ação do modal de permissões\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar do modal de permissões\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Seu microfone está desligado\",\n    \"description\": \"Título do modal de microfone desligado\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Para incluir áudio na sua gravação, você precisará ativar o seu microfone. Você deseja continuar sem áudio?\",\n    \"description\": \"Descrição do modal de microfone desligado\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Sim, continuar\",\n    \"description\": \"Continuar do modal de microfone desligado\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar do modal de microfone desligado\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Comece com o Screenity em três passos simples:\",\n    \"description\": \"Título dos passos de configuração\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Clique no ícone de \",\n    \"description\": \"Passo 1 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" extensões\",\n    \"description\": \"Passo 1 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Pressione o ícone de \",\n    \"description\": \"Passo 2 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" alfinete\",\n    \"description\": \"Passo 2 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Clique no ícone de \",\n    \"description\": \"Passo 3 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity para começar\",\n    \"description\": \"Passo 3 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Fantástico! Vamos começar\",\n    \"description\": \"Título de configuração completa\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Agora você pode começar a gravar aqui ou em qualquer outra guia.\",\n    \"description\": \"Descrição de configuração completa\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Clique aqui para desenhar\",\n    \"description\": \"Instruções para clicar aqui\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Relaxe. Clique em qualquer lugar para parar a contagem regressiva.\",\n    \"description\": \"Mensagem de contagem regressiva\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"A edição de vídeos com durações curtas pode ser imprecisa devido aos pequenos intervalos entre imagens.\",\n    \"description\": \"Informação sobre o editor ser muito pequeno\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"O vídeo está sendo processado, a reprodução pode ser imprecisa ou interrompida.\",\n    \"description\": \"Banner de processamento no editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Recortar o vídeo pode demorar algum tempo\",\n    \"description\": \"Título da informação de recorte\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Inscreva-se para editar vídeos mais rapidamente\",\n    \"description\": \"Descrição da informação de recorte\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microfone ativado\",\n    \"description\": \"Título do aviso de microfone ativado\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microfone desativado\",\n    \"description\": \"Título do aviso de microfone desativado\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Ocultar notificações da interface\",\n    \"description\": \"Botão para ocultar alertas da interface\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Botão de não mostrar novamente\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Ocultar os controles quando não estiverem em uso\",\n    \"description\": \"Botão para mostrar a barra de ferramentas apenas ao passar o cursor\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Parar gravação\",\n    \"description\": \"Botão de parar a gravação na tela de gravação\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Bem-vindo ao novo Screenity!\",\n    \"description\": \"Título do anúncio de atualização para usuários existentes\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Atualizamos o design e desenvolvemos novas funcionalidades, mas não se preocupe, continua sendo a mesma extensão gratuita, privada e de código aberto.\",\n    \"description\": \"Descrição do anúncio de atualização para usuários existentes\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Saiba mais sobre a atualização.\",\n    \"description\": \"Link 'Saiba mais' do anúncio de atualização para usuários existentes\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Começar\",\n    \"description\": \"Botão do anúncio de atualização para usuários existentes\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Erro ao iniciar a gravação\",\n    \"description\": \"Título do modal de erro de transmissão\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Houve um erro ao iniciar a gravação. Reinicie o seu navegador e tente novamente. Se o problema persistir, entre em contato conosco em support@screenity.io.\",\n    \"description\": \"Descrição do modal de erro de transmissão\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Melhor qualidade\",\n    \"description\": \"Rótulo para alternar para a melhor qualidade no popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restaurar a última gravação\",\n    \"description\": \"Botão para restaurar a última gravação no popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Enfrentando problemas?\",\n    \"description\": \"Botão de problemas no editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Não consegue ver sua gravação?\",\n    \"description\": \"Título do modal de problemas no editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Se você concluiu sua gravação e está nesta página sem ver seu vídeo, você pode tentar baixar os dados brutos do vídeo, se estiverem disponíveis. Você também pode entrar em contato e enviar um relatório de erro.\",\n    \"description\": \"Descrição do modal de problemas no editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Baixar dados brutos do vídeo\",\n    \"description\": \"Botão do modal de problemas no editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Reportar erro\",\n    \"description\": \"Segundo botão do modal de problemas no editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Nenhuma gravação encontrada, desculpe :(\",\n    \"description\": \"Alerta de nenhuma gravação encontrada no editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Limite de memória atingido\",\n    \"description\": \"Título de limite de memória atingido\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Seu dispositivo ficou sem memória para gravar, e a gravação foi interrompida automaticamente. Se desejar gravar por mais tempo, pode tentar reduzir a qualidade do vídeo ou liberar espaço no dispositivo.\",\n    \"description\": \"Descrição de limite de memória atingido\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Entendi\",\n    \"description\": \"Botão de entendi\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Espaço insuficiente\",\n    \"description\": \"Título de espaço insuficiente\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"O Screenity precisa de pelo menos 1 GB de espaço livre para gravar. Por favor, libere espaço e tente novamente.\",\n    \"description\": \"Descrição de espaço insuficiente\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Apagar gravações anteriores\",\n    \"description\": \"Botão de apagar gravações anteriores\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avançado\",\n    \"description\": \"Seção avançada na página do editor no ambiente seguro\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Baixar arquivo de vídeo bruto\",\n    \"description\": \"Botão de download de gravação bruta\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exportar os dados originais da gravação\",\n    \"description\": \"Rótulo do botão de download de gravação bruta\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Obter ajuda com sua gravação\",\n    \"description\": \"Botão de solução de problemas\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recupere seu vídeo e resolva outros problemas\",\n    \"description\": \"Rótulo de solução de problemas\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Baixar arquivo de vídeo bruto\",\n    \"description\": \"Título do modal de gravação bruta\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"O arquivo de vídeo bruto é o arquivo original gravado pelo Screenity. Este vídeo não foi processado ou editado, portanto, pode ser muito grande e pode não ser reproduzido em todos os dispositivos. Se você estiver enfrentando problemas com o vídeo processado, pode usar este arquivo para recuperar seu vídeo.\",\n    \"description\": \"Descrição do modal de gravação bruta\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Baixar\",\n    \"description\": \"Botão do modal de gravação bruta\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Baixar informações do sistema e dados de vídeo para solucionar problemas\",\n    \"description\": \"Título do modal de solução de problemas\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Após o download, envie o arquivo para support@screenity.io com todas as informações relevantes. Usaremos esses dados para ajudá-lo a solucionar quaisquer problemas que você tenha com a extensão e, se possível, recuperar seu vídeo.\",\n    \"description\": \"Descrição do modal de solução de problemas\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Baixar\",\n    \"description\": \"Botão do modal de solução de problemas\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"O vídeo é muito longo para ser processado no seu dispositivo\",\n    \"description\": \"Título do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Uma vez que o Screenity é completamente privado e é executado localmente, está limitado pelo hardware do seu dispositivo. O processamento de um vídeo dessa duração levaria muito tempo e seria muito intensivo em recursos. Não se preocupe, você ainda pode baixar o arquivo WEBM ou a gravação bruta, mas a edição e a exportação para MP4 não estão disponíveis. Dito isso, você ainda pode tentar processar o vídeo, mesmo sabendo dos riscos.\",\n    \"description\": \"Descrição do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Entendo o risco, tentar mesmo assim\",\n    \"description\": \"Botão do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Saiba mais sobre o processamento de vídeos longos.\",\n    \"description\": \"Link 'Saiba mais' do modal de limite superior a 5 minutos com um ponto\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Modo de recuperação\",\n    \"description\": \"Título do modo de recuperação\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Baixar dados para solucionar problemas\",\n    \"description\": \"Opção de download para solucionar problemas\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Central de Ajuda\",\n    \"description\": \"Item de Navegação para Obter Ajuda\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título do Aviso de Áudio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Para gravar áudio desta página, você precisa alternar para '$tab$'.\",\n    \"description\": \"Descrição do Aviso de Áudio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Área da Guia\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extensão não suportada na guia\",\n    \"description\": \"Título de Extensão não Suportada na Guia\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"O Screenity não foi suportado na sua guia anterior. Ainda é possível iniciar a sua gravação aqui e alternar para a guia, mas a sua câmera e barra de ferramentas não serão visíveis.\",\n    \"description\": \"Descrição de Extensão não Suportada na Guia\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configurar um Backup Local para as suas Gravações no Screenity\",\n    \"description\": \"Título de Backups\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Mantenha uma cópia de todas as gravações que você fizer. Se você não configurar backups, suas gravações só serão salvas quando você escolher baixá-las.\",\n    \"description\": \"Descrição de Backups\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Você pode precisar criar uma nova pasta primeiro se estiver tentando salvar em 'Downloads' ou outras pastas do sistema.\",\n    \"description\": \"Descrição de Backups\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Selecionar uma Pasta\",\n    \"description\": \"Botão Selecionar Pasta\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Agora Não\",\n    \"description\": \"Botão Agora Não\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Suas Gravações estão sendo salvas localmente\",\n    \"description\": \"Título de Backups Ativados\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Para evitar solicitar permissões toda vez que você gravar, recomendamos deixar esta guia aberta.\",\n    \"description\": \"Descrição de Backups Ativados\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Fechar guia mesmo assim\",\n    \"description\": \"Botão Fechar guia mesmo assim\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Parar todos os backups de gravações\",\n    \"description\": \"Botão Parar Backups\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Por favor, confirme novamente o acesso à sua pasta local de backup do Screenity\",\n    \"description\": \"Título de Confirmação de Backups\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Lamentamos, mas precisamos da sua permissão mais uma vez para continuar fazendo backups locais das suas gravações. Infelizmente, teremos que perguntar toda vez que você fechar esta guia, o que pode acontecer se você fechar o navegador ou reiniciar o computador.\",\n    \"description\": \"Descrição de Confirmação de Backups\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Permitir que o Screenity continue fazendo backups das gravações\",\n    \"description\": \"Botão Permitir na Confirmação de Backups\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Ativar Backups das Gravações\",\n    \"description\": \"Rótulo de Alternar Backups\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permissão não concedida para fazer backup da gravação\",\n    \"description\": \"Título de Falha de Permissão de Backup\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Você não selecionou uma pasta para fazer backup local das suas gravações. Dependendo da versão do seu navegador ou sistema operacional, você pode ser solicitado a selecionar a pasta toda vez que iniciar uma gravação. Você pode tentar novamente ou desativar completamente os backups se preferir não conceder permissões repetidamente.\",\n    \"description\": \"Descrição de Falha de Permissão de Backup\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título de Aviso de Áudio para macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"No macOS, você só pode gravar o áudio da guia. Selecione a opção 'Guia do Chrome' e certifique-se de que 'Também compartilhar áudio da guia' está ativado.\",\n    \"description\": \"Descrição de Aviso de Áudio para macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título de Aviso de Áudio para Outros Sistemas\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Certifique-se de ativar a opção 'Compartilhar áudio do sistema' se desejar gravar áudio do computador. Se você não vir essa opção, tente atualizar o Chrome ou alternar para a gravação da guia.\",\n    \"description\": \"Descrição de Aviso de Áudio para Outros Sistemas\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Resolução máxima\",\n    \"description\": \"Etiqueta de resolução máxima no menu de configurações\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Taxa de quadros máxima\",\n    \"description\": \"Etiqueta de taxa de quadros máxima no menu de configurações\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Memória RAM insuficiente\",\n    \"description\": \"Etiqueta 'Memória RAM insuficiente' no menu de configurações\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Redimensionar janela\",\n    \"description\": \"Rótulo de redimensionamento de janela no menu de configurações\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"A tela é muito pequena para essa resolução\",\n    \"description\": \"Dica de tela muito pequena no menu de configurações\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Não é possível gravar nessa resolução neste dispositivo\",\n    \"description\": \"Dica de resolução máxima no menu de configurações\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Revisar permissões\",\n    \"description\": \"Botão de revisão de permissões\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Adicionar outra gravação\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Preparando tudo...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Aguarde um momento, estamos preparando sua gravação.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barra de ferramentas oculta. Reative nas opções.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Entrar ou criar conta\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Sair\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Você saiu da conta\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Você atingiu o limite de armazenamento\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Você usou 100 GB de armazenamento. Exclua vídeos ou cenas antigas para continuar gravando.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gerenciar armazenamento\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Fechar\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Não foi possível verificar o armazenamento\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Ocorreu um erro ao verificar o armazenamento. Faça login novamente e tente gravar de novo.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Não foi possível verificar sua cota de armazenamento. Tente novamente em alguns segundos ou atualize a página.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Tentar novamente\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Cena adicionada à gravação Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Pronta para gravar uma nova cena no seu vídeo\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"A gravação está perto do limite de 90 minutos, será encerrada em breve\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Gravação encerrada ao atingir o limite de 90 minutos\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"A gravação da aba está desativada nesta página. A gravação de tela foi ativada por padrão.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Gravação do vídeo cancelada\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Link copiado para a área de transferência\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Não foi possível copiar o link\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Mais recentes\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Mais antigos\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Todos os vídeos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Nenhum vídeo encontrado\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Carregando vídeos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Ir para o painel\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Bem-vinda ao Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Sem anúncios. Sem rastreamento. Sem limites.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Apenas grave.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Use grátis — sem cadastro!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Quer fazer mais com seus vídeos?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Armazenamento na nuvem, compartilhamento por link\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combine vários clipes como capítulos de uma história\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zooms inteligentes com clique (ou adicione os seus!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Adicione modelos, legendas, mockups...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Fundos e layout da câmera personalizados\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Desbloquear recursos extras\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Apoie o desenvolvimento independente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Configurações da conta\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Suporte\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Assinatura Pro inativa\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Sua assinatura do Screenity Pro está inativa.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Reative para voltar a ter acesso.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Seus vídeos e dados serão apagados permanentemente em \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gerenciar assinatura\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Quer continuar usando a versão gratuita?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Sair e mudar para a versão gratuita\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Sessão encerrada\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Para manter suas gravações sincronizadas e acessar recursos Pro, entre novamente.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Entrar novamente\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Você pode continuar usando a extensão sem uma conta, mas as gravações não serão salvas nem recuperáveis depois.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continuar sem entrar\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Modo de gravação instantânea\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Download imediato, mas você não poderá alterar o layout da câmera após iniciar a gravação.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Modo de gravação instantânea\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Este modo grava tudo em um único vídeo para baixar e compartilhar imediatamente. Você não poderá mudar o layout da câmera, mas poderá fazer outras edições.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Entendi\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"A gravação de abas está desativada nesta página\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Adicionando a: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Finalizar\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Quer fazer mais com suas gravações?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Faça login para salvar os vídeos na nuvem, compartilhar por link e acessar recursos avançados de edição.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Faça login para desbloquear recursos Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Apoie a criadora independente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Desbloquear mais recursos\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Faça login para compartilhar (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Desbloqueie armazenamento na nuvem, edição com várias cenas, zooms automáticos, legendas e mais\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"O Screenity sempre será gratuito, open-source e sem anúncios. O Pro ajuda a cobrir os custos com nuvem e infraestrutura, e apoia uma criadora independente ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Experimentar\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Modo Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Grave várias cenas — como sua tela, câmera ou ambas — uma após a outra. É ótimo para fazer várias tomadas, alternar entre visualizações ou dividir a gravação em partes. Quando terminar, clique em Concluir para abrir o editor com todas as cenas combinadas em um único vídeo.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Ative o Pro para começar\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Assine para acessar os recursos Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Assinar o Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Usando o Screenity localmente? Ajude a apoiar o futuro do projeto!\",\n    \"description\": \"Título do banner de auto-hospedagem no editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"O Screenity é desenvolvido por uma desenvolvedora indie. Hospedar por conta própria é gratuito, mas se for útil pra você, considere apoiar com o Pro ❤️\",\n    \"description\": \"Descrição do banner de auto-hospedagem no editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Bem-vindo de volta ao Screenity\",\n    \"description\": \"Título do popup de boas-vindas mostrado aos usuários que retornam\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Novidade: Leve suas gravações para o próximo nível\",\n    \"description\": \"Título para o recurso de armazenamento na nuvem no popup de boas-vindas\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"O Screenity Pro é uma nova plataforma opcional para salvar vídeos na nuvem, compartilhar por link e desbloquear ferramentas avançadas de edição, se e quando você precisar.\",\n    \"description\": \"Descrição do recurso de armazenamento na nuvem no popup de boas-vindas\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Saiba mais sobre o Screenity Pro\",\n    \"description\": \"Botão de chamada para ação para o recurso de armazenamento na nuvem no popup de boas-vindas\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Bem-vindo ao Screenity Pro\",\n    \"description\": \"Título do modal de boas-vindas ao Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Tudo pronto para começar a gravar! Algumas coisas importantes:\\n\\n- A bolha da câmera pode desaparecer durante a gravação — isso é normal. Ela é capturada separadamente em segundo plano, então você pode posicioná-la como quiser depois.\\n- Os zooms são criados automaticamente apenas em cliques dentro de abas do Chrome, por uma limitação do navegador. Você sempre pode adicionar mais zooms manualmente após a gravação.\\n- Para gravações rápidas com download instantâneo, experimente o Modo Instantâneo. Só tenha em mente que as opções de edição são limitadas: sem fundos, layouts ou ajustes avançados.\",\n    \"description\": \"Descrição do modal de boas-vindas ao Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Entendi\",\n    \"description\": \"Botão de ação do modal de boas-vindas ao Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 falhou — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Boas-vindas à extensão Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Algumas dicas rápidas antes de gravar.<br/>Os zooms automáticos funcionam apenas em cliques dentro de <strong>abas do Chrome</strong>. Você ainda pode adicionar zoom manualmente depois.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barra de gravação e efeitos\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Use esta barra para desenhar, aplicar efeitos de cursor, desfocar e controlar a gravação.<br/><br/><strong>Esta barra aparece no vídeo</strong> se você não ocultá-la.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"A câmera é capturada separadamente\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Sua câmera pode <strong>sumir ou ir para PiP</strong> durante a gravação, isso é normal.<br/><br/>Ela é capturada separadamente para você posicioná-la como quiser depois.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Modo instantâneo\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Ideal para compartilhar rápido com <strong>downloads ilimitados</strong>.<br/><br/>Layouts e opções avançadas de edição não ficam disponíveis neste modo.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Entendi\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Próximo\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Voltar\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Saiba mais\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Redefinir onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Pouco espaço em disco. Salvando sua gravação. Tudo que foi capturado até agora está seguro.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Gravação longa\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Sua gravação está completa e segura. Baixe como WebM abaixo.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Indisponível para gravações longas\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Indisponível no modo de recuperação\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Edição de áudio muito longa\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Esta edição de áudio não é suportada para clipes com mais de 15 minutos. Seu original não foi alterado. Tente cortar o clipe primeiro.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Tempo da edição esgotado\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"A edição não foi concluída a tempo. Sua gravação original não foi alterada. Tente novamente ou corte o clipe primeiro.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"O compartilhamento de tela parou. Sua gravação está salva. Pare quando estiver pronto.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Houve um problema no processamento, mas sua gravação está segura. Você pode baixá-la abaixo.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Processando sua edição. A gravação original não foi alterada.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Não foi possível aplicar a edição\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Algo deu errado durante o processamento. Sua gravação original está segura. Você pode tentar novamente ou baixá-la como está.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"O compartilhamento de tela parou. Salvando sua gravação. Tudo que foi capturado até agora está seguro.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Áudio desconectado. Salvando sua gravação. O vídeo capturado até agora está seguro.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Sua gravação foi salva. O editor não abriu. Clique no ícone do Screenity para tentar novamente.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"O salvamento está demorando mais que o esperado. Seus dados estão seguros. O editor abrirá em breve.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copiar info de depuração\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info de depuração copiada\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Obter ajuda\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"A gravação foi curta demais para salvar\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Gravador r\\u00e1pido falhou\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"A sa\\u00edda do gravador r\\u00e1pido n\\u00e3o p\\u00f4de ser validada neste dispositivo.\\nVoc\\u00ea pode baixar o arquivo mesmo assim.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Baixar mesmo assim\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/pt_PT/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Grave o ecrã e faça anotações\",\n    \"description\": \"Nome da extensão\"\n  },\n  \"extDesc\": {\n    \"message\": \"O melhor gravador de ecrã para o Chrome sem limites. Captura, desenha, amplia, desfoca, edita vídeos e muito mais, sem necessidade de registo, totalmente privado e gratuito.\",\n    \"description\": \"Descrição da extensão\"\n  },\n  \"recordTab\": {\n    \"message\": \"Gravar\",\n    \"description\": \"Etiqueta da página de gravação no popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Os teus vídeos\",\n    \"description\": \"Etiqueta da página de vídeos no popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Ecrã\",\n    \"description\": \"Etiqueta de tipo de gravação de ecrã no popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Área\",\n    \"description\": \"Etiqueta de tipo de gravação de área de página no popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Câmara\",\n    \"description\": \"Etiqueta de tipo de gravação de câmara no popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Etiqueta de tipo de gravação de mockup no popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Gravação de área personalizada desativada\",\n    \"description\": \"Aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Atualiza a versão do Chrome para 110+\",\n    \"description\": \"Descrição do aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Atualizar\",\n    \"description\": \"Ação do aviso de gravação de área personalizada desativada para versões do Chrome anteriores à 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Verifica as tuas permissões\",\n    \"description\": \"Título do modal de aviso de permissões de câmara/microfone\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Parece que o Screenity não consegue aceder à tua câmara ou microfone. Certifica-te de permitir o acesso ao clicar no ícone da câmara na barra de endereço.\",\n    \"description\": \"Descrição do modal de aviso de permissões de câmara/microfone\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Ignorar\",\n    \"description\": \"Botão de ignorar no modal de aviso de permissões de câmara/microfone\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Permitir acesso à câmara\",\n    \"description\": \"Botão de permitir acesso à câmara\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Permitir acesso ao microfone\",\n    \"description\": \"Botão de permitir acesso ao microfone\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Premir para falar\",\n    \"description\": \"Etiqueta do interruptor premir para falar\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Definir uma área para gravar\",\n    \"description\": \"Etiqueta do interruptor de área personalizada\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Inverter câmara\",\n    \"description\": \"Etiqueta do interruptor de inverter câmara\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Efeitos de fundo\",\n    \"description\": \"Etiqueta do interruptor de efeitos de fundo\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Iniciar gravação\",\n    \"description\": \"Etiqueta do botão de gravar\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Defina uma câmara para gravar\",\n    \"description\": \"Rótulo do botão de gravação quando no modo de câmera e nenhuma câmera está selecionada\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"A iniciar gravação...\",\n    \"description\": \"Etiqueta em progresso do botão de gravar\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Mostrar mais opções\",\n    \"description\": \"Etiqueta de mostrar mais opções\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Incluir áudio do sistema\",\n    \"description\": \"Etiqueta de áudio do sistema/guia\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Ocultar barra de ferramentas\",\n    \"description\": \"Etiqueta de ocultar barra de ferramentas\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Contagem decrescente\",\n    \"description\": \"Etiqueta de contagem decrescente\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Definir um limite de tempo\",\n    \"description\": \"Etiqueta de alarme\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Blur\",\n    \"description\": \"Etiqueta de desfocar fundo\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Nenhum\",\n    \"description\": \"Nenhum dispositivo selecionado\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Sem câmara\",\n    \"description\": \"Nenhuma câmara selecionada\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Sem microfone\",\n    \"description\": \"Nenhum microfone selecionado\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Selecionar uma fonte\",\n    \"description\": \"Marcador de posição para selecionar fonte\"\n  },\n  \"offLabel\": {\n    \"message\": \"Desligado\",\n    \"description\": \"Etiqueta de desligado no menu suspenso\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Largura\",\n    \"description\": \"Etiqueta de largura de região\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Altura\",\n    \"description\": \"Etiqueta de altura de região\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Clica para colocar a imagem\",\n    \"description\": \"Brinde que aparece ao adicionar uma nova imagem\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Terminar gravação\",\n    \"description\": \"Dica de terminar gravação\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Reiniciar gravação\",\n    \"description\": \"Dica de reiniciar gravação\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Pausar gravação\",\n    \"description\": \"Dica de pausar gravação\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Dica de continuar gravação\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Cancelar gravação\",\n    \"description\": \"Dica de cancelar gravação\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Ferramentas de desenho\",\n    \"description\": \"Dica de alternar ferramentas de desenho\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Ferramenta de desfoque\",\n    \"description\": \"Dica de alternar ferramentas de desfoque\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Opções de cursor\",\n    \"description\": \"Dica de alternar opções de cursor\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Desativar câmara\",\n    \"description\": \"Dica de desativar câmara\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Ativar câmara\",\n    \"description\": \"Dica de ativar câmara\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Sem permissões de câmara\",\n    \"description\": \"Dica de sem permissões de câmara\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Desativar microfone\",\n    \"description\": \"Dica de desativar microfone\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Ativar microfone\",\n    \"description\": \"Dica de ativar microfone\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Sem permissões de microfone\",\n    \"description\": \"Dica de sem permissões de microfone\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Selecionar ferramenta\",\n    \"description\": \"Dica de selecionar ferramenta\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Ferramenta de caneta\",\n    \"description\": \"Dica de ferramenta de caneta\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Ferramenta de marcador\",\n    \"description\": \"Dica de ferramenta de marcador\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Ferramenta de borracha\",\n    \"description\": \"Dica de ferramenta de borracha\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Ferramenta de texto\",\n    \"description\": \"Dica de ferramenta de texto\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Ferramenta de formas\",\n    \"description\": \"Dica de ferramenta de formas\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Ferramenta de seta\",\n    \"description\": \"Dica de ferramenta de seta\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Ferramenta de imagem\",\n    \"description\": \"Dica de ferramenta de imagem\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Desfazer\",\n    \"description\": \"Dica de desfazer\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Refazer\",\n    \"description\": \"Dica de refazer\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Limpar tela\",\n    \"description\": \"Dica de limpar tela\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Mais cores\",\n    \"description\": \"Dica de mais cores\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Traço grosso\",\n    \"description\": \"Dica de traço grosso\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Traço médio\",\n    \"description\": \"Dica de traço médio\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Traço fino\",\n    \"description\": \"Dica de traço fino\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Alternar modo de Picture-in-Picture (PIP)\",\n    \"description\": \"Dica de alternar modo de imagem sobre imagem\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Alternar preenchimento\",\n    \"description\": \"Dica de alternar preenchimento\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Modo de desenho\",\n    \"description\": \"Brinde de modo de desenho\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Modo de desfoque\",\n    \"description\": \"Brinde de modo de desfoque\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Limpar todos os elementos desfocados\",\n    \"description\": \"Dica de limpar todos os elementos desfocados\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Destacar cliques\",\n    \"description\": \"Dica de destacar cliques\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Destacar cursor\",\n    \"description\": \"Dica de destacar cursor\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Foco no cursor\",\n    \"description\": \"Dica de foco no cursor\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Botão de fechar modal de permissões\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Tens a certeza que queres reiniciar a gravação?\",\n    \"description\": \"Título do modal de reiniciar gravação\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"O teu progresso no vídeo será perdido.\",\n    \"description\": \"Descrição do modal de reiniciar gravação\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Reiniciar gravação\",\n    \"description\": \"Botão de reiniciar gravação no modal\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Botão de continuar gravação no modal\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Tens a certeza que queres descartar a gravação?\",\n    \"description\": \"Título do modal de descartar gravação\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"O teu progresso no vídeo será perdido.\",\n    \"description\": \"Descrição do modal de descartar gravação\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Descartar gravação\",\n    \"description\": \"Botão de descartar gravação no modal\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Continuar gravação\",\n    \"description\": \"Botão de continuar gravação no modal\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Escolhe o que queres gravar\",\n    \"description\": \"Título na página de gravação\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Gravando...\",\n    \"description\": \"Título na página de gravação durante a gravação\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Mantém este separador aberto. Será fechado assim que a tua gravação for guardada.\",\n    \"description\": \"Descrição na página de gravação\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Preparando a gravação...\",\n    \"description\": \"Título na página de testes durante a gravação/salvamento\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Mantém este separador aberto. Será atualizado com a tua gravação quando estiver pronta.\",\n    \"description\": \"Descrição na página de testes durante a gravação/salvamento\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editor\",\n    \"description\": \"Título na navegação da página do editor de testes\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Botão de cancelar na página do editor de testes\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Reverter para o original\",\n    \"description\": \"Botão de reverter na página do editor de testes\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Redefinir\",\n    \"description\": \"Botão de redefinir na página do editor de testes\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Guardar alterações\",\n    \"description\": \"Botão de salvar na página do editor de testes\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Guardando...\",\n    \"description\": \"Botão de salvar na página do editor de testes durante o salvamento\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Arrasta o teu ficheiro de áudio\",\n    \"description\": \"Arraste e solte arquivo de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Ou clica para procurar\",\n    \"description\": \"Ou clique para procurar arquivo de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Configurações\",\n    \"description\": \"Título de configurações de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Volume\",\n    \"description\": \"Rótulo de volume de áudio na página do editor de testes\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Aplicar\",\n    \"description\": \"Botão de atualizar áudio na página do editor de testes\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Recortar\",\n    \"description\": \"Título de recorte na página do editor de testes\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Largura\",\n    \"description\": \"Rótulo de largura para propriedades\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Altura\",\n    \"description\": \"Rótulo de altura para propriedades\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Esquerda\",\n    \"description\": \"Rótulo de esquerda para propriedades\"\n  },\n  \"topLabel\": {\n    \"message\": \"Topo\",\n    \"description\": \"Rótulo de topo para propriedades\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Arrasta as abas e usa os botões à esquerda para editar a secção selecionada.\",\n    \"description\": \"Informações sobre recorte na página do editor de testes\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Recortar vídeo\",\n    \"description\": \"Botão de recortar na página do editor de testes\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Recortando...\",\n    \"description\": \"Botão de recortar na página do editor de testes durante o recorte\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Cortar secção\",\n    \"description\": \"Botão de cortar na página do editor de testes\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Cortando...\",\n    \"description\": \"Botão de cortar na página do editor de testes durante o corte\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Silenciar áudio\",\n    \"description\": \"Botão de silenciar na página do editor de testes\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Silenciando...\",\n    \"description\": \"Botão de silenciar na página do editor de testes durante o silenciamento\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Sabe mais.\",\n    \"description\": \"Saiba mais com um ponto\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Desfazer\",\n    \"description\": \"Rótulo de desfazer\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Refazer\",\n    \"description\": \"Rótulo de refazer\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Deixa uma avaliação\",\n    \"description\": \"Botão de deixar uma avaliação\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Seguir para atualizações\",\n    \"description\": \"Botão de seguir para atualizações\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Atualmente offline\",\n    \"description\": \"Rótulo de offline\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Algumas funcionalidades estão indisponíveis até te reconectares\",\n    \"description\": \"Descrição do rótulo de offline\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Tentar novamente\",\n    \"description\": \"Botão de tentar novamente do rótulo de offline\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"O Chrome precisa de ser atualizado\",\n    \"description\": \"Rótulo de atualizar o Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Atualiza para aceder a mais funcionalidades\",\n    \"description\": \"Descrição do rótulo de atualizar o Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Sabe mais\",\n    \"description\": \"Botão de saiba mais\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Vídeo demasiado longo para processar no teu dispositivo\",\n    \"description\": \"Etiqueta de limite superior a 5 minutos no editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Edição e exportação em formato MP4 não estão disponíveis\",\n    \"description\": \"Descrição de limite superior a 5 minutos no editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"O vídeo está a ser processado...\",\n    \"description\": \"Rótulo de processamento de vídeo no editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Faz upgrade para um editor mais rápido\",\n    \"description\": \"Descrição do processamento de vídeo no editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Editar\",\n    \"description\": \"Título de edição na página do editor de testes\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Sem conexão\",\n    \"description\": \"Rótulo de sem conexão\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Indisponível\",\n    \"description\": \"Rótulo de não disponível\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Editar vídeo\",\n    \"description\": \"Rótulo de botão de recorte\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Remove, corta ou silencia o vídeo\",\n    \"description\": \"Rótulo de recorte\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"A preparar...\",\n    \"description\": \"Rótulo de preparação\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Recortar\",\n    \"description\": \"Rótulo de botão de recorte\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Recorta e redimensiona o vídeo\",\n    \"description\": \"Rótulo de recorte\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Adicionar áudio\",\n    \"description\": \"Rótulo de botão de adicionar áudio\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Carrega o teu próprio áudio para o vídeo\",\n    \"description\": \"Rótulo de adicionar áudio\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Guardar\",\n    \"description\": \"Título de salvar na página do editor de testes\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Sair do Google Drive\",\n    \"description\": \"Rótulo de sair do Google Drive\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Guardando...\",\n    \"description\": \"Rótulo de salvamento no Google Drive\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Guardar no Google Drive\",\n    \"description\": \"Botão de salvar no Google Drive\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Guarda a versão atual do vídeo\",\n    \"description\": \"Rótulo de salvar no Google Drive\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Inicia a sessão e guarda no Google Drive\",\n    \"description\": \"Rótulo de iniciar sessão no Google Drive\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Exportar\",\n    \"description\": \"Título de exportação na página do editor de testes\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Descarregando...\",\n    \"description\": \"Rótulo de download\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Descarregar como .webm\",\n    \"description\": \"Botão de baixar WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Exportar um vídeo .webm\",\n    \"description\": \"Rótulo de baixar WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Descarregar como .mp4\",\n    \"description\": \"Botão de baixar MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Exportar um vídeo .mp4 (recomendado)\",\n    \"description\": \"Rótulo de baixar MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Descarregar como .gif\",\n    \"description\": \"Botão de baixar GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Exportar um GIF (máximo de 30 segundos)\",\n    \"description\": \"Rótulo de baixar GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Pronto para melhorar as tuas gravações?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Partilha na cloud, usa zooms, modelos... e apoia uma dev indie que respeita a tua privacidade ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Sabe mais sobre o Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Já tens conta? Inicia sessão\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Partilhar vídeo\",\n    \"description\": \"Botão de partilha na página do editor de testes\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Substituir áudio existente\",\n    \"description\": \"Botão de substituição de áudio no editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Zoom para o cursor\",\n    \"description\": \"Popup de zoom para o ponto\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Permanece na página durante a gravação\",\n    \"description\": \"Popup de permanência na página\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Mostrar aviso de microfone desligado\",\n    \"description\": \"Popup de lembrete de microfone\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Ajuda\",\n    \"description\": \"Botão de popup de ajuda\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Gravação pausada. Pressiona o botão de reprodução para continuar.\",\n    \"description\": \"Título do modal de gravação pausada\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"O Screenity não tem permissão para gravar o teu ecrã\",\n    \"description\": \"Título do modal de permissões\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Para permitir a gravação de ecrã, deves dar permissões ao Screenity.\",\n    \"description\": \"Descrição do modal de permissões\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Ativar gravação de ecrã\",\n    \"description\": \"Ação do modal de permissões\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar do modal de permissões\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"O teu microfone está desligado\",\n    \"description\": \"Título do modal de microfone desligado\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Para incluir áudio na tua gravação, terás de ativar o teu microfone. Queres continuar sem áudio?\",\n    \"description\": \"Descrição do modal de microfone desligado\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Sim, continuar\",\n    \"description\": \"Continuar do modal de microfone desligado\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Cancelar do modal de microfone desligado\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Começa com o Screenity em três passos simples:\",\n    \"description\": \"Título dos passos de configuração\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Clica no ícone de \",\n    \"description\": \"Passo 1 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" extensões\",\n    \"description\": \"Passo 1 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Pressiona o ícone de \",\n    \"description\": \"Passo 2 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" alfinete\",\n    \"description\": \"Passo 2 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Clica no ícone de \",\n    \"description\": \"Passo 3 de configuração, antes do ícone. Precisa de um espaço no final.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity para começar\",\n    \"description\": \"Passo 3 de configuração, depois do ícone. Precisa de um espaço no início.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Fantástico! Vamos começar\",\n    \"description\": \"Título de configuração completa\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Agora podes começar a gravar aqui ou em qualquer outro separdor.\",\n    \"description\": \"Descrição de configuração completa\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Clica aqui para desenhar\",\n    \"description\": \"Instruções para clicar aqui\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Relaxa. Clica em qualquer lugar para parar a contagem decrescente.\",\n    \"description\": \"Mensagem de contagem regressiva\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"A edição de vídeos com durações curtas pode ser imprecisa devido aos pequenos intervalos entre imagens.\",\n    \"description\": \"Informação sobre o editor ser demasiado pequeno\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"O vídeo está a ser processado, a reprodução pode ser imprecisa ou interrompida.\",\n    \"description\": \"Banner de processamento no editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Recortar o vídeo pode demorar algum tempo\",\n    \"description\": \"Título da informação de recorte\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Subscreva para editar vídeos mais rapidamente\",\n    \"description\": \"Descrição da informação de recorte\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Microfone ativado\",\n    \"description\": \"Título do aviso de microfone ativado\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Microfone desativado\",\n    \"description\": \"Título do aviso de microfone desativado\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Ocultar notificações da interface\",\n    \"description\": \"Botão para ocultar alertas da interface\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Botão de não mostrar novamente\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Ocultar os controles quando não estiverem em uso\",\n    \"description\": \"Botão para mostrar a barra de ferramentas apenas ao passar o cursor\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Parar gravação\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Bem-vindo ao novo Screenity!\",\n    \"description\": \"Título do anúncio de atualização para usuários existentes\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Atualizamos o design e desenvolvemos novas funcionalidades, mas não te preocupes, continua a ser a mesma extensão gratuita, privada e de código de acesso livre.\",\n    \"description\": \"Descrição do anúncio de atualização para utilizadores existentes\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Obtém mais informações sobre a atualização.\",\n    \"description\": \"Link 'Saber mais' do anúncio de atualização para usuários existentes\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Começar\",\n    \"description\": \"Botão do anúncio de atualização para usuários existentes\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Erro ao iniciar a gravação\",\n    \"description\": \"Título do modal de erro de transmissão\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Houve um erro ao iniciar a gravação. Reinicia o teu navegador e tenta novamente. Se o problema persistir, por favor, entra em contato conosco em support@screenity.io.\",\n    \"description\": \"Descrição do modal de erro de transmissão\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Qualidade mais alta\",\n    \"description\": \"Rótulo de alternar a qualidade mais alta no popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Restaurar a última gravação\",\n    \"description\": \"Botão para restaurar a última gravação no popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"A ter problemas?\",\n    \"description\": \"Botão de problemas no editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Não consegue ver a sua gravação?\",\n    \"description\": \"Título do modal de problemas no editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Se completou a sua gravação e se encontra nesta página sem conseguir ver o seu vídeo, pode tentar descarregar os dados brutos do vídeo, se estiverem disponíveis. Também pode entrar em contacto e enviar um relatório de erro.\",\n    \"description\": \"Descrição do modal de problemas no editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Descarregar dados brutos do vídeo\",\n    \"description\": \"Botão do modal de problemas no editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Reportar erro\",\n    \"description\": \"Segundo botão do modal de problemas no editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Não foi encontrada nenhuma gravação, lamento :(\",\n    \"description\": \"Alerta de não foi encontrada nenhuma gravação no editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Limite de memória atingido\",\n    \"description\": \"Título de limite de memória atingido\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"O seu dispositivo ficou sem memória para gravar, pelo que a gravação foi interrompida automaticamente. Se desejar gravar por mais tempo, pode tentar reduzir a qualidade do vídeo ou libertar espaço no dispositivo.\",\n    \"description\": \"Descrição de limite de memória atingido\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Entendi\",\n    \"description\": \"Botão de entendi\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Espaço insuficiente\",\n    \"description\": \"Título de espaço insuficiente\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"O Screenity precisa de pelo menos 1 GB de espaço livre para gravar. Por favor, liberte espaço e tente novamente.\",\n    \"description\": \"Descrição de espaço insuficiente\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Apagar gravações anteriores\",\n    \"description\": \"Botão de apagar gravações anteriores\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Avançado\",\n    \"description\": \"Secção avançada na página do editor no ambiente seguro\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Transferir ficheiro de vídeo bruto\",\n    \"description\": \"Botão de transferência de gravação em bruto\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Exportar os dados originais da gravação\",\n    \"description\": \"Etiqueta do botão de transferência de gravação em bruto\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Obter ajuda com a tua gravação\",\n    \"description\": \"Botão de resolução de problemas\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Recupera o teu vídeo e resolve outros problemas\",\n    \"description\": \"Etiqueta de resolução de problemas\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Transferir ficheiro de vídeo bruto\",\n    \"description\": \"Título do modal de gravação em bruto\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"O ficheiro de vídeo bruto é o ficheiro original gravado pelo Screenity. Este vídeo não foi processado nem editado, pelo que pode ser muito grande e pode não ser reproduzível em todos os dispositivos. Se estiveres a ter problemas com o vídeo processado, podes usar este ficheiro para recuperar o teu vídeo.\",\n    \"description\": \"Descrição do modal de gravação em bruto\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Transferir\",\n    \"description\": \"Botão do modal de gravação em bruto\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Transferir informação do sistema e dados de vídeo para resolução de problemas\",\n    \"description\": \"Título do modal de resolução de problemas\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Após a transferência, envia o ficheiro para support@screenity.io com qualquer informação relevante. Utilizaremos estes dados para te ajudar a resolver quaisquer problemas que tenhas com a extensão e, se possível, recuperar o teu vídeo.\",\n    \"description\": \"Descrição do modal de resolução de problemas\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Transferir\",\n    \"description\": \"Botão do modal de resolução de problemas\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"O vídeo é demasiado longo para ser processado no teu dispositivo\",\n    \"description\": \"Título do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Uma vez que o Screenity é completamente privado e funciona localmente, está limitado pelo hardware do teu dispositivo. Processar um vídeo com esta duração levaria demasiado tempo e seria muito intensivo em recursos. Não te preocupes, ainda podes descarregar o ficheiro WEBM ou a gravação em bruto, mas a edição e a exportação para MP4 não estão disponíveis. Dito isto, ainda podes tentar processar o vídeo mesmo assim, se compreenderes os riscos.\",\n    \"description\": \"Descrição do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Compreendo o risco, tentar mesmo assim\",\n    \"description\": \"Botão do modal de limite superior a 5 minutos\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Sabe mais sobre o processamento de vídeos longos.\",\n    \"description\": \"Link 'Sabe mais' do modal de limite superior a 5 minutos com um ponto\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Modo de recuperação\",\n    \"description\": \"Título do modo de recuperação\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Descarregar dados para solucionar problemas\",\n    \"description\": \"Opção de descarregamento para solucionar problemas\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Centro de Ajuda\",\n    \"description\": \"Item de Navegação para Obter Ajuda\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título do Aviso de Áudio\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Para gravar áudio desta página, precisa alternar para '$tab$'.\",\n    \"description\": \"Descrição do Aviso de Áudio\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Área da Guia\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Extensão não suportada na guia\",\n    \"description\": \"Título de Extensão não Suportada na Guia\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"O Screenity não foi suportado na sua guia anterior. Ainda pode iniciar a sua gravação aqui e alternar para a guia, mas a sua câmara e barra de ferramentas não serão visíveis.\",\n    \"description\": \"Descrição de Extensão não Suportada na Guia\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Configurar uma Cópia de Segurança Local para as suas Gravações no Screenity\",\n    \"description\": \"Título de Cópias de Segurança\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Mantenha uma cópia de todas as gravações que efetuar. Se não configurar cópias de segurança, as suas gravações só serão guardadas quando optar por as descarregar.\",\n    \"description\": \"Descrição de Cópias de Segurança\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Poderá ser necessário criar uma nova pasta primeiro se estiver a tentar guardar em 'Transferências' ou outras pastas do sistema.\",\n    \"description\": \"Descrição de Cópias de Segurança\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Selecionar uma Pasta\",\n    \"description\": \"Botão Selecionar Pasta\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Agora Não\",\n    \"description\": \"Botão Agora Não\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"As Suas Gravações Estão a Ser Guardadas Localmente\",\n    \"description\": \"Título de Cópias de Segurança Ativas\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Para evitar pedir permissões sempre que efetuar gravações, recomendamos que deixe esta guia aberta.\",\n    \"description\": \"Descrição de Cópias de Segurança Ativas\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Fechar Guia de Qualquer Maneira\",\n    \"description\": \"Botão Fechar Guia de Qualquer Maneira\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Parar Todas as Cópias de Segurança de Gravações\",\n    \"description\": \"Botão Parar Cópias de Segurança\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Por Favor, Reconfirme o Acesso à Sua Pasta Local de Cópias de Segurança do Screenity\",\n    \"description\": \"Título de Confirmação de Cópias de Segurança\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Lamentamos, mas precisamos da sua permissão mais uma vez para continuar a efetuar cópias locais das suas gravações. Infelizmente, teremos de pedir sempre que encerrar esta guia, o que pode acontecer se encerrar o navegador ou reiniciar o seu computador.\",\n    \"description\": \"Descrição de Confirmação de Cópias de Segurança\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Permitir ao Screenity Continuar a Efetuar Cópias de Segurança das Gravações\",\n    \"description\": \"Botão Permitir na Confirmação de Cópias de Segurança\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Efetuar Cópias de Segurança das Gravações\",\n    \"description\": \"Etiqueta de Alternar Cópias de Segurança\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Permissão Não Concedida para Efetuar uma Cópia de Segurança da Gravação\",\n    \"description\": \"Título de Falha de Permissão de Cópia de Segurança\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Não selecionou uma pasta para efetuar uma cópia local das suas gravações. Dependendo da versão do seu navegador ou sistema operativo, poderá ser solicitado a selecionar a pasta sempre que iniciar uma gravação. Pode tentar novamente ou desativar as cópias de segurança por completo se preferir não conceder permissões repetidamente.\",\n    \"description\": \"Descrição de Falha de Permissão de Cópia de Segurança\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título do Aviso de Áudio para macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"No macOS, só pode gravar áudio das guias. Selecione a opção 'Guia do Chrome' e certifique-se de que 'Também partilhar áudio da guia' está ativado.\",\n    \"description\": \"Descrição do Aviso de Áudio para macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Gravar Áudio do Computador\",\n    \"description\": \"Título do Aviso de Áudio para Outros Sistemas\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Certifique-se de ativar a opção 'Partilhar áudio do sistema' se desejar gravar áudio do computador. Se não visualizar esta opção, tente atualizar o Chrome ou alternar para a gravação de guias.\",\n    \"description\": \"Descrição do Aviso de Áudio para Outros Sistemas\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Resolução máxima\",\n    \"description\": \"Etiqueta de resolução máxima no menu de configurações\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Máximo de FPS\",\n    \"description\": \"Etiqueta de máximo de FPS no menu de configurações\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Memória RAM insuficiente\",\n    \"description\": \"Etiqueta de memória RAM insuficiente no menu de configurações\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Redimensionar janela\",\n    \"description\": \"Rótulo de redimensionar janela no menu de configurações\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"O ecrã é demasiado pequeno para esta resolução\",\n    \"description\": \"Dica de ecrã muito pequeno no menu de configurações\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Não é possível gravar nesta resolução no seu dispositivo\",\n    \"description\": \"Dica de resolução máxima no menu de configurações\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Rever permissões\",\n    \"description\": \"Botão de rever permissões\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Adicionar outra gravação\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"A preparar tudo...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Espera um momento, estamos a preparar a tua gravação.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Barra de ferramentas oculta. Reativa-a nas opções.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Iniciar sessão ou registar\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Terminar sessão\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Sessão terminada\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Atingiste o limite de armazenamento\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Usaste os 100 GB de armazenamento. Elimina vídeos ou cenas antigas para continuares a gravar.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Gerir armazenamento\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Fechar\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Não foi possível verificar o armazenamento\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Ocorreu um problema ao verificar o armazenamento. Inicia sessão novamente e tenta gravar outra vez.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Não foi possível verificar a tua quota de armazenamento. Tenta novamente dentro de segundos ou atualiza a página.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Tentar novamente\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Cena adicionada à gravação Multi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Pronta para gravar uma nova cena no teu vídeo\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"A gravação está a aproximar-se do limite de 90 minutos, será interrompida em breve\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Gravação interrompida ao atingir o limite de 90 minutos\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"A gravação do separador está desativada nesta página. A gravação de ecrã foi ativada por defeito.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Gravação do vídeo cancelada\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Ligação copiada para a área de transferência\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Não foi possível copiar a ligação\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Mais recentes\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Mais antigos\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Todos os vídeos\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Não foram encontrados vídeos\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"A carregar vídeos...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Ir para o painel\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Bem-vinda ao Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Sem anúncios. Sem rastreamento. Sem limites.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Grava, simplesmente.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Usa gratuitamente — sem registo!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Queres fazer mais com os teus vídeos?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Armazenamento na cloud, partilha por ligação\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Combina vários clips como capítulos de uma história\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Zooms inteligentes com clique (ou adiciona os teus!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Adiciona modelos, legendas, mockups...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Fundos e disposição da câmara personalizados\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Desbloqueia funcionalidades extra\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Apoia o desenvolvimento independente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Definições da conta\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Suporte\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Subscrição Pro inativa\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"A tua subscrição do Screenity Pro está inativa.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Reativa-a para voltares a ter acesso.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Os teus vídeos e dados serão eliminados permanentemente a \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Gerir subscrição\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Queres continuar a usar a versão gratuita?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Terminar sessão e mudar para a versão gratuita\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Sessão terminada\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Para manter as tuas gravações sincronizadas e aceder às funcionalidades Pro, inicia sessão novamente.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Iniciar sessão novamente\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Podes continuar a usar a extensão sem conta, mas as gravações não serão guardadas nem recuperáveis mais tarde.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Continuar sem iniciar sessão\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Modo de gravação instantânea\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Download imediato, mas não será possível editar a disposição da câmara depois de começar a gravação.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Modo de gravação instantânea\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Este modo grava tudo num único vídeo para descarregar e partilhar de imediato. Não podes alterar a disposição da câmara, mas podes fazer outras edições.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Percebi\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"A gravação de separadores está desativada nesta página\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"A adicionar a: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Terminar\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Queres fazer mais com as tuas gravações?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Inicia sessão para guardar os vídeos na cloud, partilhar com ligação e aceder a funcionalidades avançadas de edição.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Inicia sessão para desbloquear funcionalidades Pro\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Apoia a criadora independente!\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Desbloquear mais funcionalidades\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Inicia sessão para partilhar (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Desbloqueia armazenamento na cloud, edição multi-cena, zooms automáticos, legendas e mais\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"O Screenity será sempre gratuito, open-source e sem anúncios. O Pro ajuda a suportar os custos da cloud e infraestrutura, e apoia uma criadora independente ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Não mostrar novamente\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Experimentar\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Modo Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Grava várias cenas, como o ecrã, a câmara ou ambos, uma a seguir à outra. É ótimo para fazer várias tentativas, mudar de perspetiva ou dividir a gravação em partes. Quando terminares, clica em Concluir para abrir o editor com todas as cenas combinadas num único vídeo.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Ativa o Pro para começar\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Subscreve para aceder às funcionalidades Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Subscreve o Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"A usar o Screenity localmente? Ajuda a apoiar o futuro do projeto!\",\n    \"description\": \"Título do banner de autoalojamento no editor\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"O Screenity é desenvolvido por uma programadora indie. Autoalojar é gratuito, mas se achas útil, considera apoiar com o Pro ❤️\",\n    \"description\": \"Descrição do banner de autoalojamento no editor\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Bem-vindo de volta ao Screenity\",\n    \"description\": \"Título do popup de boas-vindas mostrado aos utilizadores que regressam\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Novidade: Leva as tuas gravações para o próximo nível\",\n    \"description\": \"Título para a funcionalidade de armazenamento na cloud no popup de boas-vindas\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"O Screenity Pro é uma nova plataforma opcional para guardar vídeos na cloud, partilhá-los com um link e desbloquear ferramentas avançadas de edição, se e quando precisares.\",\n    \"description\": \"Descrição da funcionalidade de armazenamento na cloud no popup de boas-vindas\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Sabe mais sobre o Screenity Pro\",\n    \"description\": \"Botão de chamada para ação da funcionalidade de armazenamento na cloud no popup de boas-vindas\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Bem-vindo ao Screenity Pro\",\n    \"description\": \"Título do modal de boas-vindas ao Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Está tudo pronto para começares a gravar! Algumas coisas a ter em conta:\\n\\n- A bolha da câmara pode desaparecer durante a gravação – isto é normal. É capturada separadamente em segundo plano, para que possas posicioná-la como quiseres mais tarde.\\n- Os zooms são criados automaticamente apenas em cliques dentro de separadores do Chrome, devido a uma limitação do navegador. Podes sempre adicionar mais zooms manualmente após a gravação.\\n- Para gravações rápidas com download imediato, experimenta o Modo Instantâneo. Tem em atenção que as opções de edição são limitadas: sem fundos, layouts ou ajustes avançados.\",\n    \"description\": \"Descrição do modal de boas-vindas ao Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Percebi\",\n    \"description\": \"Botão de ação do modal de boas-vindas ao Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 falhou — off\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Bem-vindo à extensão Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Algumas dicas rápidas antes de gravar.<br/>Os zooms automáticos só funcionam com cliques em <strong>separadores do Chrome</strong>. Pode sempre adicionar mais zooms manualmente depois.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Barra de gravação e efeitos\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Use esta barra para desenhar, aplicar efeitos do cursor, desfocar e controlar a gravação.<br/><br/><strong>Esta barra aparece no vídeo</strong> se não a ocultar.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"A câmara é captada em separado\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Durante a gravação, a câmara pode <strong>esconder-se ou passar para PiP</strong>, é normal.<br/><br/>É captada em separado para a poder posicionar depois.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Modo instantâneo\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Ideal para partilha rápida com <strong>transferências ilimitadas</strong>.<br/><br/>As opções avançadas de layout e edição não estão disponíveis neste modo.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Percebi\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Seguinte\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Voltar\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Saber mais\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Repor onboarding\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Pouco espaço em disco. A guardar a gravação. Tudo o que foi capturado até agora está seguro.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Gravação longa\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"A sua gravação está completa e segura. Transfira como WebM abaixo.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Indisponível para gravações longas\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Indisponível no modo de recuperação\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Edição de áudio demasiado longa\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Esta edição de áudio não é suportada para clipes com mais de 15 minutos. O seu original não foi alterado. Tente cortar o clipe primeiro.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Tempo da edição esgotado\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"A edição não foi concluída a tempo. A sua gravação original não foi alterada. Tente novamente ou corte o clipe primeiro.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"A partilha de ecrã parou. A sua gravação está guardada. Pare quando estiver pronto.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Houve um problema no processamento, mas a sua gravação está segura. Pode transferi-la abaixo.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"A processar a sua edição. A gravação original não foi alterada.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Não foi possível aplicar a edição\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Algo correu mal durante o processamento. A sua gravação original está segura. Pode tentar novamente ou transferi-la como está.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"A partilha de ecrã parou. A guardar a gravação. Tudo o que foi capturado até agora está seguro.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Áudio desligado. A guardar a gravação. O vídeo capturado até agora está seguro.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"A sua gravação foi guardada. O editor não abriu. Clique no ícone do Screenity para tentar novamente.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"A gravação está a demorar mais do que o esperado. Os seus dados estão seguros. O editor abrirá em breve.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Copiar info de depuração\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Info de depuração copiada\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Obter ajuda\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"A gravação foi curta demais para guardar\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Gravador r\\u00e1pido falhou\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"A sa\\u00edda do gravador r\\u00e1pido n\\u00e3o p\\u00f4de ser validada neste dispositivo.\\nPode transferir o ficheiro na mesma.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Transferir na mesma\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Cancelar\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/ru/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Запись экрана & Аннотации\",\n    \"description\": \"Название расширения\"\n  },\n  \"extDesc\": {\n    \"message\": \"Лучший бесплатный экран записи без ограничений. Захват, аннотация, масштабирование, размытие, редактирование видео и многое другое - без необходимости входа и с уважением к конфиденциальности.\",\n    \"description\": \"Описание расширения\"\n  },\n  \"recordTab\": {\n    \"message\": \"Запись\",\n    \"description\": \"Метка вкладки записи на всплывающем окне\"\n  },\n  \"videosTab\": {\n    \"message\": \"Ваши видео\",\n    \"description\": \"Метка вкладки видео на всплывающем окне\"\n  },\n  \"screenType\": {\n    \"message\": \"Экран\",\n    \"description\": \"Метка типа записи экрана на всплывающем окне\"\n  },\n  \"tabType\": {\n    \"message\": \"Область вкладки\",\n    \"description\": \"Метка типа записи области вкладки на всплывающем окне\"\n  },\n  \"cameraType\": {\n    \"message\": \"Камера\",\n    \"description\": \"Метка типа записи с камеры на всплывающем окне\"\n  },\n  \"mockupType\": {\n    \"message\": \"Макет\",\n    \"description\": \"Метка типа записи макета на всплывающем окне\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Запись в произвольной области отключена\",\n    \"description\": \"Предупреждение о выключенной записи в произвольной области для версии Chrome старше 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Обновите версию Chrome до 110+\",\n    \"description\": \"Описание предупреждения о выключенной записи в произвольной области для версии Chrome старше 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Обновить\",\n    \"description\": \"Действие предупреждения о выключенной записи в произвольной области для версии Chrome старше 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Проверьте разрешения\",\n    \"description\": \"Заголовок модального окна предупреждения о разрешениях для камеры / микрофона\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Похоже, Screenity не может получить доступ к вашей камере или микрофону. Убедитесь, что разрешаете доступ, нажав на значок камеры в строке адреса.\",\n    \"description\": \"Описание модального окна предупреждения о разрешениях для камеры / микрофона\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Отклонить\",\n    \"description\": \"Кнопка закрытия модального окна предупреждения о разрешениях для камеры / микрофона\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Разрешить доступ к камере\",\n    \"description\": \"Кнопка разрешения доступа к камере\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Разрешить доступ к микрофону\",\n    \"description\": \"Кнопка разрешения доступа к микрофону\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Нажмите, чтобы говорить\",\n    \"description\": \"Метка переключения 'Push to talk'\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Установите область для записи\",\n    \"description\": \"Метка переключения 'Custom area'\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Повернуть камеру\",\n    \"description\": \"Метка переключения 'Flip camera'\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Эффекты фона\",\n    \"description\": \"Метка переключения 'Background effects'\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Начать запись\",\n    \"description\": \"Метка кнопки записи\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Запуск записи...\",\n    \"description\": \"Метка кнопки записи в процессе\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Выберите камеру для записи\",\n    \"description\": \"Метка кнопки записи в режиме камеры, когда не выбрана камера\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Показать больше опций\",\n    \"description\": \"Метка для показа больше опций\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Включить системный звук\",\n    \"description\": \"Метка системного звука/вкладка\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Скрыть панель инструментов\",\n    \"description\": \"Метка для скрытия панели инструментов\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Обратный отсчет\",\n    \"description\": \"Метка для обратного отсчета\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Установить временное ограничение\",\n    \"description\": \"Метка для установки временного ограничения\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Размыть\",\n    \"description\": \"Метка для размытия фона\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Нет\",\n    \"description\": \"Не выбрано устройство\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Нет камеры\",\n    \"description\": \"Не выбрана камера\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Нет микрофона\",\n    \"description\": \"Не выбран микрофон\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Выберите источник\",\n    \"description\": \"Заглушка для выбора источника\"\n  },\n  \"offLabel\": {\n    \"message\": \"Выкл\",\n    \"description\": \"Метка для отключения в выпадающем списке\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Ширина\",\n    \"description\": \"Метка для ширины области\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Высота\",\n    \"description\": \"Метка для высоты области\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Щелкните, чтобы разместить изображение\",\n    \"description\": \"Всплывающее уведомление при добавлении нового изображения\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Завершить запись\",\n    \"description\": \"Подсказка для завершения записи\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Перезапустить запись\",\n    \"description\": \"Подсказка для перезапуска записи\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Приостановить запись\",\n    \"description\": \"Подсказка для приостановки записи\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Продолжить запись\",\n    \"description\": \"Подсказка для продолжения записи\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Отменить запись\",\n    \"description\": \"Подсказка для отмены записи\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Переключить инструменты рисования\",\n    \"description\": \"Подсказка для переключения инструментов рисования\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Переключить инструмент размытия\",\n    \"description\": \"Подсказка для переключения инструментов размытия\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Переключить настройки курсора\",\n    \"description\": \"Подсказка для переключения настроек курсора\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Отключить камеру\",\n    \"description\": \"Подсказка для отключения камеры\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Включить камеру\",\n    \"description\": \"Подсказка для включения камеры\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Нет разрешения на использование камеры\",\n    \"description\": \"Подсказка о отсутствии разрешения на использование камеры\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Отключить микрофон\",\n    \"description\": \"Подсказка для отключения микрофона\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Включить микрофон\",\n    \"description\": \"Подсказка для включения микрофона\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Нет разрешения на использование микрофона\",\n    \"description\": \"Подсказка о отсутствии разрешения на использование микрофона\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Выбрать инструмент\",\n    \"description\": \"Подсказка для выбора инструмента\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Инструмент ручки\",\n    \"description\": \"Подсказка для инструмента ручки\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Инструмент маркера\",\n    \"description\": \"Подсказка для инструмента маркера\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Инструмент стирания\",\n    \"description\": \"Подсказка для инструмента стирания\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Инструмент текста\",\n    \"description\": \"Подсказка для инструмента текста\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Инструмент фигур\",\n    \"description\": \"Подсказка для инструмента фигур\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Инструмент Стрелка\",\n    \"description\": \"Подсказка для инструмента Стрелка\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Инструмент Изображение\",\n    \"description\": \"Подсказка для инструмента Изображение\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Отменить\",\n    \"description\": \"Подсказка для отмены\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Повторить\",\n    \"description\": \"Подсказка для повтора\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Очистить холст\",\n    \"description\": \"Подсказка для очистки холста\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Дополнительные цвета\",\n    \"description\": \"Подсказка для дополнительных цветов\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Толстая линия\",\n    \"description\": \"Подсказка для толстой линии\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Средняя линия\",\n    \"description\": \"Подсказка для средней линии\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Тонкая линия\",\n    \"description\": \"Подсказка для тонкой линии\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Переключить режим изображения в картине\",\n    \"description\": \"Подсказка для переключения режима изображения в картине\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Включить заливку\",\n    \"description\": \"Подсказка для включения заливки\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Режим рисования\",\n    \"description\": \"Уведомление о режиме рисования\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Режим размытия\",\n    \"description\": \"Уведомление о режиме размытия\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Очистить все размытые элементы\",\n    \"description\": \"Подсказка для очистки всех размытых элементов\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Подсветить клики\",\n    \"description\": \"Подсказка для подсветки кликов\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Подсветить курсор\",\n    \"description\": \"Подсказка для подсветки курсора\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Прожектор на курсор\",\n    \"description\": \"Подсказка для прожектора на курсор\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Больше не показывать\",\n    \"description\": \"Кнопка отклонения в модальном окне разрешений\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Вы уверены, что хотите перезапустить запись?\",\n    \"description\": \"Заголовок модального окна перезапуска записи\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Ваш текущий прогресс видеозаписи будет потерян.\",\n    \"description\": \"Описание модального окна перезапуска записи\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Перезапустить запись\",\n    \"description\": \"Кнопка перезапуска записи в модальном окне\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Продолжить запись\",\n    \"description\": \"Кнопка продолжения записи в модальном окне\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Вы уверены, что хотите отменить запись?\",\n    \"description\": \"Заголовок модального окна отмены записи\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Ваш текущий прогресс видеозаписи будет потерян.\",\n    \"description\": \"Описание модального окна отмены записи\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Отменить запись\",\n    \"description\": \"Кнопка отмены записи в модальном окне\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Продолжить запись\",\n    \"description\": \"Кнопка продолжения записи в модальном окне\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Выберите, что вы хотите записать\",\n    \"description\": \"Заголовок на странице записи\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Идет запись...\",\n    \"description\": \"Заголовок на странице записи во время записи\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Оставьте эту вкладку открытой. Она закроется, когда ваша запись будет сохранена.\",\n    \"description\": \"Описание на странице записи\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Подготовка записи...\",\n    \"description\": \"Заголовок на странице песочницы во время записи / сохранения\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Оставьте эту вкладку открытой. Она будет обновлена с вашей записью, как только она будет готова.\",\n    \"description\": \"Описание на странице песочницы во время записи / сохранения\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Редактор\",\n    \"description\": \"Заголовок в навигации страницы редактора песочницы\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Отмена\",\n    \"description\": \"Кнопка отмены на странице редактора песочницы\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Вернуть к оригиналу\",\n    \"description\": \"Кнопка отмены в редакторе песочницы\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Сбросить\",\n    \"description\": \"Кнопка сброса в редакторе песочницы\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Сохранить изменения\",\n    \"description\": \"Кнопка сохранения в редакторе песочницы\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Сохранение...\",\n    \"description\": \"Кнопка сохранения в процессе сохранения в редакторе песочницы\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Перетащите ваш аудиофайл\",\n    \"description\": \"Перетащите аудиофайл в редактор песочницы\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Или нажмите, чтобы выбрать\",\n    \"description\": \"Или нажмите, чтобы выбрать аудиофайл в редакторе песочницы\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Настройки\",\n    \"description\": \"Заголовок настроек аудио в редакторе песочницы\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Громкость\",\n    \"description\": \"Метка громкости аудио в редакторе песочницы\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Обновить\",\n    \"description\": \"Кнопка обновления аудио в редакторе песочницы\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Обрезать\",\n    \"description\": \"Заголовок обрезки в редакторе песочницы\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Ширина\",\n    \"description\": \"Метка ширины для свойств\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Высота\",\n    \"description\": \"Метка высоты для свойств\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Слева\",\n    \"description\": \"Метка слева для свойств\"\n  },\n  \"topLabel\": {\n    \"message\": \"Сверху\",\n    \"description\": \"Метка сверху для свойств\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Перетащите ручки и используйте кнопки слева, чтобы редактировать выбранный участок.\",\n    \"description\": \"Информация о обрезке в редакторе песочницы\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Обрезать видео\",\n    \"description\": \"Кнопка обрезки в редакторе песочницы\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Обрезка...\",\n    \"description\": \"Кнопка обрезки в редакторе песочницы во время обрезки\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Вырезать участок\",\n    \"description\": \"Кнопка вырезки в редакторе песочницы\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Вырезка...\",\n    \"description\": \"Кнопка вырезки в редакторе песочницы во время вырезки\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Выключить звук\",\n    \"description\": \"Кнопка выключения звука в редакторе песочницы\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Выключение звука...\",\n    \"description\": \"Кнопка выключения звука в редакторе песочницы во время выключения звука\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Узнать больше.\",\n    \"description\": \"Узнать больше с точкой\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Отменить\",\n    \"description\": \"Метка отмены\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Повторить\",\n    \"description\": \"Метка повтора\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Оставьте отзыв\",\n    \"description\": \"Кнопка оставления отзыва\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Подписаться на обновления\",\n    \"description\": \"Кнопка подписки на обновления\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"В данный момент вы офлайн\",\n    \"description\": \"Метка офлайн\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Некоторые функции недоступны до восстановления подключения\",\n    \"description\": \"Описание метки офлайн\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Попробуйте снова\",\n    \"description\": \"Кнопка повторной попытки при офлайне\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Требуется обновление Chrome\",\n    \"description\": \"Метка обновления Chrome\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Обновите для доступа к дополнительным функциям\",\n    \"description\": \"Описание метки обновления Chrome\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Узнать больше\",\n    \"description\": \"Кнопка узнать больше\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Видео слишком длинное для обработки локально\",\n    \"description\": \"Метка ограничения более 5 минут в редакторе\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Редактирование и экспорт в формате MP4 недоступны\",\n    \"description\": \"Описание ограничения более 5 минут в редакторе\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Видео обрабатывается...\",\n    \"description\": \"Метка обработки видео в редакторе\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Обновите для более быстрого редактирования\",\n    \"description\": \"Описание обработки видео в редакторе\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Редактирование\",\n    \"description\": \"Заголовок редактора в песочнице\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Нет соединения\",\n    \"description\": \"Метка отсутствия соединения\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Недоступно\",\n    \"description\": \"Метка недоступности\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Редактировать видео\",\n    \"description\": \"Кнопка обрезки\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Обрезать, удалять или отключать звук в видео\",\n    \"description\": \"Метка обрезки\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Подготовка...\",\n    \"description\": \"Метка подготовки\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Обрезать\",\n    \"description\": \"Кнопка обрезки\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Обрезать и изменять размер видео\",\n    \"description\": \"Метка обрезки\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Добавить аудио\",\n    \"description\": \"Кнопка добавления аудио\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Загрузите свой звук для добавления в видео\",\n    \"description\": \"Метка добавления аудио\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Сохранить\",\n    \"description\": \"Заголовок сохранения в песочнице\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Выйти из Диска\",\n    \"description\": \"Метка выхода из Google Диска\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Сохранение...\",\n    \"description\": \"Метка сохранения в Google Диск\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Сохранить на Диск\",\n    \"description\": \"Кнопка сохранения в Google Диск\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Сохранить текущую версию на Google Диск\",\n    \"description\": \"Метка сохранения в Google Диск\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Войти и сохранить на Диск\",\n    \"description\": \"Метка входа в Google Диск\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Экспорт\",\n    \"description\": \"Заголовок экспорта в песочнице\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Загрузка...\",\n    \"description\": \"Метка загрузки\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Скачать как .webm\",\n    \"description\": \"Кнопка скачивания WEBM\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Экспортировать видео в формате .webm\",\n    \"description\": \"Метка скачивания WEBM\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Скачать как .mp4\",\n    \"description\": \"Кнопка скачивания MP4\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Экспортировать видео в формате .mp4 (рекомендуется)\",\n    \"description\": \"Метка скачивания MP4\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Скачать как .gif\",\n    \"description\": \"Кнопка скачивания GIF\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Экспортировать GIF (максимум 30 секунд)\",\n    \"description\": \"Метка скачивания GIF\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Готов улучшить свои записи?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Облачный доступ, зум, шаблоны... и поддержка инструмента от инди-разработчицы, уважающей твою приватность ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Узнать больше о Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Уже есть аккаунт? Войти\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Поделиться видео\",\n    \"description\": \"Кнопка обмена в редакторе песочницы\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Заменить существующий аудиофайл\",\n    \"description\": \"Кнопка замены аудиофайла в редакторе\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Увеличить к курсору\",\n    \"description\": \"Всплывающее окно для увеличения к курсору\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Оставаться на странице во время записи\",\n    \"description\": \"Всплывающее окно для оставания на странице\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Предупреждение: микрофон выключен\",\n    \"description\": \"Всплывающее окно напоминания о микрофоне\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Помощь\",\n    \"description\": \"Кнопка всплывающего окна справки\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Запись приостановлена. Нажмите кнопку воспроизведения, чтобы продолжить.\",\n    \"description\": \"Заголовок всплывающего окна приостановленной записи\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity не имеет разрешения на запись вашего экрана\",\n    \"description\": \"Заголовок модального окна разрешения\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Для включения записи экрана вы должны предоставить разрешение на запись экрана при запросе от Screenity.\",\n    \"description\": \"Описание модального окна разрешения\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Включить запись экрана\",\n    \"description\": \"Действие модального окна разрешения\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Отмена\",\n    \"description\": \"Отмена модального окна разрешения\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Ваш микрофон отключен\",\n    \"description\": \"Заголовок модального окна отключенного микрофона\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Для включения звука в вашей записи вам нужно включить микрофон. Хотите продолжить без звука?\",\n    \"description\": \"Описание модального окна отключенного микрофона\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Да, продолжить\",\n    \"description\": \"Действие модального окна отключенного микрофона\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Отмена\",\n    \"description\": \"Отмена модального окна отключенного микрофона\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Начните работу с Screenity в трех простых шагах:\",\n    \"description\": \"Заголовок шагов настройки\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Нажмите на \",\n    \"description\": \"Шаг 1 настройки, перед значком. Требуется пробел в конце.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" значок расширения\",\n    \"description\": \"Шаг 1 настройки, после значка. Требуется пробел в начале.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Нажмите на \",\n    \"description\": \"Шаг 2 настройки, перед значком. Требуется пробел в конце.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" значок закрепления\",\n    \"description\": \"Шаг 2 настройки, после значка. Требуется пробел в начале.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Нажмите на \",\n    \"description\": \"Шаг 3 настройки, перед значком. Требуется пробел в конце.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" значок Screenity для запуска\",\n    \"description\": \"Шаг 3 настройки, после значка. Требуется пробел в начале.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Отлично! Вы готовы\",\n    \"description\": \"Заголовок завершения настройки\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Теперь вы можете начать запись здесь или в любой другой вкладке.\",\n    \"description\": \"Описание завершения настройки\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Нажмите здесь, чтобы рисовать\",\n    \"description\": \"Сообщение об обучении: нажмите здесь\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Расслабьтесь. Нажмите в любом месте, чтобы остановить обратный отсчет.\",\n    \"description\": \"Сообщение обратного отсчета\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Изменения могут быть неточными для коротких временных интервалов из-за интервалов между кадрами.\",\n    \"description\": \"Информация о слишком маленьком редакторе\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Видео обрабатывается, воспроизведение может быть неточным или прерываться.\",\n    \"description\": \"Баннер обработки в редакторе\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Обрезка может занять некоторое время\",\n    \"description\": \"Заголовок информации об обрезке\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Обновите для более быстрой обработки\",\n    \"description\": \"Описание информации об обрезке\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Микрофон включен\",\n    \"description\": \"Заголовок всплывающего окна включенного микрофона\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Микрофон отключен\",\n    \"description\": \"Заголовок всплывающего окна отключенного микрофона\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Скрыть уведомления интерфейса\",\n    \"description\": \"Кнопка скрытия уведомлений интерфейса\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Не показывать снова\",\n    \"description\": \"Кнопка 'Не показывать снова'\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Скрывать панель инструментов, когда она не используется\",\n    \"description\": \"Кнопка 'Показывать панель инструментов только при наведении'\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Остановить запись\",\n    \"description\": \"Кнопка 'Остановить запись' на экране записи\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Добро пожаловать в новый Screenity!\",\n    \"description\": \"Заголовок объявления об обновлении для существующих пользователей\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Мы представляем новый дизайн и множество увлекательных функций, но не волнуйтесь, это все еще бесплатное, частное и открытое расширение, которое вы знаете и любите.\",\n    \"description\": \"Описание объявления об обновлении для существующих пользователей\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Узнать больше об обновлении.\",\n    \"description\": \"Ссылка 'Узнать больше' в объявлении об обновлении для существующих пользователей\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Начать\",\n    \"description\": \"Кнопка 'Начать' в объявлении об обновлении для существующих пользователей\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Ошибка при запуске записи\",\n    \"description\": \"Заголовок модального окна ошибки потока\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Похоже, произошла ошибка при запуске записи. Перезапустите браузер и попробуйте снова. Если проблема сохраняется, пожалуйста, свяжитесь с нами по адресу support@screenity.io.\",\n    \"description\": \"Описание модального окна ошибки потока\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Наивысшее качество\",\n    \"description\": \"Метка переключения наивысшего качества во всплывающем окне\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Восстановить последнюю запись\",\n    \"description\": \"Кнопка восстановления последней записи во всплывающем окне\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Возникли проблемы?\",\n    \"description\": \"Кнопка проблем в редакторе\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Не видите свою запись?\",\n    \"description\": \"Заголовок модального окна проблем в редакторе\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Если вы завершили запись и оказались на этой странице, не видя свое видео, вы можете попробовать скачать необработанные видеоданные, если они доступны. Вы также можете связаться с нами и отправить отчет об ошибке.\",\n    \"description\": \"Описание модального окна проблем в редакторе\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Скачать необработанные видеоданные\",\n    \"description\": \"Кнопка в модальном окне проблем в редакторе\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Сообщить о проблеме\",\n    \"description\": \"Вторая кнопка в модальном окне проблем в редакторе\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Запись не найдена, извините :(\",\n    \"description\": \"Предупреждение о отсутствии записи в редакторе\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Достигнут лимит памяти\",\n    \"description\": \"Заголовок сообщения о достижении лимита памяти\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Ваше устройство исчерпало доступную память для записи, что привело к автоматической остановке записи. Если вы хотите записывать дольше, вы можете попробовать уменьшить качество видео или освободить место на вашем устройстве.\",\n    \"description\": \"Описание сообщения о достижении лимита памяти\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Понятно\",\n    \"description\": \"Кнопка 'Понятно'\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Недостаточно места\",\n    \"description\": \"Заголовок сообщения о нехватке места\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Для работы Screenity требуется как минимум 1 ГБ свободного места. Пожалуйста, освободите место на вашем устройстве и попробуйте снова.\",\n    \"description\": \"Описание сообщения о нехватке места\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Удалить предыдущие записи\",\n    \"description\": \"Кнопка 'Удалить предыдущие записи'\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Дополнительно\",\n    \"description\": \"Раздел 'Дополнительно' на странице редактора в режиме песочницы\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Скачать необработанный видеофайл\",\n    \"description\": \"Кнопка скачивания необработанной записи\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Экспорт исходных данных записи\",\n    \"description\": \"Метка скачивания необработанной записи\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Получить помощь по вашей записи\",\n    \"description\": \"Кнопка Устранения неполадок\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Восстановите свое видео и решите другие проблемы\",\n    \"description\": \"Метка Устранения неполадок\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Скачать необработанный видеофайл\",\n    \"description\": \"Заголовок модального окна необработанной записи\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Необработанный видеофайл - это оригинальный видеофайл, записанный Screenity. Этот видеофайл не обработан и не редактирован, поэтому он может быть очень большим и не всегда можно воспроизвести на всех устройствах. Если у вас возникли проблемы с обработанным видео, вы можете использовать этот файл для восстановления своего видео.\",\n    \"description\": \"Описание модального окна необработанной записи\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Скачать\",\n    \"description\": \"Кнопка модального окна необработанной записи\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Скачать информацию о системе и видеоданные для устранения неполадок\",\n    \"description\": \"Заголовок модального окна Устранения неполадок\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"После скачивания отправьте этот файл на support@screenity.io вместе с любой актуальной информацией. Мы используем эти данные, чтобы помочь вам устранить любые проблемы с расширением и, при возможности, восстановить ваше видео.\",\n    \"description\": \"Описание модального окна Устранения неполадок\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Скачать\",\n    \"description\": \"Кнопка модального окна Устранения неполадок\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Видео слишком длинное для обработки на вашем устройстве\",\n    \"description\": \"Заголовок модального окна ограничения более 5 минут\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Поскольку Screenity полностью конфиденциален и работает локально, его возможности ограничены аппаратными ресурсами вашего устройства. Обработка видео такой длины займет слишком много времени и ресурсов. Но не волнуйтесь, вы всё равно можете скачать файл WEBM или оригинальную запись, но редактирование и экспорт в MP4 недоступны. Тем не менее, вы можете попробовать обработать видео, если осознаете риски.\",\n    \"description\": \"Описание модального окна ограничения более 5 минут\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Я понимаю риск, попробовать в любом случае\",\n    \"description\": \"Кнопка модального окна ограничения более 5 минут\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Узнать больше о обработке длинных видео.\",\n    \"description\": \"Ссылка «Узнать больше» в модальном окне ограничения более 5 минут с точкой\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Режим восстановления\",\n    \"description\": \"Заголовок режима восстановления\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Скачать данные для устранения проблем\",\n    \"description\": \"Опция скачивания для устранения проблем\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Центр помощи\",\n    \"description\": \"Элемент навигации помощи\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Запись аудио с страницы\",\n    \"description\": \"Заголовок предупреждения о звуке\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Для записи аудио с этой страницы вы должны использовать опцию '$tab$'.\",\n    \"description\": \"Описание предупреждения о звуке\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Область вкладки\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Расширение не поддерживается на странице\",\n    \"description\": \"Заголовок предупреждения о не поддерживаемом расширении на вкладке\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity не поддерживался на вашей предыдущей вкладке. Тем не менее, вы все равно можете начать запись здесь, а затем перейти на другую вкладку, но камера и панель инструментов не будут видны.\",\n    \"description\": \"Описание предупреждения о не поддерживаемом расширении на вкладке\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Настройка локальных резервных копий для записей в Screenity\",\n    \"description\": \"Заголовок резервных копий\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Сохраняйте копии всех ваших записей. Если вы не настроите резервные копии, ваши записи будут сохранены только при скачивании.\",\n    \"description\": \"Описание резервных копий\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Вам может потребоваться создать новую папку сначала, если вы пытаетесь сохранить записи в папках, таких как 'Загрузки' или других системных папках.\",\n    \"description\": \"Описание резервных копий\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Выбрать папку\",\n    \"description\": \"Кнопка Выбрать папку\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Не сейчас\",\n    \"description\": \"Кнопка Не сейчас\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Ваши записи локально синхронизированы\",\n    \"description\": \"Заголовок активированных резервных копий\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Для избежания запросов на разрешение при каждой записи рекомендуется оставить эту вкладку открытой.\",\n    \"description\": \"Описание активированных резервных копий\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Закрыть вкладку в любом случае\",\n    \"description\": \"Кнопка Закрыть вкладку в любом случае\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Остановить все резервные копии записей\",\n    \"description\": \"Кнопка Остановить резервные копии\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Пожалуйста, подтвердите доступ к вашей локальной папке резервных копий Screenity\",\n    \"description\": \"Заголовок подтверждения резервных копий\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Извините, но нам нужно ваше разрешение еще раз, чтобы продолжить создавать локальные копии ваших записей. К сожалению, нам придется спрашивать каждый раз, когда вы закроете эту вкладку, что может случиться, если вы закроете браузер или перезагрузите компьютер.\",\n    \"description\": \"Описание подтверждения резервных копий\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Разрешить Screenity продолжить создавать резервные копии записей\",\n    \"description\": \"Кнопка Разрешить в подтверждении резервных копий\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Создавать резервные копии записей\",\n    \"description\": \"Метка Переключить резервные копии\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Отказано в разрешении на создание резервных копий записи\",\n    \"description\": \"Заголовок отказа в разрешении на резервное копирование\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Вы не выбрали папку для создания локальных резервных копий записей. В зависимости от версии вашего браузера или операционной системы, вас могут попросить выбрать папку каждый раз, когда вы начнете запись. Вы можете попробовать снова или полностью отключить резервное копирование, если вы предпочтете не давать разрешения повторно.\",\n    \"description\": \"Описание отказа в разрешении на резервное копирование\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Запись аудио с компьютера\",\n    \"description\": \"Заголовок предупреждения о звуке для macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"В macOS вы можете записывать звук только с вкладок. Выберите опцию 'Вкладка Chrome' и убедитесь, что включена опция 'Также делиться аудио с вкладки'.\",\n    \"description\": \"Описание предупреждения о звуке для macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Запись аудио с компьютера\",\n    \"description\": \"Заголовок предупреждения о звуке для других систем\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Для записи звука с компьютера убедитесь, что включена опция 'Делиться аудио системы'. Если вы не видите эту опцию, попробуйте обновить Chrome или перейти на опцию записи вкладок.\",\n    \"description\": \"Описание предупреждения о звуке для других систем\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Максимальное разрешение\",\n    \"description\": \"Метка максимального разрешения в меню настроек\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Максимальное количество кадров в секунду\",\n    \"description\": \"Метка максимального количества кадров в секунду в меню настроек\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Недостаточно оперативной памяти\",\n    \"description\": \"Метка 'Недостаточно оперативной памяти' в меню настроек\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Изменить размер окна\",\n    \"description\": \"Метка изменения размера окна в меню настроек\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Экран слишком мал для этого разрешения\",\n    \"description\": \"Подсказка о слишком маленьком экране в меню настроек\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Невозможно записать в этом разрешении на вашем устройстве\",\n    \"description\": \"Подсказка о максимальном разрешении в меню настроек\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Просмотреть разрешения\",\n    \"description\": \"Кнопка просмотра разрешений\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Добавить ещё одну запись\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Готовим всё...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Подождите немного! Ваша запись готовится.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Панель инструментов скрыта. Включите её снова в настройках расширения.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Войти или зарегистрироваться\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Выйти\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Вы вышли из аккаунта\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Достигнут лимит хранилища\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Вы использовали все 50 ГБ хранилища. Удалите старые видео или сцены, чтобы продолжить запись.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Управление хранилищем\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Закрыть\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Не удалось проверить хранилище\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Произошла ошибка при проверке хранилища. Пожалуйста, войдите заново и попробуйте снова.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Не удалось проверить ваш лимит хранилища. Попробуйте снова через несколько секунд или обновите страницу.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Попробовать снова\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Сцена добавлена в Multi-запись\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Готовы записать новую сцену в видео\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Запись приближается к лимиту 90 минут, скоро завершится\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Запись остановлена из-за лимита 90 минут\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Запись вкладки отключена на этой странице. Переключено на запись экрана.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Запись видео отменена\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Ссылка скопирована в буфер\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Не удалось скопировать ссылку\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Новые\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Старые\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Все видео\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Видео не найдены\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Загрузка видео...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Перейти в панель\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Добро пожаловать в Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Без рекламы. Без слежки. Без ограничений.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Просто нажмите «Запись».\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Бесплатно — без регистрации!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Хотите больше возможностей для видео?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Облачное хранилище, делитесь ссылкой на видео\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Объединяйте клипы в единую историю\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Умные увеличения при клике (или добавьте свои!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Добавляйте шаблоны, подписи, макеты...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Пользовательские фоны и мокапы\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Открыть дополнительные функции\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Поддержите разработку от инди-разработчицы!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Настройки аккаунта\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Поддержка\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Ваша подписка Pro неактивна\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Ваша подписка Screenity Pro неактивна.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Пожалуйста, активируйте снова, чтобы продолжить использование.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Ваши видео и данные будут безвозвратно удалены \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Управление подпиской\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Хотите продолжить использовать бесплатную версию?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Выйти и перейти на бесплатную версию\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Вы вышли из аккаунта\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Чтобы сохранить записи в аккаунте и получить доступ к функциям Pro, войдите снова.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Войти снова\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Вы можете продолжать использовать расширение без аккаунта — но ваши записи не будут сохранены и не смогут быть восстановлены позже.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Продолжить без входа\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Мгновенный режим записи\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Мгновенное скачивание, но камеру и макет потом нельзя будет изменить.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Мгновенный режим записи\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Всё записывается в одно видео для мгновенной загрузки и публикации. Расположение камеры изменить будет нельзя, но другие правки возможны.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Поняла\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Запись вкладки отключена на этой странице\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Добавление в: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Завершить\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Хотите больше от своих записей?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Войдите, чтобы сохранить видео в облако, делиться ссылкой и использовать расширенное редактирование.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Войти, чтобы открыть платные функции\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Поддержите разработку от инди-разработчицы\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Открыть больше функций\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Войти, чтобы поделиться (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Доступ к облачному обмену, редактированию сцен, автоувеличению, подписям и другим функциям\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity всегда будет бесплатным, с открытым кодом и без рекламы. Pro покрывает облачные и серверные расходы и поддерживает инди-разработку! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Больше не показывать\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Попробовать\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Режим Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Записывайте несколько сцен — экран, камеру или оба варианта — одну за другой. Отлично подходит для нескольких дублей, смены ракурса или разбивки записи на части. Когда закончите, нажмите «Готово», чтобы открыть редактор со всеми сценами, объединёнными в одно видео.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Активируйте Pro, чтобы начать\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Оформите подписку, чтобы получить доступ к функциям Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Оформить подписку на Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Нравится пользоваться Screenity локально? Поддержите дальнейшую разработку!\",\n    \"description\": \"Заголовок баннера о самостоятельном хостинге в редакторе\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity разработана независимой разработчицей. Самостоятельный хостинг бесплатен, но если вам полезен этот инструмент, подумайте о поддержке через Pro ❤️\",\n    \"description\": \"Описание баннера о самостоятельном хостинге в редакторе\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"С возвращением в Screenity\",\n    \"description\": \"Заголовок приветственного окна для возвращающихся пользователей\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Новое: Хотите больше, чем просто запись?\",\n    \"description\": \"Заголовок для функции облачного хранилища в приветственном окне\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro — это новая дополнительная платформа для сохранения видео в облаке, обмена по ссылке и использования расширенных инструментов редактирования — если и когда они вам понадобятся.\",\n    \"description\": \"Описание функции облачного хранилища в приветственном окне\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Узнать больше о Screenity Pro\",\n    \"description\": \"Кнопка призыва к действию для функции облачного хранилища в приветственном окне\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Добро пожаловать в Screenity Pro\",\n    \"description\": \"Заголовок приветственного окна Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Вы готовы начать запись! Вот несколько моментов, которые стоит учитывать:\\n\\n- Пузырёк с камерой может исчезнуть во время записи — это нормально. Он записывается отдельно в фоновом режиме, чтобы вы могли позже разместить его где угодно.\\n- Увеличения автоматически создаются только при кликах внутри вкладок Chrome — это ограничение браузера. Вы всегда можете добавить дополнительные увеличения вручную после записи.\\n- Для быстрых записей с мгновенным скачиванием используйте Мгновенный режим. Учтите, что возможности редактирования в нём ограничены: нет фонов, макетов и продвинутых настроек.\",\n    \"description\": \"Описание приветственного окна Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Понятно\",\n    \"description\": \"Кнопка действия в приветственном окне Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 ошибка — выкл\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Добро пожаловать в расширение Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Пара быстрых советов перед записью.<br/>Автозум работает только по кликам внутри <strong>вкладок Chrome</strong>. Дополнительные зумы всегда можно добавить вручную позже.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Панель записи и эффекты\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Используйте эту панель для рисования, эффектов курсора, размытия и управления записью.<br/><br/><strong>Эта панель попадет в видео</strong>, если её не скрыть.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Камера записывается отдельно\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Во время записи камера может <strong>скрываться или переходить в PiP</strong> - это нормально.<br/><br/>Она записывается отдельно, чтобы вы могли настроить её положение позже.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Мгновенный режим\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Лучший вариант для быстрого обмена с <strong>безлимитными загрузками</strong>.<br/><br/>В этом режиме недоступны расширенные макеты и функции редактора.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Понятно\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Далее\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Назад\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Подробнее\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Сбросить онбординг\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Мало места на диске. Сохраняем запись. Всё, что было записано, в безопасности.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Длинная запись\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Запись завершена и в безопасности. Скачайте в формате WebM ниже.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Недоступно для длинных записей\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Недоступно в режиме восстановления\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Редактирование аудио слишком длинное\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Это редактирование аудио не поддерживается для клипов длиннее 15 минут. Оригинал не изменён. Попробуйте сначала обрезать клип.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Время редактирования истекло\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Редактирование не завершилось вовремя. Исходная запись не изменена. Повторите попытку или сначала обрежьте клип.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Демонстрация экрана прекращена. Ваша запись сохранена. Остановите, когда будете готовы.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Произошла ошибка обработки, но ваша запись в безопасности. Вы можете скачать её ниже.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Обработка редактирования. Исходная запись не изменена.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Не удалось применить редактирование\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Что-то пошло не так при обработке. Исходная запись в безопасности. Вы можете попробовать снова или скачать как есть.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Демонстрация экрана прекращена. Сохраняем запись. Всё, что было записано, в безопасности.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Аудио отключено. Сохраняем запись. Записанное видео в безопасности.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Запись сохранена. Редактор не открылся. Нажмите на значок Screenity, чтобы попробовать снова.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Сохранение занимает больше времени, чем ожидалось. Ваши данные в безопасности. Редактор скоро откроется.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Скопировать данные отладки\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Данные отладки скопированы\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Получить помощь\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Запись слишком короткая для сохранения\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Быстрая запись не удалась\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"Результат быстрой записи не удалось проверить на этом устройстве.\\nВы все равно можете скачать файл.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Скачать все равно\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Отмена\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/ta/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"ஸ்க்ரீனிட்டி - திரைப்பதிவு & குறிப்புகள்\",\n    \"description\": \"நீட்சிதம் பெயர்\"\n  },\n  \"extDesc\": {\n    \"message\": \"எந்த வரம்பின்னரும் இல்லாமாக உள்ள சிறந்த இலவச திரை படிக்குழு. படத்தை பிடிக்க, குறிப்பிடுக, அமை, மெதுவாக்கு, வீடியோக்களை திருத்துகின்றன மற்றும் மேலும் - புகுபதிக்க அந்த பேர் பதிவு செய்ய இல்லை, மற்றும் தனியுரிமை பருவமாக இருக்கின்றது.\",\n    \"description\": \"நீட்சிதம் விளக்கம்\"\n  },\n  \"recordTab\": {\n    \"message\": \"பதிவு\",\n    \"description\": \"பதிவு தலைப்பு படிதம்\"\n  },\n  \"videosTab\": {\n    \"message\": \"உங்கள் வீடியோக்கள்\",\n    \"description\": \"வீடியோக்கள் தலைப்பு படிதம்\"\n  },\n  \"screenType\": {\n    \"message\": \"திரை\",\n    \"description\": \"திரை படம் பதிவு வகை படிதம்\"\n  },\n  \"tabType\": {\n    \"message\": \"கையெழுத்து பகுதி\",\n    \"description\": \"கையெழுத்து பகுதி படம் பதிவு வகை படிதம்\"\n  },\n  \"cameraType\": {\n    \"message\": \"கேமரா\",\n    \"description\": \"கேமரா பதிவு வகை படிதம்\"\n  },\n  \"mockupType\": {\n    \"message\": \"மாக்கப்\",\n    \"description\": \"மாக்கப் பதிவு வகை படிதம்\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"தனிப்பட்ட பகுதி பதிவு முடக்கப்பட்டுள்ளது\",\n    \"description\": \"கூடித் தனிப்பட்ட பகுதி பதிவு முடக்கப்பட்டுள்ளது எச்சரிக்கை படம் (Chrome பதிப்பு 104 க்கு குறைந்த பதிப்புகள்)\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Chrome பதிப்பு 110+ க்கு புதுப்பிக்கவும்\",\n    \"description\": \"கூடித் தனிப்பட்ட பகுதி பதிவு முடக்கப்பட்டுள்ளது எச்சரிக்கை விளக்கம் (Chrome பதிப்பு 104 க்கு குறைந்த பதிப்புகள்)\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"புதுப்பிக்க\",\n    \"description\": \"104 க்கு குறுகிய க்ரோம் பதிப்புகளுக்கு மேம்படுத்த மாட்சியை முடக்கிய பிரிவு பதிப்பு எச்சரிக்கை செய்தியாகும்\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"உங்கள் அனுமதிகளை சரிபார்க்கவும்\",\n    \"description\": \"கேமரா / மைக்ரோஃபோன் அனுமதிகள் எச்சரிக்கை செய்தியின் தலைப்பு\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"போன்று போன்று தேர்வு செய்வதற்கு உங்கள் கேமரா அல்லது மைக்ரோஃபோனுக்கு அணுகலை அனுமதிக்க முடியவில்லை. அணுகலை அனுமதிக்க அனுமதி அளிக்க அடியில் கேமரா ஐகானை கிளிக் செய்து அனுமதி அளிக்கவும்.\",\n    \"description\": \"கேமரா / மைக்ரோஃபோன் அனுமதிகள் எச்சரிக்கை செய்தியின் விளக்கம் விளக்கம்\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"விலகு\",\n    \"description\": \"கேமரா / மைக்ரோஃபோன் அனுமதிகள் எச்சரிக்கை செய்தியின் விளக்கம் விளக்கம்\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"கேமரா அணுகலை அனுமதிக்கவும்\",\n    \"description\": \"கேமரா அணுகலை அனுமதிக்க பொத்தான்\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"மைக்ரோஃபோன் அணுகலை அனுமதிக்கவும்\",\n    \"description\": \"மைக்ரோஃபோன் அணுகலை அனுமதிக்க பொத்தான்\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"அழுக்கு\",\n    \"description\": \"அழுக்கு ஸ்விட்ச் பொத்தான்\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"புதுப்பிக்க பருவத்தை அமைக்கவும்\",\n    \"description\": \"புதுப்பிக்க ஸ்விட்ச் பொத்தான்\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"கேமரா தளம்\",\n    \"description\": \"கேமரா தளம் ஸ்விட்ச் பொத்தான்\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"பின்னணி விளைவுகள்\",\n    \"description\": \"பின்னணி விளைவுகள் ஸ்விட்ச் பொத்தான்\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"பதிவு செய்ய ஆரம்பிக்கவும்\",\n    \"description\": \"பதிவு பொத்தான் பொத்தான்\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"பதிவு ஆரம்பிக்கின்றது...\",\n    \"description\": \"பதிவு பொத்தான் செயலி ஆரம்பிக்கின்றது செய்தி\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"கேமரா அமைக்கவும்\",\n    \"description\": \"கேமரா முறையில் உள்ளபடியாக கேமரா தேர்வு செய்தால் உள்ளபடி கேமரா செய்தி\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"மேலும் விருப்பங்கள் காட்டு\",\n    \"description\": \"மேலும் விருப்பங்கள் செய்தி\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"அமைப்பு ஒலியை சேர்க்கவும்\",\n    \"description\": \"அமைப்பு ஒலி குறியீடு/அறை\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"கட்டுப்பாடு மறை\",\n    \"description\": \"கட்டுப்பாடு மறை செய்தி\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"எண்ணிக்கை செய்யவும்\",\n    \"description\": \"எண்ணிக்கை செய்தி\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"நேரம் எதிர்க்காட்டவும்\",\n    \"description\": \"அலாரம் செய்தி\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"மறை\",\n    \"description\": \"பிரிவு பிரிப்பு செய்தி\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"இல்லை\",\n    \"description\": \"சாதனம் தேர்வு செய்தி\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"கேமரா இல்லை\",\n    \"description\": \"கேமரா தேர்வு செய்தி\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"மைக்ரோஃபோன் இல்லை\",\n    \"description\": \"மைக்ரோஃபோன் தேர்வு செய்தி\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"ஒரு மூலம் தேர்வு செய்யவும்\",\n    \"description\": \"மூலம் தேர்வு செய்தி உள்ளிடம்\"\n  },\n  \"offLabel\": {\n    \"message\": \"அணுக்கம் இல்லை\",\n    \"description\": \"அணுக்கம் இல்லை செய்தி கீழே\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"அகலம்\",\n    \"description\": \"பிரிவு அகல செய்தி\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"உயரம்\",\n    \"description\": \"பிரிவு உயர செய்தி\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"படம் வைக்க கிளிக் செய்யவும்\",\n    \"description\": \"புதிய படம் சேர்க்கும் போப்அப் தலைப்பு\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"பதிவு முடிக்க\",\n    \"description\": \"பதிவு முடிக்க குழப்பு\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"பதிவு மீட்டல்\",\n    \"description\": \"பதிவு மீட்டல் குழப்பு\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"பதிவு முடிக்க\",\n    \"description\": \"பதிவு முடிக்க குழப்பு\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"பதிவு தொடங்க\",\n    \"description\": \"பதிவு தொடங்க குழப்பு\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"பதிவு ரத்து செய்\",\n    \"description\": \"பதிவு ரத்து செய் உத்திபத்தி\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"வரையறு கருவிகள் சோதனை செய்யுங்கள்\",\n    \"description\": \"வரையறு கருவிகள் சோதனை செய் உத்திபத்தி\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"சுரூரிட கருவியை மாற்று\",\n    \"description\": \"சுரூரிட கருவிகள் சோதனை செய் உத்திபத்தி\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"கோசர் விருப்பங்களை மாற்று\",\n    \"description\": \"கோசர் விருப்பங்களை மாற்று உத்திபத்தி\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"கேமரா முடக்கு\",\n    \"description\": \"கேமரா முடக்கு உத்திபத்தி\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"கேமரா இயக்கு\",\n    \"description\": \"கேமரா இயக்கு உத்திபத்தி\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"கேமரா அனுமதிகள் இல்லை\",\n    \"description\": \"கேமரா அனுமதிகள் இல்லை உத்திபத்தி\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"மைக்ரோஃபோன் முடக்கு\",\n    \"description\": \"மைக்ரோஃபோன் முடக்கு உத்திபத்தி\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"மைக்ரோஃபோன் இயக்கு\",\n    \"description\": \"மைக்ரோஃபோன் இயக்கு உத்திபத்தி\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"மைக்ரோஃபோன் அனுமதிகள் இல்லை\",\n    \"description\": \"மைக்ரோஃபோன் அனுமதிகள் இல்லை உத்திபத்தி\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"கருவியை தேர்ந்தெடு\",\n    \"description\": \"கருவி உத்திபத்தி\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"பேன் கருவி\",\n    \"description\": \"பேன் கருவி உத்திபத்தி\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"முக்கியமான கருவி\",\n    \"description\": \"முக்கியமான கருவி உத்திபத்தி\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"மொழி கருவி\",\n    \"description\": \"மொழி கருவி உத்திபத்தி\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"உரை கருவி\",\n    \"description\": \"உரை கருவி உத்திபத்தி\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"வடிவ கருவி\",\n    \"description\": \"வடிவ கருவி உத்திபத்தி\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"அம்பு கருவி\",\n    \"description\": \"அம்பு கருவி உத்திபத்தி\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"படம் கருவி\",\n    \"description\": \"படம் கருவி உத்திபத்தி\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"மீட்டமை\",\n    \"description\": \"மீட்டமை குருவி\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"மீட்டமை செய்\",\n    \"description\": \"மீட்டமை செய் குருவி\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"கவசம் அழி\",\n    \"description\": \"கவச டூல்டிப்\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"மேலும் வண்ணங்கள்\",\n    \"description\": \"மேலும் வண்ணங்கள் டூல்டிப்\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"படிக்குழப்பு அச்சு\",\n    \"description\": \"படிக்குழப்பு அச்சு டூல்டிப்\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"இடைச்சுழப்பு அச்சு\",\n    \"description\": \"இடைச்சுழப்பு அச்சு டூல்டிப்\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"இலேமெண்ட் அச்சு\",\n    \"description\": \"இலேமெண்ட் அச்சு டூல்டிப்\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"புகைப்படத்தில் புகைப்பட\",\n    \"description\": \"புகைப்படத்தில் புகைப்பட டூல்டிப்\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"நிரப்பு செய்\",\n    \"description\": \"நிரப்பு செய் டூல்டிப்\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"வரைபட முடிந்தது\",\n    \"description\": \"வரைபட முடிந்தது டோஸ்ட்\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"மயிர் முடிந்தது\",\n    \"description\": \"மயிர் முடிந்தது டோஸ்ட்\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"அனைத்து மூடிய உருக்கங்களையும் அகற்று\",\n    \"description\": \"அனைத்து மூடிய உருக்கங்களையும் அகற்று டூல்டிப்\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"கிளிக்குகளை குறிக்க\",\n    \"description\": \"கிளிக்குகளை குறிக்க டூல்டிப்\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"கோர்சரை குறிக்க\",\n    \"description\": \"கோர்சரை குறிக்க டூல்டிப்\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"கோர்சரை ஒளிப்படுத்து\",\n    \"description\": \"கோர்சரை ஒளிப்படுத்து டூல்டிப்\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"மீது காண்பிக்க வேண்டாம்\",\n    \"description\": \"அனுமதிகள் மோடால் காண்பிக்க பொதுப்படுத்து பொத்தான்\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"புதியதாக பதிவுசெய்ய விரும்புகிறீர்களா?\",\n    \"description\": \"புதியதாக பதிவுசெய்ய விரும்புகிறீர்களா மோடால் தலைப்பு\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"உங்கள் தற்போதைய வீடியோ முன்னேற்றம் இழக்கப்படும்.\",\n    \"description\": \"மீள் அமைப்பு மாடல் விளக்கம்\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"வீடியோ மீள் அமைப்பு\",\n    \"description\": \"மீள் அமைப்பு மாடல் மீள் அமைப்பு பொத்தான்\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"வீடியோ செயலாக்கம் தொடர்க\",\n    \"description\": \"மீள் அமைப்பு மாடல் தொடர்க பொத்தான்\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"நீங்கள் வீடியோ செயலாக்கியில் துணைமைக்கப்படுவீர்களா?\",\n    \"description\": \"வீடியோ விலக்க மாடல் தலைப்பு\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"உங்கள் தற்போதைய வீடியோ முன்னேற்றம் இழக்கப்படும்.\",\n    \"description\": \"வீடியோ விலக்க மாடல் விளக்கம்\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"வீடியோ விலக்கு\",\n    \"description\": \"வீடியோ விலக்க மாடல் விலக்கு பொத்தான்\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"வீடியோ செயலாக்கம் தொடர்க\",\n    \"description\": \"வீடியோ விலக்க மாடல் தொடர்க பொத்தான்\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"நீங்கள் எந்தவையை பதிவு செய்ய விரும்புகின்றீர்கள் என்று தேர்ந்தெடுக்கவும்\",\n    \"description\": \"பதிவு பக்கத்தில் தலைப்பு\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"பதிவுசெய்தல்...\",\n    \"description\": \"பதிவு பக்கத்தில் பதிவுசெய்தல் போன்ற தலைப்பு\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"இந்த உதவியை மூட வேண்டும். உங்கள் பதிவு சேமிக்கப்பட்ட பிறகு அந்த உதவியும் மூடப்படும்.\",\n    \"description\": \"பதிவு பக்கத்தில் விளக்கம்\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"பதிவுசெய்தல் தயாராகிக் கொள்ளப்படுகின்றது...\",\n    \"description\": \"பதிவு பக்கத்தில் பதிவுசெய்தல் / சேமிக்குகின்ற போன்ற தலைப்பு\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"இந்த உதவியை மூட வேண்டும். அது உங்கள் பதிவு சேமிக்கப்பட்ட பிறகு அப்டேட் செய்யும்.\",\n    \"description\": \"பதிவு பக்கத்தில் விளக்கம்\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"எடிட்டர்\",\n    \"description\": \"மையத்தின் வழிமுறைப்பு பக்கத்தின் தலைப்பு\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"ரத்து செய்\",\n    \"description\": \"ரத்து செய்க பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"மூல செய்தி திரும்பு\",\n    \"description\": \"மூல செய்தி திரும்பு பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"மீட்டமை\",\n    \"description\": \"மீட்டமை பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"மாற்றங்கள் சேமிக்க\",\n    \"description\": \"சேமிப்பு பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"சேமிப்பதில்...\",\n    \"description\": \"சேமிப்பு பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"உங்கள் ஒலியின் கோப்பை விளக்கம் செய்யவும்\",\n    \"description\": \"ஒலி கோப்பை விளக்கம் செய்யவும் பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"அல்லது பார்க்க கிளிக் செய்யவும்\",\n    \"description\": \"ஒலி கோப்பை பார்க்க கிளிக் செய்யவும் பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"அமைப்புகள்\",\n    \"description\": \"ஒலி அமைப்புகள் தலைப்பு பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"ஒலி அளவு\",\n    \"description\": \"ஒலி அளவு விளக்கம் பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"புதுப்பி\",\n    \"description\": \"ஒலி புதுப்பி பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"வெட்டு\",\n    \"description\": \"வெட்டு தலைப்பு பொத்தான் முகத்தில் உள்ள பொத்தான்\"\n  },\n  \"widthLabel\": {\n    \"message\": \"பருவம்\",\n    \"description\": \"சொத்துக்கள் குறிப்புக்கு பருவம் விளக்கம்\"\n  },\n  \"heightLabel\": {\n    \"message\": \"உயரம்\",\n    \"description\": \"சொத்துக்கள் குறிப்புக்கு உயரம் விளக்கம்\"\n  },\n  \"leftLabel\": {\n    \"message\": \"இடம்\",\n    \"description\": \"குறிப்பிட்ட பணியாளர் விவரங்கள் சிட்டில் இடம் குறிப்பு\"\n  },\n  \"topLabel\": {\n    \"message\": \"மேல்\",\n    \"description\": \"குறிப்பிட்ட பணியாளர் விவரங்கள் சிட்டில் மேல் குறிப்பு\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"ஹாண்டிளை இழுக்கவும் மற்றும் தொகுப்புகளை பயன்படுத்த இடது பட்டன்களை பயன்படுத்தவும்.\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் வித்திக்குது பற்றிய விவரங்கள்\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"வீடியோ குறை\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் குறை பட்டன்\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"குறைக்கின்றது...\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் குறை பட்டன் வித்திக்கின்றது போன்ற போது\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"பிளவு வைத்துப்படு\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் கட் பட்டன்\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"கட்டுகின்றது...\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் கட் பட்டன் வித்திக்கின்றது போன்ற போது\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"ஒலியை முடக்கு\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் ஒலி முடக்கு பட்டன்\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"ஒலி முடக்குகின்றது...\",\n    \"description\": \"சாண்ட்பாக்ஸ் எடிட்டிங்கில் ஒலி முடக்கு பட்டன் வித்திக்கின்றது போன்ற போது\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"அதிகம் அறிய\",\n    \"description\": \"ஒரு டாட் சொல் சார்பு வெளியீடு செய்யும்\"\n  },\n  \"undoLabel\": {\n    \"message\": \"மீட்டமை\",\n    \"description\": \"மீட்டமை செய்\"\n  },\n  \"redoLabel\": {\n    \"message\": \"மீட்டமை செய்\",\n    \"description\": \"மீட்டமை செய் செய்\"\n  },\n  \"leaveReview\": {\n    \"message\": \"ஒரு மதிப்பீடு விளக்க\",\n    \"description\": \"விமர்சனம் விடுங்கள் பட்டன்\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"புதுப்பிப்புக்காக பின்னுங்கள்\",\n    \"description\": \"புதுப்பிப்புக்காக பின்னுங்கள் பட்டன்\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"நீங்கள் தற்போது ஆஃப்லைன்\",\n    \"description\": \"ஆஃப்லைன் லேபிள்\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"சில அம்சங்கள் மீண்டும் இணைக்கப்படும் வரை கிடைக்காது\",\n    \"description\": \"ஆஃப்லைன் லேபிள் விளக்கம்\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"மீண்டும் முயற்சி செய்\",\n    \"description\": \"ஆஃப்லைன் லேபிள் மீண்டும் முயற்சி செய் பொத்தான்\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"கூகுள் புதுப்பிக்க வேண்டும்\",\n    \"description\": \"கைப்பு கூகுள் புதுப்பிப்பு லேபிள்\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"கூகுள் கைப்பு கூட்டுத்தளம் அணுக புதுப்பிக்கவும்\",\n    \"description\": \"கைப்பு கூகுள் புதுப்பிப்பு லேபிள் விளக்கம்\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"மேலும் அறிய\",\n    \"description\": \"மேலும் அறிய பொத்தான்\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"உங்கள் சாதனத்தில் வீடியோ மிகம் நீளம்\",\n    \"description\": \"எடிட்டரில் 5 நிமிடத்திற்குக் கீழ் வரும் கட்டுரை\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"திருத்தல் மற்றும் MP4 வடிவம் விரும்பில்லை\",\n    \"description\": \"எடிட்டரில் 5 நிமிடத்திற்குக் கீழ் வரும் விளக்கம்\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"வீடியோ செயலாக்கப்படுகின்றது...\",\n    \"description\": \"வீடியோ செயலாக்க லேபிள் இன் பதிவு\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"விரிவாக்க மற்றும் விருத்திப் பதிவு செய்வதற்கு மேல் புதுப்பிப்பு செய்யவும்\",\n    \"description\": \"வீடியோ செயலாக்க விளக்கம் இன் பதிவு\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"திருத்து\",\n    \"description\": \"மின்னணு திருத்து பக்கத்தில் திருத்த தலைப்பு\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"இணைப்பு இல்லை\",\n    \"description\": \"இணைப்பு லேபிள்\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"கிடைக்காது\",\n    \"description\": \"கிடைக்காது லேபிள்\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"வீடியோ திருத்து\",\n    \"description\": \"குறைத்து திருத்த பொத்தான்\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"வீடியோவை குறைத்து, வெட்டுக்கள் அல்லது பாரம் செய்யுங்கள்\",\n    \"description\": \"குறைத்து பொத்தான்\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"தயாராகிறது...\",\n    \"description\": \"தயாரிக்கின்றது லேபிள்\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"வீடியோவை வெட்டு\",\n    \"description\": \"வெட்டு பொத்தான் பொத்தான்\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"வீடியோவை வெட்டுக்கும் மற்றும் அளவை அமைக்கவும்\",\n    \"description\": \"வெட்டு பொத்தான்\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"ஒலியை சேர்\",\n    \"description\": \"ஒலி பொத்தான் பொத்தான்\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"உங்கள் சொந்த ஒலியை வீடியோக்கு சேர்க்க பொத்தான்\",\n    \"description\": \"ஒலி பொத்தான்\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"சேமிக்க\",\n    \"description\": \"மின்னணு திருத்து பக்கத்தில் சேமிக்க தலைப்பு\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"டிரைவை விடு\",\n    \"description\": \"கூகுள் டிரைவ் பெயர் விடுக\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"சேமிக்கப்படுகின்றது...\",\n    \"description\": \"கூகுள் டிரைவ் கீபேழ்க\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"டிரைவ் கீபேழ்க\",\n    \"description\": \"கூகுள் டிரைவ் பொத்தான் பொத்தான்\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"தற்போதைய பதிப்பை கூகுள் டிரைவ் கீபேழ்க\",\n    \"description\": \"கூகுள் டிரைவ் பொத்தான்\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"உள்நுழைக்கி டிரைவ் கீபேழ்க\",\n    \"description\": \"கூகுள் டிரைவ் பெயர் விடுக\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"ஏற்று\",\n    \"description\": \"மின்னணு திருத்து பக்கத்தில் ஏற்ற தலைப்பு\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"பதிவிறக்குதல்...\",\n    \"description\": \"பதிவிறக்குதல் லேபிள்\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \".webm ஆகப் பதிவிறக்க\",\n    \"description\": \"WEBM பொத்தான் பொத்தான்\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \".webm வீடியோவை ஏற்று\",\n    \"description\": \"WEBM பொத்தான்\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \".mp4 ஆகப் பதிவிறக்க\",\n    \"description\": \"MP4 பொத்தான் பொத்தான்\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \".mp4 வீடியோ ஏற்றுக் கொள்ள (பரிந்துரைக்கப்படுகின்றது)\",\n    \"description\": \".mp4 பொத்தான் பொத்தான்\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \".gif ஆகப் பதிவிறக்க\",\n    \"description\": \"GIF பொத்தான் பொத்தான்\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"GIF ஐ ஏற்று (அதிகம் 30 வினாடிகள்)\",\n    \"description\": \"GIF பொத்தான்\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"உங்கள் பதிவு தரத்தை உயர்த்த தயாரா?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"க்ளவுட் பகிர்வு, பூம், டெம்ப்ளேட்டுகள்... மற்றும் தனியொரு டெவலப்பரால் உருவாக்கப்பட்ட, தனியுரிமையை மதிக்கும் டூலை ஆதரிக்கவும் ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Pro பற்றி மேலும் அறிக\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"ஏற்கனவே கணக்கு உள்ளதா? உள்நுழைக\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"வீடியோ பகிர்\",\n    \"description\": \"மின்னணு திருத்தி மின்னணு பெயர் மின்னணு பெயர்\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"தற்போதைய ஒலியை மாற்றுக\",\n    \"description\": \"தொகுப்பாளி இல் ஒலி பெயர்\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"கோர்சரை முடக்கு\",\n    \"description\": \"புள்ளி புள்ளி பெயர் பொத்தான்\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"பதிவு செய்யும் பேஜில் உள்ளே இரு\",\n    \"description\": \"பக்க படுகின்றது பப்ப்\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"மைக்ரோஃபோன் அணை எச்சரிக்கை\",\n    \"description\": \"மைக் அனுக்கிரிப் பப்\"\n  },\n  \"helpPopup\": {\n    \"message\": \"உத்தமம்\",\n    \"description\": \"உத்தமம் பப் பொத்தான்\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"பதிவு நிறுத்தப்பட்டது. மீடியோ தொடர இயக்க புத்தரம் அழுக்கவும்.\",\n    \"description\": \"தற்போது இயக்கப்படுகின்ற விசை பட்டியல் தற்போது இயக்கப்படுகின்றது\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity உங்கள் திரையை பதிவு செய்ய அனுமதியில்லை\",\n    \"description\": \"அனுமதி பட்டியல் தலைப்பு\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"திரை பதிவு செய்ய உங்கள் திரையை காட்சிப்படுத்த அனுமதி இயக்க வேண்டும். போக்கில் உங்கள் திரையை பட்டியல் தலைப்பில் அனுமதி இயக்கவும்.\",\n    \"description\": \"அனுமதி பட்டியல் விளக்கம்\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"திரை பதிவை இயக்கு\",\n    \"description\": \"அனுமதி பட்டியல் செயல்\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"ரத்து செய்\",\n    \"description\": \"அனுமதி பட்டியல் ரத்து\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"உங்கள் மைக்ரோஃபோன் அமைக்கப்பட்டுள்ளது\",\n    \"description\": \"மைக்ரோஃபோன் அமைக்கப்பட்டுள்ள தலைப்பு\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"உங்கள் பதிவுக்கு ஒலி சேர்க்க உங்கள் மைக்ரோஃபோன் அமைக்க வேண்டும். ஒலி இல்லாதிருந்தால் தொடர்வது விரும்புகின்றீர்களா?\",\n    \"description\": \"மைக்ரோஃபோன் அமைக்கப்பட்டுள்ள விளக்கம்\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"ஆம், தொடர\",\n    \"description\": \"மைக்ரோஃபோன் அமைக்கப்பட்டுள்ள விளக்கம் தொடர்வது\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"ரத்து செய்\",\n    \"description\": \"மைக்ரோஃபோன் அமைக்கப்பட்டுள்ள விளக்கம் ரத்து\"\n  },\n  \"setupTitle\": {\n    \"message\": \"மூன்று எளிய படிகளில் ஸ்கிரீனிடி பயன்படுத்த ஆரம்பிக்கவும்:\",\n    \"description\": \"அமைதிகள் தலைப்பு\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- கிளிக் செய்யவும் \",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 1, ஐகானுக்கு முந்திர விசை தேவை.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" நீக்குதல் ஐகான்\",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 1, ஐகானுக்கு பின்னர் விசை தேவை.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- அழுத்தவும் \",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 2, ஐகானுக்கு முந்திர விசை தேவை.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" அடைவு ஐகான்\",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 2, ஐகானுக்கு பின்னர் விசை தேவை.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- கிளிக் செய்யவும் \",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 3, ஐகானுக்கு முந்திர விசை தேவை.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity ஐகானை தொடர்க\",\n    \"description\": \"அமைதிப் படிகள் பட்டியல் 3, ஐகானுக்கு பின்னர் விசை தேவை.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"அற்புதம்! உங்கள் அமைப்பு அனைத்து அமைப்புகளும் அமைந்துவிட்டன\",\n    \"description\": \"அமைதிப் படிகள் படிகள் பட்டியல் தலைப்பு\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"நீங்கள் இப்போது இங்கே அல்லது வேறு பக்கத்தில் பதிவு செய்ய முடியும்.\",\n    \"description\": \"அமைதிப் படிகள் படிகள் பட்டியல் விளக்கம்\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"எங்கேயும் வரையுங்கள்\",\n    \"description\": \"இங்கே செய்ய வரையுங்கள்\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"சமாதானமாக. எங்கேயும் கிளிக் செய்யவும் எண்ணிக்கொள்ள முடியும்.\",\n    \"description\": \"எண்ணிக்கொள்ள செய்தி\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"பட்டியல்கள் படிகளை திருத்த நிரப்பாது கூடிய கால இடங்களுக்காக வடிவங்கள் இடைவெளிக்காட்டப்படும் காரணமாக, திருத்தங்கள் தவறாக இருக்கலாம்.\",\n    \"description\": \"எடிட்டர் சிறியப் படிகள் படிக்கும் தகவல்\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"வீடியோ செயலாக்கப்படுகின்றது, விளக்கத்துக்குச் சிறந்த அல்லது அடுத்துப் பார்க்கப்படும் என்று புதியது சொல்கின்றது.\",\n    \"description\": \"எடிட்டரில் செயலாக்கப்படும் அற்புத விளக்கம்\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"வெட்டுத்ததம் சிறிய நேரம் ஆகலாம்\",\n    \"description\": \"வெட்டுத்தம் தகவல் தலைப்பு\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"விரைவாக செயலாக்க முடியும் என்று மேம்படுகின்றது\",\n    \"description\": \"வெட்டுத்தம் தகவல் விளக்கம்\"\n  },\n  \"micOnToast\": {\n    \"message\": \"மைக்ரோஃபோன் இயக்கப்பட்டுள்ளது\",\n    \"description\": \"மைக்ரோஃபோன் இயக்கப்பட்டுள்ளது விளக்க தலைப்பு\"\n  },\n  \"micOffToast\": {\n    \"message\": \"மைக்ரோஃபோன் முடக்கப்பட்டுள்ளது\",\n    \"description\": \"மைக்ரோஃபோன் முடக்கப்பட்டுள்ளது விளக்க தலைப்பு\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"UI அறிவிப்புகளை மறை\",\n    \"description\": \"UI அறிவிப்புகளை மறைக்க பொது பொது\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"மீண்டும் காட்ட விரும்பாதே\",\n    \"description\": \"மீண்டும் காட்ட விரும்பாதே பொது\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"பயன்படுத்தப்பட்ட போக்குகை மீட்டை மறை\",\n    \"description\": \"ஹோவர் பொத்தாக்கல் முக்கியம் பொது பொது\"\n  },\n  \"stopRecording\": {\n    \"message\": \"பதிவு முடிக்க\",\n    \"description\": \"பதிவு முடிக்க பொது பொது\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"புதிய Screenity க்கு வரவேற்கின்றது!\",\n    \"description\": \"அந்தப் பயனாளர்களுக்காக புதியது அற்புதம் தலைப்பு\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"நாங்கள் புதிய பார்வையும் அந்தப் பயனாளர்களுக்கு அற்புதமான அமைப்பை அறிமுகப்படுத்துகின்றோம், ஆனால் சிறுபக்கங்கள் அனைத்து இலவச, தனியுரிமையுடன் மூன்று விளம்பரச் செயல்பாடுகளாக இல்லை என்றாக, அதே கூட நீங்கள் அறிந்து கொள்ளும் மற்றும் காத்திருப்பதே அந்த படிவிக்கு மீதம்.\",\n    \"description\": \"அப்டேட் அறிவிப்பு விளக்கம் ஏற்படும் பயனாளர்களுக்கு\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"அப்டேட் பற்றி மேலும் அறிந்து கொள்ளவும்.\",\n    \"description\": \"அப்டேட் அறிவிப்பு மேலும் அறிந்து கொள்ள பட்டன் பயனாளர்களுக்கு\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"ஆரம்பிக்கவும்\",\n    \"description\": \"அப்டேட் அறிவிப்பு பட்டன் பயனாளர்களுக்கு\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"பதிவு ஆரம்பிப்பதில் பிழை\",\n    \"description\": \"ஸ்ட்ரீம் பிழை மாடல் தலைப்பு\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"பதிவு ஆரம்பிப்பதில் ஒரு பிழை ஏற்பட்டதேன் என்று பார்க்கின்றது. உங்கள் உலாவி மீளவும் முயலுக்காக மீளவும் முயலவும் முயலுக்கு மீள்வார்கள். சிந்தனை தொடர்ந்து உத்தரவு பெறுகின்றதில், தயவுசெய்து எங்களுடன் தொடர்க support@screenity.io என்ற முகவரிக்கு தொடர்பு கொள்ளவும்.\",\n    \"description\": \"ஸ்ட்ரீம் பிழை மாடல் விளக்கம்\"\n  },\n  \"highestQuality\": {\n    \"message\": \"அதிசய உயர தரம்\",\n    \"description\": \"பாப் பப் வரிசையில் அதிசய உயர தரம் லேபிள்\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"கடைசி பதிவு மீட்டமை\",\n    \"description\": \"பாப் பப் வரிசையில் கடைசி பதிவு மீட்டமை பட்டன்\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"பிரச்சினைகள் உள்ளனவா?\",\n    \"description\": \"எடிட்டரில் பிரச்சினைகள் பொத்தான்\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"உங்கள் ரெக்கார்டிங் காணாதுவா?\",\n    \"description\": \"எடிட்டரில் பிரச்சினை படிக்கு தலைப்பு\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"உங்கள் ரெக்கார்டிங் முடிந்துவிட்டது மற்றும் உங்கள் வீடியோவை காணாத அவகாச வீடியோ தரவை பெற்ற பிறகு, அது கிடைக்கின்றால் உங்கள் அசையப்பட்ட வீடியோ தரவை பதிவிறக்க முயற்சிக்கலாம். அந்த தரவு கிடைக்கின்றதில், பிழை அறிக்கை அனுப்பி அழைக்கலாம்.\",\n    \"description\": \"எடிட்டரில் பிரச்சினை படிவம்\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"அவகாச வீடியோ தரவை பதிவிறக்குங்கள்\",\n    \"description\": \"எடிட்டரில் பிரச்சினை படிக்கு பொத்தான்\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"பிழை அறிக்கை செய்க\",\n    \"description\": \"எடிட்டரில் பிரச்சினை படிக்கு இரண்டாவது பொத்தான்\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"ஒரு பதிவுக்கு காணப்பட்டதில்லை, மன்னிக்கவும் :(\",\n    \"description\": \"எடிட்டரில் ஒரு பதிவுக்கு காணப்பட்டதில்லை எச்சரிக்கை\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"நினைவர வரம்பு அடைந்துள்ளது\",\n    \"description\": \"நினைவர வரம்பு அடைந்துள்ளது தலைப்பு\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"உங்கள் சாதனம் பதிவு செய்ய உள்ள நினைவரம் முழுவதும் அவசியமாக அடைந்துள்ளது, அதனால் பதிவு தானியக்கம் நிறுத்தப்பட்டுள்ளது. கூடுதல் காலம் பதிவு செய்ய விரும்பினால், வீடியோ தரம் குறைக்க அல்லது உங்கள் சாதனத்தில் சில இடம் வெளியிட முடியும் என்பதை முயற்சிக்கலாம்.\",\n    \"description\": \"நினைவர வரம்பு அடைந்துள்ளது விளக்கம்\"\n  },\n  \"understoodButton\": {\n    \"message\": \"புரிந்தேன்\",\n    \"description\": \"புரிந்தேன் பட்டன்\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"புதியவில் போக்கு இல்லை\",\n    \"description\": \"புதியவில் போக்கு இல்லை தலைப்பு\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity பதிவு செய்ய குண்டெய்க குண்டு முதல் 1 ஜிபி குளையிடம் தேவை. உங்கள் சாதனத்தில் இடம் வெளியிடும்போது, தயவுசெய்து இடம் வெளியிட்டு மீட்டெடுக்கவும்.\",\n    \"description\": \"புதியவில் போக்கு இல்லை விளக்கம்\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"முந்தைய பதிவுகளை நீக்கு\",\n    \"description\": \"முந்தைய பதிவுகளை நீக்கு பட்டன்\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"உயர்தரம்\",\n    \"description\": \"பராமரிப்பு எடிட்டர் பகுதி பரம்பரையில்\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"மூல வீடியோ கோப்புகளை பதிவிறக்கம் செய்க\",\n    \"description\": \"மூல பதிவிறக்கம் பட்டன்\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"உயர்நிலை பதிவிறக்க தரவை வழங்குக\",\n    \"description\": \"மூல பதிவிறக்கம் லேபிள்\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"உங்கள் பதிவிறக்கத்தில் உதவியை பெறுங்கள்\",\n    \"description\": \"பிரச்சினைக்கு பெறுவதற்கான பட்டன்\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"உங்கள் வீடியோவை மீட்டரியாக்குகின்றது மற்றும் பிற சிக்கல்களை தீர்க்க\",\n    \"description\": \"பிரச்சினைக்கு பெறுவதற்கான லேபிள்\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"மூல வீடியோ கோப்புகளை பதிவிறக்கம் செய்க\",\n    \"description\": \"மூல பதிவிறக்கம் மாடல் தலைப்பு\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"மூல வீடியோ கோப்பு, ஸ்க்ரீனிட்டி மூலம் பதிக்கப்பட்ட உண்மை வீடியோ கோப்புக்கு அமைந்தது. இந்த வீடியோ செயலாக்கப்படாதது அல்லது திருத்தப்படாதது, அது மிக பெரியதாக இருக்கலாம் மற்றும் அனைத்து சாதனங்களிலும் புரட்டாததாக இருக்கலாம். உங்கள் செயலாக்க வீடியோவில் சிக்கல்கள் உள்ளதில், இந்த கோப்பை உங்கள் வீடியோவை மீட்டரியாக்க மற்றும் மேலும் திருத்த படுத்த முடியும்.\",\n    \"description\": \"மூல பதிவிறக்கம் மாடல் விளக்கம்\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"பதிவிறக்குக\",\n    \"description\": \"மூல பதிவிறக்கம் மாடல் பட்டன்\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"அமைப்பு தகவலுக்கு மற்றும் வீடியோ தரவுகளுக்கு உதவிக்கு அளிக்கவும்\",\n    \"description\": \"தகவலுக்கு உதவி மாடல் தலைப்பு\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"பதிவிறக்கும் பின், இந்த கோப்பை support@screenity.io க்கு அனுப்புங்கள் மற்றும் அதிக தொடர்பான தகவலுடன். நாங்கள் இந்த தரவை பயன்படுத்தி உங்கள் விளக்க பெறும் பிரச்சினைகளை தீர்க்க உதவுவோம் மற்றும் வாடிக்கைக்கு வரை உங்கள் வீடியோவை மீட்டரியாக்குவதில் முடியும் என்பதை நிச்சயமாக சொல்லுக.\",\n    \"description\": \"தகவலுக்கு உதவி மாடல் விளக்கம்\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"பதிவிறக்குக\",\n    \"description\": \"தகவலுக்கு உதவி மாடல் பட்டன்\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"உங்கள் சாதனத்தில் விடியோ பொதுவாக செயல்படுத்த முடியவில்லை\",\n    \"description\": \"5 நிமிட வாரந்தோற்ற உள்ளடக்கம் உடைய மாதிரி மோடல் தலைப்பு\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Screenity முழுவதும் தனியாக உழைந்து உழைக்கும் போது, உங்கள் சாதனத்தின் உபகரண வகைகளால் கட்டாயமாக வர்ம்வேர் செய்வது. இந்த நீளம் உள்ள விடியோவை செயல்படுத்துவது மிகவும் நேரம் எடுக்கும் மற்றும் மிகவும் வளர்ச்சியடையும். ஆனால் வேறு பரிமாற்றம் முடியும் என்று கவனிக்கவும், நீங்கள் இந்த அபரிசீலிதம் புரிந்திருந்தால்.\",\n    \"description\": \"5 நிமிட வாரந்தோற்ற உள்ளடக்கம் மோடல் விளக்கம்\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"ஞாபகம், அதை முயற்சிக்கவும்\",\n    \"description\": \"5 நிமிட வாரந்தோற்ற உள்ளடக்கம் மோடல் பட்டன்\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"நீண்ட வீடியோக்களை செயல்படுத்துவது பற்றிய மேலும் அறிந்து கொள்ளுங்கள்.\",\n    \"description\": \"5 நிமிட வாரந்தோற்ற உள்ளடக்கம் மோடல் மேலும் அறிந்து கொள்ளுங்கள் இணைப்பு மூலம்\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"மீட்டமைப்பு முறை\",\n    \"description\": \"மீட்டமைப்பு முறை தலைப்பு\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"பிரச்சனைகளை தீர்க்க தரவை பதிவிறக்கவும்\",\n    \"description\": \"பிரச்சனைகளை தீர்க்க விருப்ப பதிவிறக்க விருப்பம்\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"உதவி மையம்\",\n    \"description\": \"உதவி வழியிடம் உருவாக்குதல்\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"பக்கத்தில் ஒலி பதிவு\",\n    \"description\": \"ஒலி எச்சரிக்கை தலைப்பு\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"இந்த பக்கத்தில் ஒலியை பதிவு செய்ய உங்கள் விருப்பத்தை '$tab$' விருப்பம் பயன்படுத்த வேண்டும்.\",\n    \"description\": \"ஒலி எச்சரிக்கை விளக்கம்\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Tab area\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"பக்கத்தில் கொள்ளும்போது ஆதரிக்கப்படாத நீக்கம்\",\n    \"description\": \"அட்டவணை ஆதரிக்கப்படாத நீக்கம் தலைப்பு\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"உங்கள் முந்தைய பக்கத்தில் Screenity ஆதரிக்கப்படவில்லை. இதையடுத்து, நீங்கள் இங்கே பதிவு ஆரம்பிக்கலாம், அப்போது மற்றொரு பக்கத்திற்கு செல்ல அநுமதிக்கப்படும், ஆனால் உங்கள் கேமரா மற்றும் கருவிப்பட்டி காணப்படாது.\",\n    \"description\": \"அட்டவணை ஆதரிக்கப்படாத நீக்கம் விளக்கம்\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"உங்கள் Screenity பதிவுகளுக்கான உள்ளூர் காப்பிடுதல் அமைத்துக்கொள்க\",\n    \"description\": \"காப்பிடுதல் தலைப்பு\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"நீங்கள் செய்த அனைத்து பதிவுகளின் ஒரு நகர்வு சொல்லியில் வைக்கவும். உங்கள் உள்ளூரை காப்பிடுவதில் தடுமாற்றம் அமையவில்லை, நீங்கள் அவை பதிவிறக்குவதை விரும்பும்போது மட்டும் பதிவு செய்யப்படும்.\",\n    \"description\": \"காப்பிடுதல் விளக்கம்\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"நீங்கள் பதிவு செய்ய விரும்பும்போது, 'பதிவுகள்' போன்ற அமைப்புகளில் பதிவு செய்ய முதல் புதிய கோப்பகத்தை உருவாக்க வேண்டும்.\",\n    \"description\": \"காப்பிடுதல் விளக்கம்\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"ஒரு கோப்பகம் தேர்ந்தெடுக்க\",\n    \"description\": \"கோப்பக தேர்ந்தெடுத்தோர் பொத்தான்\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"இப்போது அல்ல\",\n    \"description\": \"இப்போது பொத்தான்\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"உங்கள் பதிவுகள் இடப்பட்டிருக்கின்றன\",\n    \"description\": \"செயல்பாட்டு காப்புகள் தலைப்பு\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"பதிவிடும்போது ஒலி பதிவிறக்க விண்ணப்பங்களை கேட்டுக் கொள்கின்ற போது ஒலி பதிவிறக்க வேண்டாம், இந்த அட்டவணையை மூடவும் என்று பரிந்துரைக்கப்படுகின்றது.\",\n    \"description\": \"செயல்பாட்டு காப்புகள் விளக்கம்\"\n  },\n  \"backupsClose\": {\n    \"message\": \"பக்கத்தை அதேசமயம் மூடு\",\n    \"description\": \"அதேசமயம் பக்கத்தை மூடு\"\n  },\n  \"backupsStop\": {\n    \"message\": \"அனைத்து பதிவு காப்புகளையும் நிறுத்து\",\n    \"description\": \"பக்கத்து காப்பிடுதல் நிறுத்து\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"தயவுசெய்து Screenity உங்கள் உள்ளூர் கோப்பகத்துக்கு அணுகலை மீள்க\",\n    \"description\": \"காப்பிடுதல் உறுதிப்படுத்துதல் தலைப்பு\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"மன்னிக்கவும், உங்கள் பதிவிகளின் உள்ளூர் நகர்வுக்கு உங்கள் அனுமதியை மீண்டும் சேமிக்க தேவை. அதிசயமாக, நீங்கள் இந்த அட்டவணையை எப்போதும் மூடுவது வேண்டும், அது உங்கள் உலாவியை மூடும் அல்லது உங்கள் கணினியை மீட்டரைக் கொண்டு மீண்டும் ஆரம்பிக்கலாம் என்று சொல்கின்றது.\",\n    \"description\": \"காப்பிடுதல் உறுதிப்படுத்துதல் விளக்கம்\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Screenity பதிவிகளை தொடர்ந்து காப்புகள் உருவாக்க அனுமதிக்க\",\n    \"description\": \"காப்பிடுதல் உறுதிப்படுத்துதல் அனுமதிக்க\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"பதிவுகளின் காப்பிடுதல் செய்ய\",\n    \"description\": \"காப்பிடுதல் மூலம் வார்த்தை\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"பதிவு காப்பிடுதல் அனுமதி முடக்கப்பட்டது\",\n    \"description\": \"காப்பிடுதல் அனுமதி தோல்வி தலைப்பு\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"உங்கள் பதிவுகளின் உள்நிலை நகல் செய்ய ஒரு கோப்பகத்தை தேர்வு செய்தவில்லை. உங்கள் உலாவியம் அல்லது இயக்கம் பதிப்பின் பதிப்பிக்கு விருப்பம் கோரப்படும், நீங்கள் எப்போதும் பதிவுசெய்து அநுமதிக்க கேட்கப்படலாம். நீங்கள் அநுமதிகளை அனைத்தும் அவாரமாக வழிநடத்துவதை விரும்பின்றிதல் போன்றதான்.\",\n    \"description\": \"பூர்வமாக அனுமதி பிழை விளக்கம்\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"கணினியிலிருந்து ஒலியை பதிவுசெய்ய\",\n    \"description\": \"macOS க்கு ஒலியின் எச்சரிக்கை தலைப்பு\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"macOS-ல், நீங்கள் வரிசைகளிலிருந்து ஒலியை மட்டும் பதிவுசெய்யலாம். 'Chrome வரிசை' விருப்பத்தை தேர்வு செய்து, 'வரிசை ஒலி குணம் பகிரவும்' என்பதை இயக்கின்றதை உறுதிப்படுத்துங்கள்.\",\n    \"description\": \"macOS க்கு ஒலியை பதிவுசெய்தல் விருப்பம் தலைப்பு\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"கணினியிலிருந்து ஒலியை பதிவுசெய்ய\",\n    \"description\": \"பிற கணினி வடிவம் க்கு ஒலியின் எச்சரிக்கை தலைப்பு\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"கணினியிலிருந்து ஒலியை பதிவுசெய்ய விரும்பினதால், 'கணினி ஒலியை பகிர்க' விருப்பத்தை செயல்படுத்துக. இந்த விருப்பத்தை காணாதிருக்கின்றார் என்பதை பார்க்கின்றதால், Chrome ஐ புதுப்பிக்கவும் அல்லது வரிசை பதிவு விருப்பம் மாற்றுவதை முயற்சிக்கவும்.\",\n    \"description\": \"பிற கணினி வடிவம் க்கு ஒலியின் எச்சரிக்கை தலைப்பு\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"அதிசய கருவி\",\n    \"description\": \"அமைப்புகள் பட்டியலில் அதிசய கருவி குறியீட்டு\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"அதிசய கோட்டிகள் கோட்டிகள்\",\n    \"description\": \"அமைப்புகள் பட்டியலில் அதிசய கோட்டிகள் குறியீட்டு\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"புதிய ரேம் போதுமில்லை\",\n    \"description\": \"அமைப்புக்கு அந்த ரேம் போதுமில்லை பதில்\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"சாளரம் பருவமை\",\n    \"description\": \"அமைப்புகள் பட்டியலில் சாளரம் பருவமை குறிப்பு\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"இந்த திசையுக்கு உயர்வு மிகவும் சிறியது\",\n    \"description\": \"அமைப்புகள் பட்டியலில் திசை மிகவும் சிறியது குறிப்பு\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"உங்கள் சாதனத்தில் இந்த உயர்வில் பதிவு செய்ய முடியாது\",\n    \"description\": \"அமைப்புகள் பட்டியலில் அதிசய உயர்வு குறிப்பு\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"அனுமதிகளை பரவி\",\n    \"description\": \"அனுமதிகளை பரவி பொத்தானை\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"დაამატე კიდევ ერთი ჩაწერა\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"მიმდინარეობს მომზადება...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"დაიცადე! ჩაწერა მზადდება.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"ხელსაწყოთა ზოლი დამალულია. ჩართე ხელახლა მენიუდან.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"შესვლა ან რეგისტრაცია\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"გასვლა\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"თქვენ გამოხვედით სისტემიდან\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"საცავის ლიმიტი ამოწურულია\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"გამოიყენე მთელი 100 GB. წაშალე ძველი ვიდეოები ან სცენები გასაგრძელებლად.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"მართე საცავი\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"დახურვა\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"შეცდომა საცავის შემოწმებისას\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"პრობლემა წარმოიშვა საცავის შემოწმებისას. შედი ხელახლა და სცადე თავიდან.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"ვერ შემოწმდა საცავის კვოტა. სცადე თავიდან ან განაახლე გვერდი.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"სცადე ხელახლა\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"სცენა დაემატა Multi-ს\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"მზად ხარ ახალი სცენის ჩასაწერად\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"ჩაწერა უახლოვდება 90 წუთის ლიმიტს, მალე შეწყდება\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"ჩაწერა შეწყდა 90 წუთის ლიმიტის გამო\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"ჩანართის ჩაწერა ამ გვერდზე მიუწვდომელია. ჩაირთო ეკრანის ჩაწერა.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"ვიდეოს ჩაწერა გაუქმდა\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"ბმული დაკოპირდა\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"ბმულის დაკოპირება ვერ მოხერხდა\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"უახლესი\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"უძველესი\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"ყველა ვიდეო\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"ვიდეოები არ მოიძებნა\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"იტვირთება ვიდეოები...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"გადადით პანელზე\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"მოგესალმებით Screenity-ში\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"არა რეკლამებს. არა თვალთვალს. არა ლიმიტებს.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"  უბრალოდ დააჭირე ჩაწერას.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"გამოიყენე უფასოდ — რეგისტრაციის გარეშე!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"გინდა უფრო მეტი გააკეთო ვიდეოებით?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"საცავი ღრუბელში, ვიდეოს გაზიარება ბმულით\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"შეაერთე რამდენიმე კლიპი ერთ ისტორიად\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"ჭკვიანი გადიდება დაჭერაზე (ან დაამატე შენი!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"დამატე შაბლონები, სუბტიტრები, განლაგებები...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"მორგებული ფონი და მაკეტები\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 გახსენი დამატებითი ფუნქციები\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ მხარი დაუჭირე დამოუკიდებელ შემქმნელს!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"ანგარიშის პარამეტრები\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"მხარდაჭერა\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"თქვენი Pro გამოწერა არ არის აქტიური\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"თქვენი Screenity Pro გამოწერა არ არის აქტიური.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"გთხოვთ, გაააქტიურეთ ხელახლა, რომ კვლავ მიიღოთ წვდომა.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"თქვენი ვიდეოები და მონაცემები წაიშლება სამუდამოდ \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"მართე გამოწერა\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"გინდა გააგრძელო უფასო ვერსიის გამოყენება?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"გასვლა და უფასო ვერსიაზე გადართვა\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"თქვენ გამოხვედით სისტემიდან\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"ჩაწერების ანგარიშთან სინქრონიზაციისა და პრემიუმ ფუნქციებზე წვდომისთვის, შედით ხელახლა.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"შედი ხელახლა\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"შეგიძლიათ გააგრძელოთ გამოყენება ანგარიშის გარეშე – მაგრამ ჩაწერები არ შეინახება და აღდგენადიც არ იქნება.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"გაგრძელება ანგარიშის გარეშე\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"მყისიერი ჩაწერის რეჟიმი\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"მყისიერი ჩამოტვირთვა, მაგრამ კამერისა და განლაგების შეცვლა შეუძლებელი იქნება მოგვიანებით.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"მყისიერი ჩაწერის რეჟიმი\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"ეს ყველაფერი ჩაწერს ერთ ვიდეოში მყისიერი ჩამოსატვირთად და გასაზიარებლად. მოგვიანებით ვერ შეცვლი კამერის განლაგებას, მაგრამ სხვა რედაქტირება შესაძლებელი იქნება.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"გასაგებია\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"ჩანართის ჩაწერა ამ გვერდზე მიუწვდომელია\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"დამატება პროექტში: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"დასრულება\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"გინდა მეტი შეძლო ჩაწერებით?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"შედით, რომ შეინახოთ ვიდეოები ღრუბელში, გაუზიაროთ ბმულით და გამოიყენოთ დამატებითი რედაქტირების ფუნქციები.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"შედით ფასიანი ფუნქციების გასახსნელად\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"მხარი დაუჭირე დამოუკიდებელ შემქმნელს\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"გახსენი მეტი ფუნქცია\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"შედით გასაზიარებლად (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"გახსენი ღრუბლოვანი გაზიარება, მრავალსცენიანი რედაქტირება, ავტომატური გადიდებები, სუბტიტრები და სხვა\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity ყოველთვის იქნება უფასო, ღია კოდი და რეკლამის გარეშე. Pro ეხმარება ღრუბლისა და ინფრასტრუქტურის ხარჯების დაფარვაში და დამოუკიდებელი შემქმნელის მხარდაჭერაში! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"არ აჩვენო კვლავ\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"სცადე\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"მულტი რეჟიმი\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"ჩაწერე რამდენიმე სცენა — ეკრანი, კამერა ან ორივე — თანმიმდევრობით. შესანიშნავია მრავალჯერადი დუბლებისთვის, ხედების გადასართავად ან ჩანაწერის ნაწილებად დასაყოფად. დასრულების შემდეგ დააჭირე 'დასრულება', რომ ყველა სცენა ერთ ვიდეოში გაერთიანებული იხსნა რედაქტორში.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"გააქტიურეთ Pro დასაწყებად\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"გადახდით सदस्यობას Pro ფუნქციებზე წვდომისთვის.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"გადადი Pro-ზე\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Screenity-ஐ உள்ளூர் சேவையகத்தில் பயன்படுத்திக் கொண்டிருக்கிறீர்களா? எதிர்கால மேம்பாட்டை ஆதரிக்குங்கள்!\",\n    \"description\": \"தானாக ஹோஸ்ட் செய்யும் பயனர்களுக்கான பேனர் தலைப்பு\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity ஒரு தனிப்பட்ட பெண் டெவலப்பரால் உருவாக்கப்பட்டதாகும். தானாக ஹோஸ்ட் செய்வது இலவசம், ஆனால் இது பயனுள்ளதாக இருந்தால், Pro பதிப்பின் மூலம் ஆதரியுங்கள் ❤️\",\n    \"description\": \"தானாக ஹோஸ்ட் செய்யும் பயனர்களுக்கான பேனர் விளக்கம்\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"திரும்ப வருக Screenity-க்கு!\",\n    \"description\": \"திரும்ப வந்த பயனர்களுக்கான வரவேற்பு பாப்அப்பின் தலைப்பு\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ புதியது: உங்கள் பதிவு அனுபவத்தை மேம்படுத்துங்கள்\",\n    \"description\": \"மேகம் சார்ந்த சேமிப்பு அம்சத்துக்கான தலைப்பு\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro என்பது உங்கள் வீடியோக்களை மேகத்தில் சேமிக்க, ஒரு இணைப்பின் மூலம் பகிர, மேலும் மேம்பட்ட தொகுப்பு கருவிகளை அணுக ஒரு விருப்பமான புதிய தளமாகும் – நீங்கள் விரும்பும் நேரத்தில் பயன்படுத்தலாம்.\",\n    \"description\": \"மேக சேமிப்பு அம்சத்தின் விளக்கம்\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Screenity Pro பற்றி மேலும் அறிக\",\n    \"description\": \"மேக சேமிப்பு அம்சத்துக்கான செயல் அழைப்பு பொத்தான்\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Screenity Pro-க்கு வரவேற்கிறோம்\",\n    \"description\": \"Pro வரவேற்பு தோன்றும் பதிலின் தலைப்பு\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"பதிவைத் தொடங்க தயார்! நினைவில் கொள்ள சில விஷயங்கள்:\\n\\n- பதிவு செய்யும் போது கேமரா பபிள் மறைந்துவிடலாம், இது சாதாரணம். இது பின்னணியில் தனியாகப் பதிவாகும், பிறகு நீங்கள் விரும்பும் இடத்தில் அதை அமைக்கலாம்.\\n- பெரிதாக்கங்கள் (zooms) Chrome தாவல்கள் உள்ளே கிளிக் செய்தால் மட்டுமே தானாக உருவாகும் — இது ஒரு உலாவி கட்டுப்பாடு. பதிவுக்குப் பிறகு கைமுறையாக மேலும் பெரிதாக்கங்களைச் சேர்க்கலாம்.\\n- உடனடி பதிவுகளுக்காக Instant Mode-ஐ முயற்சிக்கவும். ஆனால், தொகுப்பதற்கான விருப்பங்கள் குறைவாக இருக்கும்: பின்னணிகள், அமைப்புகள், மேம்பட்ட கட்டுப்பாடுகள் கிடையாது.\",\n    \"description\": \"Pro வரவேற்பு தோன்றும் பதிலின் விளக்கம்\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"சரி, புரிந்துவிட்டது\",\n    \"description\": \"Pro வரவேற்பு தோன்றும் பதிலின் செயல்பாடு பொத்தான்\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 தோல்வி — ஆஃப்\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Screenity Pro நீட்டிப்புக்கு வரவேற்கிறோம்\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"பதிவு செய்ய முன் சில வேகமான குறிப்புகள்.<br/>தானியங்கி zoom என்பது <strong>Chrome தாவல்கள்</strong> உள்ள கிளிக்குகளுக்கு மட்டும் வேலை செய்யும். பிறகு நீங்கள் zoom-ஐ கைமுறையாகச் சேர்க்கலாம்.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"பதிவு கருவிப்பட்டை மற்றும் விளைவுகள்\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"இந்த கருவிப்பட்டையில் வரைதல், கர்சர் விளைவுகள், blur மற்றும் பதிவு கட்டுப்பாடுகளைப் பயன்படுத்தலாம்.<br/><br/><strong>இந்த கருவிப்பட்டை வீடியோவில் சேரும்</strong> மறைக்கவில்லை என்றால்.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"கேமரா தனியாக பதிவு செய்யப்படுகிறது\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"பதிவு செய்யும்போது கேமரா <strong>மறையவோ அல்லது PiP-க்கு மாறவோ</strong>லாம்; இது சாதாரணம்.<br/><br/>பிறகு நீங்கள் இடத்தை மாற்றுவதற்கு அது தனியாகப் பதிவு செய்யப்படுகிறது.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"உடனடி முறை\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"வேகமாக பகிர்வதற்கு மற்றும் <strong>வரம்பில்லா பதிவிறக்கங்களுக்கு</strong> சிறந்தது.<br/><br/>இந்த முறையில் மேம்பட்ட layout/editor விருப்பங்கள் கிடைக்காது.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"சரி\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"அடுத்து\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"பின்\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"மேலும் அறிக\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Onboarding-ஐ மீட்டமை\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"டிஸ்க் இடம் குறைவு. உங்கள் பதிவு சேமிக்கப்படுகிறது. இதுவரை பதிவான அனைத்தும் பாதுகாப்பாக உள்ளது.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"நீண்ட பதிவு\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"உங்கள் பதிவு முழுமையானது மற்றும் பாதுகாப்பானது. கீழே WebM ஆக பதிவிறக்கம் செய்யுங்கள்.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"நீண்ட பதிவுகளுக்கு கிடைக்காது\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"மீட்டமைப்பு முறையில் கிடைக்காது\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"ஆடியோ திருத்தம் மிக நீளமானது\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"இந்த ஆடியோ திருத்தம் 15 நிமிடங்களுக்கு மேல் உள்ள கிளிப்களுக்கு ஆதரவில்லை. அசல் மாற்றப்படவில்லை. முதலில் கிளிப்பை வெட்டி சுருக்க முயற்சிக்கவும்.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"திருத்தத்திற்கான நேரம் முடிந்தது\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"திருத்தம் சரியான நேரத்தில் முடியவில்லை. அசல் பதிவு மாற்றப்படவில்லை. மீண்டும் முயற்சிக்கவும் அல்லது முதலில் கிளிப்பை வெட்டுங்கள்.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"திரைப் பகிர்வு நிறுத்தப்பட்டது. உங்கள் பதிவு சேமிக்கப்பட்டது. தயாரானதும் நிறுத்துங்கள்.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"செயலாக்கத்தில் சிக்கல் ஏற்பட்டது, ஆனால் உங்கள் பதிவு பாதுகாப்பாக உள்ளது. கீழே பதிவிறக்கம் செய்யலாம்.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"உங்கள் திருத்தம் செயலாக்கப்படுகிறது. அசல் பதிவு மாற்றப்படவில்லை.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"திருத்தத்தைப் பயன்படுத்த முடியவில்லை\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"செயலாக்கத்தின் போது ஏதோ தவறு ஏற்பட்டது. உங்கள் அசல் பதிவு பாதுகாப்பாக உள்ளது. மீண்டும் முயற்சிக்கலாம் அல்லது அப்படியே பதிவிறக்கம் செய்யலாம்.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"திரைப் பகிர்வு நிறுத்தப்பட்டது. உங்கள் பதிவு சேமிக்கப்படுகிறது. இதுவரை பதிவான அனைத்தும் பாதுகாப்பாக உள்ளது.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"ஆடியோ துண்டிக்கப்பட்டது. உங்கள் பதிவு சேமிக்கப்படுகிறது. இதுவரை பதிவான வீடியோ பாதுகாப்பாக உள்ளது.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"உங்கள் பதிவு சேமிக்கப்பட்டது. எடிட்டர் திறக்கப்படவில்லை. மீண்டும் முயற்சிக்க Screenity சின்னத்தைக் கிளிக் செய்யுங்கள்.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"சேமிப்பு எதிர்பார்த்ததை விட அதிக நேரம் எடுக்கிறது. உங்கள் தரவு பாதுகாப்பாக உள்ளது. எடிட்டர் விரைவில் திறக்கும்.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Debug தகவலை நகலெடு\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Debug தகவல் நகலெடுக்கப்பட்டது\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"உதவி பெறுங்கள்\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"பதிவு சேமிக்க முடியாத அளவுக்கு சிறியது\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"வேகமான பதிவி தோல்வியடைந்தது\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"இந்த சாதனத்தில் வேகமான பதிவியின் வெளியீட்டைச் சரிபார்க்க முடியவில்லை.\\nநீங்கள் கோப்பை எப்படியும் பதிவிறக்கலாம்.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"எப்படியும் பதிவிறக்கு\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"ரத்துசெய்\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/tr/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Ekran Kayıt & Not Aracı\",\n    \"description\": \"Uzantı adı\"\n  },\n  \"extDesc\": {\n    \"message\": \"Sınırsız en iyi ücretsiz ekran kaydedici. Kayıt, açıklama, yakınlaştırma, bulanıklık, video düzenleme ve daha fazlasını yapın - giriş yapmanıza gerek yok, gizlilik dostu.\",\n    \"description\": \"Uzantı açıklaması\"\n  },\n  \"recordTab\": {\n    \"message\": \"Kaydet\",\n    \"description\": \"Açılır pencere üzerinde kayıt sekmesi etiketi\"\n  },\n  \"videosTab\": {\n    \"message\": \"Videolarınız\",\n    \"description\": \"Açılır pencere üzerinde videolar sekmesi etiketi\"\n  },\n  \"screenType\": {\n    \"message\": \"Ekran\",\n    \"description\": \"Açılır pencere üzerinde ekran kaydı türü etiketi\"\n  },\n  \"tabType\": {\n    \"message\": \"Sekme alanı\",\n    \"description\": \"Açılır pencere üzerinde sekme alanı kaydı türü etiketi\"\n  },\n  \"cameraType\": {\n    \"message\": \"Kamera\",\n    \"description\": \"Açılır pencere üzerinde kamera kaydı türü etiketi\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Açılır pencere üzerinde mockup kaydı türü etiketi\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Özel alan kaydı devre dışı\",\n    \"description\": \"Chrome sürümü 104'ten eski olanlar için özel alan kaydı devre dışı uyarısı başlığı\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Daha fazla özellik için Chrome sürümünü 110+ olarak güncelleyin\",\n    \"description\": \"Chrome sürümü 104'ten eski olanlar için özel alan kaydı devre dışı uyarısı açıklaması\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Güncelle\",\n    \"description\": \"Chrome sürümü 104'ten eski olanlar için özel alan kaydı devre dışı uyarısı eylemi\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"İzinlerinizi kontrol edin\",\n    \"description\": \"Kamera / mikrofon izinleri uyarı modalı başlığı\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Screenity'nin kamera veya mikrofonunuza erişemiyor gibi görünüyor. Kameranın simgesine tıklayarak erişime izin verdiğinizden emin olun.\",\n    \"description\": \"Kamera / mikrofon izinleri uyarı modalı açıklaması\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Kapat\",\n    \"description\": \"Kamera / mikrofon izinleri uyarı modalı kapatma düğmesi\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Kamera erişimine izin ver\",\n    \"description\": \"Kamera erişimine izin verme düğmesi\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Mikrofon erişimine izin ver\",\n    \"description\": \"Mikrofon erişimine izin verme düğmesi\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Konuşmaya başlamak için basın\",\n    \"description\": \"Konuşmaya başla anahtarı etiketi\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Kaydedilecek bir alan belirleyin\",\n    \"description\": \"Özel alan anahtarı etiketi\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Kamerayı çevir\",\n    \"description\": \"Kamera çevirme anahtarı etiketi\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Arka plan efektleri\",\n    \"description\": \"Arka plan efektleri anahtarı etiketi\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Kayda başla\",\n    \"description\": \"Kayıt düğme etiketi\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Kayıt başlatılıyor...\",\n    \"description\": \"Kayıt düğmesi işlem süreci etiketi\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Kayıt yapmak için bir kamera seçin\",\n    \"description\": \"Kamera modundayken ve hiçbir kamera seçilmediğinde kayıt düğme etiketi\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Daha fazla seçeneği göster\",\n    \"description\": \"Daha fazla seçenekler etiketi\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Sistem sesini dahil et\",\n    \"description\": \"Sistem sesi etiketi/sekmeleri\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Araç çubuğunu gizle\",\n    \"description\": \"Araç çubuğunu gizle etiketi\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Geri sayım\",\n    \"description\": \"Geri sayım etiketi\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Zaman sınırlaması ayarla\",\n    \"description\": \"Alarm etiketi\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Bulanık\",\n    \"description\": \"Arka planı bulanıklaştır etiketi\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Hiçbiri\",\n    \"description\": \"Hiçbir cihaz seçilmedi etiketi\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Kamera yok\",\n    \"description\": \"Hiçbir kamera seçilmedi etiketi\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Mikrofon yok\",\n    \"description\": \"Hiçbir mikrofon seçilmedi etiketi\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Bir kaynak seçin\",\n    \"description\": \"Kaynak seçme alanı etiketi\"\n  },\n  \"offLabel\": {\n    \"message\": \"Kapalı\",\n    \"description\": \"Açılır listede Kapalı etiketi\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Genişlik\",\n    \"description\": \"Bölge genişliği etiketi\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Yükseklik\",\n    \"description\": \"Bölge yüksekliği etiketi\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Resim eklemek için tıklayın\",\n    \"description\": \"Yeni bir resim eklenirken gösterilen bilgilendirme penceresi\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Kaydı Bitir\",\n    \"description\": \"Kaydı Bitir araç ipucu\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Kaydı Yeniden Başlat\",\n    \"description\": \"Kaydı Yeniden Başlat araç ipucu\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Kaydı Durdur\",\n    \"description\": \"Kaydı Durdur araç ipucu\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Kaydı Devam Ettir\",\n    \"description\": \"Kaydı Devam Ettir araç ipucu\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Kaydı İptal Et\",\n    \"description\": \"Kaydı İptal Et araç ipucu\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Çizim Araçlarını Aç/Kapat\",\n    \"description\": \"Çizim Araçlarını Aç/Kapat araç ipucu\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Bulanıklaştırma Aracını Aç/Kapat\",\n    \"description\": \"Bulanıklaştırma Aracını Aç/Kapat araç ipucu\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"İşaretçi Seçeneklerini Aç/Kapat\",\n    \"description\": \"İşaretçi Seçeneklerini Aç/Kapat araç ipucu\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Kamerayı Kapat\",\n    \"description\": \"Kamerayı Kapat araç ipucu\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Kamerayı Aç\",\n    \"description\": \"Kamerayı Aç araç ipucu\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Kamera İzni Yok\",\n    \"description\": \"Kamera İzni Yok araç ipucu\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Mikrofonu Kapat\",\n    \"description\": \"Mikrofonu Kapat araç ipucu\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Mikrofonu Aç\",\n    \"description\": \"Mikrofonu Aç araç ipucu\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Mikrofon İzni Yok\",\n    \"description\": \"Mikrofon İzni Yok araç ipucu\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Araç Seç\",\n    \"description\": \"Araç Seç araç ipucu\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Kalem Aracı\",\n    \"description\": \"Kalem Aracı araç ipucu\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Marker Aracı\",\n    \"description\": \"Marker Aracı araç ipucu\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Silgi Aracı\",\n    \"description\": \"Silgi Aracı araç ipucu\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Metin Aracı\",\n    \"description\": \"Metin Aracı araç ipucu\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Şekil Aracı\",\n    \"description\": \"Şekil Aracı araç ipucu\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Ok Aracı\",\n    \"description\": \"Ok Aracı araç ipucu\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Resim Aracı\",\n    \"description\": \"Resim Aracı araç ipucu\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Geri Al\",\n    \"description\": \"Geri Al araç ipucu\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Yinele\",\n    \"description\": \"Yinele araç ipucu\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Tuvali Temizle\",\n    \"description\": \"Tuvali Temizle araç ipucu\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Daha Fazla Renk\",\n    \"description\": \"Daha Fazla Renk araç ipucu\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Kalın Hat\",\n    \"description\": \"Kalın Hat araç ipucu\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Orta Kalınlıkta Hat\",\n    \"description\": \"Orta Kalınlıkta Hat araç ipucu\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"İnce Hat\",\n    \"description\": \"İnce Hat araç ipucu\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Resim İçinde Resim Modunu Aç/Kapat\",\n    \"description\": \"Resim İçinde Resim Modunu Aç/Kapat araç ipucu\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Dolgu Seçeneğini Aç/Kapat\",\n    \"description\": \"Dolgu Seçeneğini Aç/Kapat araç ipucu\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Çizim Modu\",\n    \"description\": \"Çizim Modu bilgilendirme penceresi\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Bulanıklaştırma Modu\",\n    \"description\": \"Bulanıklaştırma Modu bilgilendirme penceresi\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Tüm bulanık öğeleri temizle\",\n    \"description\": \"Tüm bulanık öğeleri açıklama\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Tıklamaları vurgula\",\n    \"description\": \"Tıklamaları vurgula açıklama\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"İmleci vurgula\",\n    \"description\": \"İmleci vurgula açıklama\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"İmleci aydınlat\",\n    \"description\": \"İmleci aydınlat açıklama\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Tekrar gösterme\",\n    \"description\": \"İzinler penceresini kapat düğmesi\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Kaydı yeniden başlatmak istediğinizden emin misiniz?\",\n    \"description\": \"Yeniden başlatma modalı başlık\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Mevcut video ilerlemeniz kaybedilecektir.\",\n    \"description\": \"Yeniden başlatma modalı açıklama\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Kaydı yeniden başlat\",\n    \"description\": \"Yeniden başlatma modalı yeniden başlat düğmesi\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Kayda devam et\",\n    \"description\": \"Yeniden başlatma modalı devam et düğmesi\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Kaydı silmek istediğinizden emin misiniz?\",\n    \"description\": \"Silme modalı başlık\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Mevcut video ilerlemeniz kaybedilecektir.\",\n    \"description\": \"Silme modalı açıklama\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Kaydı sil\",\n    \"description\": \"Silme modalı sil düğmesi\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Kayda devam et\",\n    \"description\": \"Silme modalı devam et düğmesi\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Ne kaydetmek istediğinizi seçin\",\n    \"description\": \"Kayıt sayfasındaki başlık\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Kaydediliyor...\",\n    \"description\": \"Kayıt sayfasındaki kaydediliyor başlığı\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Bu sekme açık kalmalıdır. Kaydınız kaydedildikten sonra kapanacaktır.\",\n    \"description\": \"Kayıt sayfasındaki açıklama\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Kayıt hazırlanıyor...\",\n    \"description\": \"Kum havuzu sayfasındaki başlık (kayıt / kaydediliyor)\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Bu sekme açık kalmalıdır. Kaydınız hazır olduğunda güncellenecektir.\",\n    \"description\": \"Kum havuzu sayfasındaki açıklama (kayıt / kaydediliyor)\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Editör\",\n    \"description\": \"Kum havuzu editör sayfasındaki başlık\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"İptal\",\n    \"description\": \"Kum havuzu editör sayfasındaki İptal düğmesi\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Orijinaline geri dön\",\n    \"description\": \"Kum havuzu editör sayfasındaki Geri Dön düğmesi\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Sıfırla\",\n    \"description\": \"Kum havuzu editör sayfasındaki Sıfırla düğmesi\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Değişiklikleri kaydet\",\n    \"description\": \"Kum havuzu editör sayfasındaki Kaydet düğmesi\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Kaydediliyor...\",\n    \"description\": \"Kum havuzu editör sayfasındaki Kaydet düğmesi (kaydediliyor)\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Ses dosyanızı sürükleyip bırakın\",\n    \"description\": \"Kum havuzu editör sayfasındaki ses dosyasını sürükleyip bırakın\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Veya taramak için tıklayın\",\n    \"description\": \"Kum havuzu editör sayfasındaki ses dosyasını taramak için tıklayın\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Ayarlar\",\n    \"description\": \"Kum havuzu editör sayfasındaki ses ayarları başlığı\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Ses Seviyesi\",\n    \"description\": \"Kum havuzu editör sayfasındaki ses seviyesi etiketi\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Güncelle\",\n    \"description\": \"Kum havuzu editör sayfasındaki sesi güncelle düğmesi\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Kırp\",\n    \"description\": \"Kum havuzu editör sayfasındaki kırpma başlığı\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Genişlik\",\n    \"description\": \"Özellikler için Genişlik etiketi\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Yükseklik\",\n    \"description\": \"Özellikler için Yükseklik etiketi\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Sol\",\n    \"description\": \"Özellikler için Sol etiketi\"\n  },\n  \"topLabel\": {\n    \"message\": \"Üst\",\n    \"description\": \"Özellikler için Üst etiketi\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Kolları sürükleyin ve seçilen bölümü düzenlemek için soldaki düğmeleri kullanın.\",\n    \"description\": \"Kum havuzlu editörde kesme hakkında bilgi\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Videoyu Kırp\",\n    \"description\": \"Kum havuzlu editörde kırpma düğmesi\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Kırpılıyor...\",\n    \"description\": \"Kum havuzlu editörde kesme sırasında kırpma düğmesi\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Bölümü Kes\",\n    \"description\": \"Kum havuzlu editörde kesme düğmesi\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Kesiliyor...\",\n    \"description\": \"Kum havuzlu editörde kesme sırasında kesme düğmesi\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Sesi Kapat\",\n    \"description\": \"Kum havuzlu editörde sesi kapatma düğmesi\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Susturuluyor...\",\n    \"description\": \"Kum havuzlu editörde sesi sustururken sesi kapatma düğmesi\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Daha Fazla Bilgi\",\n    \"description\": \"Nokta ile daha fazla bilgi\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Geri Al\",\n    \"description\": \"Geri Al etiketi\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Yinele\",\n    \"description\": \"Yinele etiketi\"\n  },\n  \"leaveReview\": {\n    \"message\": \"İnceleme yazın\",\n    \"description\": \"İnceleme bırakma düğmesi\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Güncellemeleri Takip Edin\",\n    \"description\": \"Güncellemeleri takip etme düğmesi\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Şu anda çevrimdışısınız\",\n    \"description\": \"Çevrimdışı etiketi\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Bağlantınız yeniden sağlanana kadar bazı özellikler kullanılamaz\",\n    \"description\": \"Çevrimdışı etiketi açıklaması\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Yeniden Deneyin\",\n    \"description\": \"Çevrimdışı etiketi tekrar deneme düğmesi\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome güncellenmesi gerekiyor\",\n    \"description\": \"Chrome güncelleme etiketi\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Daha fazla özelliğe erişmek için güncelleyin\",\n    \"description\": \"Chrome güncelleme açıklaması\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Daha Fazla Bilgi\",\n    \"description\": \"Daha fazla bilgi düğmesi\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Video yerel olarak işlemek için çok uzun\",\n    \"description\": \"Editörde 5 dakikadan fazla sınır etiketi\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Düzenleme ve MP4 biçiminde dışa aktarma mevcut değil\",\n    \"description\": \"Editörde 5 dakikadan fazla sınır açıklaması\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Video işleniyor...\",\n    \"description\": \"Editörde video işleme etiketi\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Daha hızlı düzenleme için yükseltme yapın\",\n    \"description\": \"Editörde video işleme açıklaması\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Düzenle\",\n    \"description\": \"Kum havuzlu editör sayfasındaki düzenleme başlığı\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Bağlantı Yok\",\n    \"description\": \"Bağlantı yok etiketi\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Kullanılamıyor\",\n    \"description\": \"Kullanılamıyor etiketi\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Videoyu Düzenle\",\n    \"description\": \"Kırpma etiket düğmesi\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Videoyu kırp, kes veya sustur\",\n    \"description\": \"Kırpma etiketi\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Hazırlanıyor...\",\n    \"description\": \"Hazırlanıyor etiketi\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Kırp\",\n    \"description\": \"Kırpma düğmesi\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Videoyu kırp ve yeniden boyutlandır\",\n    \"description\": \"Kırpma etiketi\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Ses Ekle\",\n    \"description\": \"Ses ekleme düğmesi\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Kendi sesinizi yükleyerek videoya ses ekleyin\",\n    \"description\": \"Ses ekleme etiketi\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Kaydet\",\n    \"description\": \"Kum havuzlu editör sayfasındaki kaydetme başlığı\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Drive'dan Çıkış Yap\",\n    \"description\": \"Google Drive'dan çıkış yapma etiketi\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Kaydediliyor...\",\n    \"description\": \"Google Drive'a kaydetme etiketi\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Drive'a Kaydet\",\n    \"description\": \"Google Drive'a kaydetme düğmesi\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Mevcut sürümü Google Drive'a kaydedin\",\n    \"description\": \"Google Drive'a kaydetme etiketi\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Oturum açın ve Drive'a kaydedin\",\n    \"description\": \"Google Drive'a oturum açma etiketi\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Dışa Aktar\",\n    \"description\": \"Kum havuzlu editör sayfasındaki dışa aktarma başlığı\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"İndiriliyor...\",\n    \"description\": \"İndiriliyor etiketi\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \".webm olarak İndir\",\n    \"description\": \"WEBM olarak İndir düğmesi\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \".webm video olarak dışa aktar\",\n    \"description\": \"WEBM olarak İndir etiketi\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \".mp4 olarak İndir\",\n    \"description\": \"MP4 olarak İndir düğmesi\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \".mp4 video olarak dışa aktar (önerilen)\",\n    \"description\": \"MP4 olarak İndir etiketi\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \".gif olarak İndir\",\n    \"description\": \"GIF olarak İndir düğmesi\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"GIF olarak dışa aktar (maksimum 30 saniye)\",\n    \"description\": \"GIF olarak İndir etiketi\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Kayıtlarını geliştirmeye hazır mısın?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Bulutta paylaşım, yakınlaştırmalar, şablonlar... ve gizliliğe önem veren bağımsız bir kadın geliştiriciyi destekle ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Pro hakkında daha fazla bilgi\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Zaten bir hesabın var mı? Giriş yap\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Videoyu Paylaş\",\n    \"description\": \"Kum havuzlu editör sayfasındaki paylaşma düğmesi\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Mevcut sesi değiştir\",\n    \"description\": \"Editörde sesi değiştirme düğmesi\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"İşarete Yakınlaştır\",\n    \"description\": \"Noktaya yakınlaşma açılır penceresi\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Kayıt yaparken sayfada kal\",\n    \"description\": \"Sayfa içinde kalma açılır penceresi\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Mikrofon kapalı uyarısı\",\n    \"description\": \"Mikrofon hatırlatma açılır penceresi\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Yardım\",\n    \"description\": \"Yardım açılır pencere düğmesi\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Kayıt durdu. Devam etmek için oynatma düğmesine basın.\",\n    \"description\": \"Durdurulan kayıt modal başlığı\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity ekranınızı kaydetme iznine sahip değil\",\n    \"description\": \"İzin modal başlığı\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Ekran kaydını etkinleştirmek için, Screenity'nin ekranınızı yakalamasına izin vermelisiniz.\",\n    \"description\": \"İzin modal açıklaması\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Ekran kaydını etkinleştir\",\n    \"description\": \"İzin modal düğmesi\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"İptal\",\n    \"description\": \"İzin modal iptal\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Mikrofonunuz kapalı\",\n    \"description\": \"Mikrofon kapalı modal başlığı\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Kaydınıza ses eklemek için mikrofonunuzu açmanız gerekecek. Sesi olmadan devam etmek istiyor musunuz?\",\n    \"description\": \"Mikrofon kapalı modal açıklaması\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Evet, devam et\",\n    \"description\": \"Mikrofon kapalı modal devam et\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"İptal\",\n    \"description\": \"Mikrofon kapalı modal iptal\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Screenity ile üç basit adımda başlayın:\",\n    \"description\": \"Kurulum adımları başlığı\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- \",\n    \"description\": \"Kurulum adımı 1, ikonun önünde. Sonunda bir boşluk olmalı.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" uzantı ikonuna tıklayın\",\n    \"description\": \"Kurulum adımı 1, ikonun sonunda. Başında bir boşluk olmalı.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- \",\n    \"description\": \"Kurulum adımı 2, ikonun önünde. Sonunda bir boşluk olmalı.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" pin ikonuna basın\",\n    \"description\": \"Kurulum adımı 2, ikonun sonunda. Başında bir boşluk olmalı.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- \",\n    \"description\": \"Kurulum adımı 3, ikonun önünde. Sonunda bir boşluk olmalı.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity ikonuna tıklayarak başlayın\",\n    \"description\": \"Kurulum adımı 3, ikonun sonunda. Başında bir boşluk olmalı.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Harika! Şimdi hazırsınız\",\n    \"description\": \"Kurulum tamamlandı başlığı\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Şimdi burada veya başka bir sekmede kayıt yapmaya başlayabilirsiniz.\",\n    \"description\": \"Kurulum tamamlandı açıklaması\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Çizim yapmak için buraya tıklayın\",\n    \"description\": \"Buraya tıklama rehberi\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Rahatlayın. Saymayı durdurmak için herhangi bir yere tıklayın.\",\n    \"description\": \"Gerisayım mesajı\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Çerçeve aralıkları nedeniyle kısa zaman aralıkları için düzenlemeler doğru olmayabilir.\",\n    \"description\": \"Editörün çok küçük olduğu hakkında bilgi\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Video işleniyor, oynatma yanıltıcı veya kesilebilir.\",\n    \"description\": \"Editörde işleme bannerı\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Kırpma biraz zaman alabilir\",\n    \"description\": \"Kırpma bilgi başlığı\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Daha hızlı işlem için yükseltme yapın\",\n    \"description\": \"Kırpma bilgi açıklaması\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Mikrofon etkin\",\n    \"description\": \"Mikrofon etkinleştirildi bildirimi başlığı\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Mikrofon devre dışı\",\n    \"description\": \"Mikrofon devre dışı bırakıldı bildirimi başlığı\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"UI bildirimlerini gizle\",\n    \"description\": \"UI uyarılarını gizle düğmesi\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Tekrar gösterme\",\n    \"description\": \"Tekrar gösterme düğmesi\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Kullanılmadığında araç çubuğunu gizle\",\n    \"description\": \"Yalnızca üzerine gelindiğinde araç çubuğunu göster düğmesi\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Kaydı Durdur\",\n    \"description\": \"Kaydı Durdur düğmesi\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Yeni Screenity'ye Hoş Geldiniz!\",\n    \"description\": \"Mevcut kullanıcılar için güncelleme duyurusu başlığı\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Yeni bir görünüm ve birçok heyecan verici özellik tanıtıyoruz, ama endişelenmeyin, hala bildiğiniz ve sevdiğiniz ücretsiz, özel ve açık kaynaklı uzantı aynı.\",\n    \"description\": \"Mevcut kullanıcılar için güncelleme duyurusu açıklaması\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Güncelleme hakkında daha fazla bilgi edinin.\",\n    \"description\": \"Mevcut kullanıcılar için güncelleme duyurusu daha fazla bilgi bağlantısı\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Başlamak İçin\",\n    \"description\": \"Mevcut kullanıcılar için güncelleme duyurusu düğmesi\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Kaydın başlatılmasında hata oluştu\",\n    \"description\": \"Akış hatası modal başlığı\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Kaydın başlatılmasında bir hata oluştu gibi görünüyor. Tarayıcınızı yeniden başlatın ve tekrar deneyin. Sorun devam ederse, lütfen support@screenity.io adresinden bize ulaşın.\",\n    \"description\": \"Akış hatası modal açıklaması\"\n  },\n  \"highestQuality\": {\n    \"message\": \"En Yüksek Kalite\",\n    \"description\": \"Açılır penceredeki en yüksek kalite etiketi\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Son kaydı geri yükle\",\n    \"description\": \"Açılır penceredeki son kaydı geri yükle düğmesi\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Sorun mu yaşıyorsunuz?\",\n    \"description\": \"Editörde sorunlar düğmesi\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Kaydınızı göremiyor musunuz?\",\n    \"description\": \"Editörde sorunlar modal başlığı\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Kaydınızı tamamladınız ve videonuzu görmeyen bir sayfadaysanız, mevcutsa ham video verilerini indirmeyi deneyebilirsiniz. Ayrıca bize ulaşabilir ve bir hata raporu gönderebilirsiniz.\",\n    \"description\": \"Editörde sorunlar modal açıklaması\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Ham video verilerini indir\",\n    \"description\": \"Editörde sorunlar modal düğmesi\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Hata Bildir\",\n    \"description\": \"Editörde sorunlar modal düğme 2\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Kayıt bulunamadı, üzgünüz :(\",\n    \"description\": \"Editörde kayıt bulunamadı uyarısı\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Hafıza Sınırına Ulaşıldı\",\n    \"description\": \"Hafıza Sınırına Ulaşıldı Başlık\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Cihazınız, kayıt için kullanılabilir belleği tüketti ve kayıt otomatik olarak durdu. Daha uzun süre kayıt yapmak istiyorsanız video kalitesini düşürmeyi veya cihazınızdaki alanı temizlemeyi deneyebilirsiniz.\",\n    \"description\": \"Hafıza Sınırına Ulaşıldı Açıklama\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Anladım\",\n    \"description\": \"Anladım Düğmesi\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Yetersiz Alan\",\n    \"description\": \"Yetersiz Alan Başlık\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity, kayıt için en az 1 GB boş alan gerektirir. Lütfen cihazınızda alan açın ve yeniden deneyin.\",\n    \"description\": \"Yetersiz Alan Açıklama\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Önceki Kayıtları Temizle\",\n    \"description\": \"Önceki Kayıtları Temizle Düğmesi\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Gelişmiş\",\n    \"description\": \"Güvenli düzenleyici sayfasındaki Gelişmiş bölüm\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Ham video dosyasını indir\",\n    \"description\": \"Ham kayıt düğmesi\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Orijinal kayıt verilerini dışa aktar\",\n    \"description\": \"Ham kayıt etiketi\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Kaydınızla ilgili yardım alın\",\n    \"description\": \"Sorun giderme düğmesi\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Videoyu kurtarın ve diğer sorunları çözün\",\n    \"description\": \"Sorun giderme etiketi\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Ham video dosyasını indir\",\n    \"description\": \"Ham kayıt modal başlığı\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Ham video dosyası, Screenity tarafından kaydedilen orijinal video dosyasıdır. Bu video işlenmemiş veya düzenlenmemiş olduğundan çok büyük olabilir ve tüm cihazlarda oynatılamayabilir. İşlenmiş video ile sorun yaşıyorsanız, bu dosyayı videoyu kurtarmak için kullanabilirsiniz.\",\n    \"description\": \"Ham kayıt modal açıklaması\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"İndir\",\n    \"description\": \"Ham kayıt modal düğmesi\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Sistem bilgisi ve video verilerini indirip sorun giderme için gönderin\",\n    \"description\": \"Sorun giderme modal başlığı\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"İndirdikten sonra dosyayı support@screenity.io adresine gönderin ve ilgili bilgileri ekleyin. Bu verileri, uzantı ile ilgili yaşadığınız sorunları çözmek ve mümkünse videonuzu kurtarmak için kullanacağız.\",\n    \"description\": \"Sorun giderme modal açıklaması\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"İndir\",\n    \"description\": \"Sorun giderme modal düğmesi\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Videonuz cihazınızda işlemek için çok uzun\",\n    \"description\": \"5 dakika sınırlı modal başlığı\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Screenity tamamen özel ve yerel olarak çalıştığı için cihazınızın donanımıyla sınırlıdır. Bu uzunluktaki bir videoyu işlemek çok uzun sürer ve çok fazla kaynak kullanır. Ancak, hala WEBM dosyasını veya ham kaydı indirebilirsiniz, ancak düzenleme ve MP4 dışa aktarma seçenekleri mevcut değildir. Yine de riskleri anlıyorsanız, videoyu işlemeyi deneyebilirsiniz.\",\n    \"description\": \"5 dakika sınırlı modal açıklaması\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Riski anladım, yine de dene\",\n    \"description\": \"5 dakika sınırlı modal düğmesi\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Uzun videoları işleme hakkında daha fazla bilgi edinin.\",\n    \"description\": \"5 dakika sınırlı modal daha fazla bilgi öğrenme bağlantısı\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Kurtarma Modu\",\n    \"description\": \"Kurtarma Modu Başlığı\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Sorunları Gidermek için Verileri İndir\",\n    \"description\": \"Sorunları Giderme Seçeneği\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Yardım Merkezi\",\n    \"description\": \"Yardım Navigasyon Öğesi\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Sayfa Sesini Kaydet\",\n    \"description\": \"Ses Uyarı Başlığı\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Bu sayfanın sesini kaydetmek için '$tab$' seçeneğini kullanmalısınız.\",\n    \"description\": \"Ses Uyarı Açıklaması\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Sekme Alanı\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Sayfada Desteklenmeyen Uzantı\",\n    \"description\": \"Sekme Üzerinde Desteklenmeyen Uzantı Başlığı\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity önceki sekmenizde desteklenmiyordu. Yine de burada kaydı başlatabilir ve ardından sekmeye geçebilirsiniz, ancak kamera ve araç çubuğunuz görünür olmayacaktır.\",\n    \"description\": \"Sekme Üzerinde Desteklenmeyen Uzantı Açıklaması\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Screenity Kayıtlarınız için Yerel Yedekleme Ayarları\",\n    \"description\": \"Yedeklemeler Başlığı\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Yaptığınız tüm kayıtların bir yedeğini alın. Yedeklemeleri yapılandırmazsanız, kayıtlarınızı indirmeye karar verdiğinizde yalnızca kaydedilecektir.\",\n    \"description\": \"Yedeklemeler Açıklaması\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"İndirmeler veya diğer sistem klasörlerine kaydetmeye çalışıyorsanız, önce yeni bir klasör oluşturmanız gerekebilir.\",\n    \"description\": \"Yedeklemeler Açıklaması\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Klasör Seç\",\n    \"description\": \"Klasör Seçme Düğmesi\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Şimdi Değil\",\n    \"description\": \"Şimdi Değil Düğmesi\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Kayıtlarınız Yerel Olarak Senkronize Ediliyor\",\n    \"description\": \"Etkin Yedeklemeler Başlığı\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Her kayıt yaparken izin istemekten kaçınmak için bu sekme açık bırakmanızı öneririz.\",\n    \"description\": \"Etkin Yedeklemeler Açıklaması\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Yine de Sekmeyi Kapat\",\n    \"description\": \"Yine de Sekmeyi Kapat Düğmesi\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Tüm Kayıtların Yedeklemesini Durdur\",\n    \"description\": \"Yedeklemeleri Durdur Düğmesi\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Lütfen Screenity'in Yerel Yedekleme Klasörüne Erişimini Tekrar Onaylayın\",\n    \"description\": \"Yedeklemeler Onay Başlığı\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Üzgünüz, ancak kayıtlarınızın yerel kopyalarını oluşturmaya devam etmek için tekrar izniniz gerekiyor. Maalesef, bu sekme kapandığında her seferinde sormamız gerekecek, bu da tarayıcıyı kapatmanız veya bilgisayarınızı yeniden başlatmanız durumunda olabilir.\",\n    \"description\": \"Yedeklemeler Onay Açıklaması\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Screenity'e Kayıtların Yedeklemesine Devam Etme İzni Ver\",\n    \"description\": \"Yedeklemeler Onayında İzin Ver Düğmesi\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Kayıtların Yedeklerini Al\",\n    \"description\": \"Yedeklemeleri Değiştir Etiketi\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Kaydın Yedeklemesi İçin İzin Verilmedi\",\n    \"description\": \"Yedekleme İzin Reddi Başlığı\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Kayıtlarınızın yerel bir kopyası oluşturmak için bir klasör seçmediniz. Tarayıcınızın veya işletim sisteminizin sürümüne bağlı olarak, her kayıt yapmaya başladığınızda klasörü seçmeniz istenebilir. Tekrar deneyebilir veya izinleri tekrar vermek istemiyorsanız yedeklemeleri tamamen devre dışı bırakabilirsiniz.\",\n    \"description\": \"Yedekleme İzin Reddi Açıklaması\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Bilgisayar Sesini Kaydet\",\n    \"description\": \"macOS İçin Ses Uyarı Başlığı\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"macOS'ta yalnızca sekmelerden ses kaydedebilirsiniz. 'Chrome Sekmesi' seçeneğini seçin ve 'Ses Sekmesiyle de paylaş' seçeneğinin etkin olduğundan emin olun.\",\n    \"description\": \"macOS İçin Ses Uyarı Açıklaması\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Bilgisayar Sesini Kaydet\",\n    \"description\": \"Diğer Sistemler İçin Ses Uyarı Başlığı\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Bilgisayar sesini kaydetmek istiyorsanız, 'Sistem Sesini Paylaş' seçeneğinin etkin olduğundan emin olun. Bu seçeneği görmüyorsanız, Chrome'u güncellemeyi deneyin veya sekme kaydı seçeneğine geçin.\",\n    \"description\": \"Diğer Sistemler İçin Ses Uyarı Açıklaması\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Maksimum çözünürlük\",\n    \"description\": \"Ayarlar menüsündeki maksimum çözünürlük etiketi\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Maksimum FPS\",\n    \"description\": \"Ayarlar menüsündeki maksimum FPS etiketi\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Yetersiz RAM\",\n    \"description\": \"Ayarlar menüsünde yetersiz RAM etiketi\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Pencereyi Yeniden Boyutlandır\",\n    \"description\": \"Ayarlar menüsündeki pencere boyutlandırma etiketi\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Bu çözünürlüğe pencereyi yeniden boyutlandırmak için ekran çok küçük\",\n    \"description\": \"Ayarlar menüsündeki ekran çok küçük bilgi baloncuğu\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Bu çözünürlükte kayıt yapmak cihazınızda mümkün değil\",\n    \"description\": \"Ayarlar menüsündeki maksimum çözünürlük bilgi baloncuğu\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"İzinleri İncele\",\n    \"description\": \"İzinleri İncele düğmesi\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Başka bir kayıt ekle\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Hazırlanıyor...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Beklemede kal! Kayıt hazırlanıyor.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Araç çubuğu gizlendi. Açılır menüden yeniden etkinleştirin.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Giriş yap veya kaydol\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Çıkış yap\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Oturumunuz kapatıldı\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Depolama sınırına ulaşıldı\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"100 GB depolama alanınızın tamamını kullandınız. Lütfen kayıt yapmaya devam etmek için eski videoları veya sahneleri silin.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Depolamayı yönet\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Kapat\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Depolama kontrol edilemedi\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Depolamanız kontrol edilirken bir sorun oluştu. Lütfen tekrar giriş yapın ve kaydı deneyin.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Depolama kotanızı doğrulayamadık. Lütfen birkaç saniye sonra tekrar deneyin veya sayfayı yenileyin.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Tekrar dene\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Sahne Multi kaydına eklendi\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Videona yeni bir sahne kaydetmeye hazırsın\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Kayıt 90 dakikalık sınıra ulaşıyor, yakında duracak\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Kayıt 90 dakikalık sınıra ulaştığı için durduruldu\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Bu sayfada sekme kaydı devre dışı. Ekran kaydına geçildi.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Video kaydı iptal edildi\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Bağlantı panoya kopyalandı\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Bağlantı kopyalanamadı\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"En yeni\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"En eski\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Tüm videolar\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Hiç video bulunamadı\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Videolar yükleniyor...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Panel'e git\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Screenity'e hoş geldin\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Reklam yok. Takip yok. Sınır yok.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Sadece kayda bas.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Ücretsiz kullan — kaydolmana gerek yok!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Videolarınla daha fazlasını yapmak ister misin?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Bulut depolama, bağlantıyla paylaş\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Birden fazla klibi tek bir hikâyede birleştir\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Tıklamada akıllı yakınlaştırmalar (ya da kendi yakınlaştırmanı ekle!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Şablonlar, altyazılar, düzenler ekle...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Özel arka planlar ve maketler\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Ekstra özelliklerin kilidini aç\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Bağımsız bir geliştiriciyi destekle!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Hesap ayarları\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Destek\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Pro aboneliğiniz aktif değil\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Screenity Pro aboneliğiniz aktif değil.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Erişime devam etmek için lütfen yeniden etkinleştirin.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Videolarınız ve verileriniz şu tarihte kalıcı olarak silinecek: \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Aboneliğini yönet\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Ücretsiz sürümü kullanmaya devam etmek ister misiniz?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Çıkış yap ve ücretsiz sürüme geç\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Oturumunuz kapatıldı\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Kayıtlarınızın hesabınıza senkronize edilmesi ve premium özelliklere erişim için tekrar giriş yapmalısınız.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Yeniden giriş yap\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Uzantıyı hesap olmadan kullanmaya devam edebilirsin - ancak kayıtların kaydedilmeyecek ve daha sonra kurtarılamayacak.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Giriş yapmadan devam et\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Anında kayıt modu\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Anında indirme, ancak kamera ve düzen daha sonra düzenlenemez.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Anında kayıt modu\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Bu mod her şeyi tek bir videoya kaydeder, anında indirip paylaşabilirsiniz. Kamera düzenini daha sonra değiştiremezsiniz, ancak diğer düzenlemeler hâlâ mümkündür.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Anladım\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Bu sayfada sekme kaydı devre dışı\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Şuraya ekleniyor: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Bitir\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Kayıtlarınla daha fazlasını yapmak ister misin?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Videolarını buluta kaydetmek, bağlantıyla paylaşmak ve gelişmiş düzenleme özelliklerine erişmek için giriş yap.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Ücretli özelliklerin kilidini açmak için giriş yap\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Bağımsız bir geliştiriciyi destekle\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Daha fazla özelliğin kilidini aç\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Paylaşmak için giriş yap (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Bulut paylaşımı, çok sahneli düzenleme, otomatik yakınlaştırma, altyazılar ve daha fazlasının kilidini aç\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity her zaman ücretsiz, açık kaynak ve reklamsız olacak. Pro, bulut & altyapı maliyetlerini karşılar ve bağımsız bir geliştiriciyi destekler! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Bir daha gösterme\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Dene\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Multi modu\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Ekran, kamera veya her ikisi gibi birden fazla sahneyi art arda kaydedin. Birden fazla deneme yapmak, görünümleri değiştirmek veya kaydınızı parçalara ayırmak için harikadır. İşiniz bittiğinde, tüm sahnelerin tek bir videoda birleştirildiği editörü açmak için Bitir'e tıklayın.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Başlamak için Pro’yu etkinleştir\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Pro özelliklere erişmek için abone olun.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Pro’ya abone ol\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"Screenity’yi yerel olarak mı kullanıyorsunuz? Gelecekteki geliştirmelere destek olun!\",\n    \"description\": \"Kendi sunucusunda barındıran kullanıcılar için banner başlığı\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity, bağımsız bir kadın geliştirici tarafından oluşturuldu. Kendi sunucunuzda kullanmak ücretsizdir, ancak faydalı buluyorsanız, Pro ile desteklemeyi düşünebilirsiniz ❤️\",\n    \"description\": \"Kendi sunucusunda barındıran kullanıcılar için banner açıklaması\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"Screenity’e tekrar hoş geldiniz\",\n    \"description\": \"Geri dönen kullanıcılar için karşılama açılır penceresi başlığı\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Yeni: Kayıtlarınızı bir üst seviyeye taşıyın\",\n    \"description\": \"Karşılama açılır penceresinde bulut depolama özelliği başlığı\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro, videolarınızı bulutta saklamak, bağlantıyla paylaşmak ve gelişmiş düzenleme araçlarının kilidini açmak için isteğe bağlı yeni bir platformdur – ihtiyacınız olduğunda kullanabilirsiniz.\",\n    \"description\": \"Karşılama açılır penceresinde bulut depolama özelliğinin açıklaması\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Screenity Pro hakkında daha fazla bilgi edinin\",\n    \"description\": \"Karşılama açılır penceresinde bulut depolama özelliği için harekete geçirici buton\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Screenity Pro'ya Hoş Geldiniz\",\n    \"description\": \"Pro karşılama modali başlığı\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Kayda başlamaya hazırsınız! Aklınızda bulundurmanız gereken birkaç şey:\\n\\n- Kayıt sırasında kamera balonu kaybolabilir, bu normaldir. Arka planda ayrı olarak kaydedilir, böylece sonradan istediğiniz gibi konumlandırabilirsiniz.\\n- Yakınlaştırmalar (zoom), yalnızca Chrome sekmeleri içindeki tıklamalarda otomatik olarak oluşturulur — bu, tarayıcıdan kaynaklı bir sınırlamadır. Kayıttan sonra manuel olarak daha fazla yakınlaştırma ekleyebilirsiniz.\\n- Hızlı kayıtlar ve anında indirme için Instant Mode'u deneyin. Ancak, düzenleme seçeneklerinin sınırlı olduğunu unutmayın: arka planlar, yerleşimler veya gelişmiş ayarlar mevcut değildir.\",\n    \"description\": \"Pro karşılama modali açıklaması\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Tamamdır\",\n    \"description\": \"Pro karşılama modali eylem düğmesi\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 hata — kapalı\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Screenity Pro uzantısına hoş geldin\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Kayda başlamadan önce birkaç hızlı ipucu.<br/>Otomatik yakınlaştırma yalnızca <strong>Chrome sekmeleri</strong> içindeki tıklamalarda çalışır. Daha sonra manuel olarak yakınlaştırma ekleyebilirsin.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Kayıt araç çubuğu ve efektler\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Bu çubuğu çizim, imleç efektleri, bulanıklaştırma ve kayıt kontrolleri için kullan.<br/><br/><strong>Bu çubuk, gizlemezsen videoda görünür</strong>.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Kamera ayrı kaydedilir\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Kayıt sırasında kamera <strong>gizlenebilir veya PiP'e geçebilir</strong>, bu normaldir.<br/><br/>Daha sonra istediğin gibi konumlandırabilmen için ayrı kaydedilir.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Anlık mod\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Hızlı paylaşım ve <strong>sınırsız indirme</strong> için en iyi seçenek.<br/><br/>Bu modda gelişmiş düzen ve düzenleyici seçenekleri kullanılamaz.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Anladım\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"İleri\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Geri\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Daha fazla bilgi\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Onboarding'i sıfırla\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Disk alanı düşük. Kaydınız şimdi kaydediliyor. Şimdiye kadar yakalanan her şey güvende.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Uzun kayıt\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Kaydınız tamamlandı ve güvende. Aşağıdan WebM olarak indirin.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Uzun kayıtlar için kullanılamıyor\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Kurtarma modunda kullanılamıyor\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Ses düzenlemesi çok uzun\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Bu ses düzenlemesi 15 dakikadan uzun klipler için desteklenmiyor. Orijinaliniz değişmedi. Önce klibi kırpmayı deneyin.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Düzenleme zaman aşımına uğradı\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Düzenleme zamanında tamamlanamadı. Orijinal kaydınız değişmedi. Tekrar deneyin veya önce klibi kırpın.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Ekran paylaşımı durdu. Kaydınız kaydedildi. Hazır olduğunuzda durdurun.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Bir işleme sorunu oluştu, ancak kaydınız güvende. Aşağıdan indirebilirsiniz.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Düzenlemeniz işleniyor. Orijinal kaydınız değiştirilmedi.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Düzenleme uygulanamadı\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"İşleme sırasında bir sorun oluştu. Orijinal kaydınız güvende. Tekrar deneyebilir veya olduğu gibi indirebilirsiniz.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Ekran paylaşımı durdu. Kaydınız şimdi kaydediliyor. Şimdiye kadar yakalanan her şey güvende.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Ses bağlantısı kesildi. Kaydınız şimdi kaydediliyor. Şimdiye kadar yakalanan video güvende.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Kaydınız kaydedildi. Düzenleyici açılmadı. Tekrar denemek için Screenity simgesine tıklayın.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Kaydetme beklenenden uzun sürüyor. Verileriniz güvende. Düzenleyici kısa süre içinde açılacak.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Hata ayıklama bilgisini kopyala\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Hata ayıklama bilgisi kopyalandı\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Yardım al\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Kayıt kaydedilemeyecek kadar kısa\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"H\\u0131zl\\u0131 kay\\u0131t ba\\u015far\\u0131s\\u0131z oldu\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"H\\u0131zl\\u0131 kaydedicinin \\u00e7\\u0131kt\\u0131s\\u0131 bu cihazda do\\u011frulanamad\\u0131.\\nDosyay\\u0131 yine de indirebilirsiniz.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Yine de indir\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"\\u0130ptal\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/uk/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - Запис екрану & Анотації\",\n    \"description\": \"Extension name\"\n  },\n  \"extDesc\": {\n    \"message\": \"Найкращий безкоштовний відеореєстратор екрану без обмежень. Захоплення, анотація, збільшення, розмиття, редагування відео та багато іншого - без необхідності входу та з увагою до конфіденційності.\",\n    \"description\": \"Extension description\"\n  },\n  \"recordTab\": {\n    \"message\": \"Запис\",\n    \"description\": \"Record tab label on popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"Ваші відео\",\n    \"description\": \"Videos tab label on popup\"\n  },\n  \"screenType\": {\n    \"message\": \"Екран\",\n    \"description\": \"Screen recording type label on popup\"\n  },\n  \"tabType\": {\n    \"message\": \"Область вкладки\",\n    \"description\": \"Tab area recording type label on popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"Камера\",\n    \"description\": \"Camera recording type label on popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Макет\",\n    \"description\": \"Mockup recording type label on popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"Запис області користувача вимкнено\",\n    \"description\": \"Custom area recording disabled warning on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"Оновіть версію Chrome до 110+\",\n    \"description\": \"Custom area recording disabled warning description on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"Оновити\",\n    \"description\": \"Custom area recording disabled warning action on popup for Chrome version older than 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"Перевірте ваші дозволи\",\n    \"description\": \"Camera / microphone permissions warning modal title\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Схоже, що Screenity не може отримати доступ до вашої камери або мікрофону. Обов'язково дозвольте доступ, натиснувши на значок камери у панелі адреси.\",\n    \"description\": \"Camera / microphone permissions warning modal description\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"Відхилити\",\n    \"description\": \"Camera / microphone permissions warning modal dismiss button\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"Дозволити доступ до камери\",\n    \"description\": \"Allow camera access button\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"Дозволити доступ до мікрофону\",\n    \"description\": \"Allow microphone access button\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"Натискайте, щоб говорити\",\n    \"description\": \"Push to talk switch label\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"Встановіть область для запису\",\n    \"description\": \"Custom area switch label\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"Перевернути камеру\",\n    \"description\": \"Flip camera switch label\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"Ефекти фону\",\n    \"description\": \"Background effects switch label\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"Почати запис\",\n    \"description\": \"Record button label\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"Початок запису...\",\n    \"description\": \"Record button in progress label\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"Встановіть камеру для запису\",\n    \"description\": \"Record button label when in camera mode and no camera is selected\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"Показати більше опцій\",\n    \"description\": \"Show more options label\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"Включити системний звук\",\n    \"description\": \"Мітка системного аудіо/вкладка\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"Приховати панель інструментів\",\n    \"description\": \"Hide toolbar label\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"Зворотний відлік\",\n    \"description\": \"Countdown label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"Встановити обмеження за часом\",\n    \"description\": \"Alarm label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"Розмиття\",\n    \"description\": \"Blur background label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"Немає\",\n    \"description\": \"No device selected\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"Немає камери\",\n    \"description\": \"No camera selected\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"Немає мікрофону\",\n    \"description\": \"No microphone selected\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"Виберіть джерело\",\n    \"description\": \"Select source placeholder\"\n  },\n  \"offLabel\": {\n    \"message\": \"Вимкнено\",\n    \"description\": \"Off label on dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"Ширина\",\n    \"description\": \"Region width label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"Висота\",\n    \"description\": \"Region height label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"Клацніть, щоб вставити зображення\",\n    \"description\": \"Toast that shows when adding a new image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"Завершити запис\",\n    \"description\": \"Finish recording tooltip\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"Почати запис знову\",\n    \"description\": \"Restart recording tooltip\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"Призупинити запис\",\n    \"description\": \"Pause recording tooltip\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"Продовжити запис\",\n    \"description\": \"Resume recording tooltip\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"Скасувати запис\",\n    \"description\": \"Cancel recording tooltip\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"Перемкнути малювальні інструменти\",\n    \"description\": \"Toggle drawing tools tooltip\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"Перемкнути інструмент розмиття\",\n    \"description\": \"Toggle blur tools tooltip\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"Перемкнути параметри курсора\",\n    \"description\": \"Toggle cursor options tooltip\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"Вимкнути камеру\",\n    \"description\": \"Disable camera tooltip\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"Увімкнути камеру\",\n    \"description\": \"Enable camera tooltip\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"Відсутні дозволи на камеру\",\n    \"description\": \"No camera permissions tooltip\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"Вимкнути мікрофон\",\n    \"description\": \"Disable microphone tooltip\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"Увімкнути мікрофон\",\n    \"description\": \"Enable microphone tooltip\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"Відсутні дозволи на мікрофон\",\n    \"description\": \"No microphone permissions tooltip\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"Вибрати інструмент\",\n    \"description\": \"Select tool tooltip\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"Інструмент ручки\",\n    \"description\": \"Pen tool tooltip\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"Інструмент маркера\",\n    \"description\": \"Highlighter tool tooltip\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"Інструмент гумки\",\n    \"description\": \"Eraser tool tooltip\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"Текстовий інструмент\",\n    \"description\": \"Text tool tooltip\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"Фігурний інструмент\",\n    \"description\": \"Shape tool tooltip\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"Інструмент стрілки\",\n    \"description\": \"Arrow tool tooltip\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"Інструмент зображення\",\n    \"description\": \"Image tool tooltip\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Undo tooltip\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"Повторити\",\n    \"description\": \"Redo tooltip\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"Очистити полотно\",\n    \"description\": \"Clear canvas tooltip\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"Більше кольорів\",\n    \"description\": \"More colors tooltip\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"Товста обводка\",\n    \"description\": \"Thick stroke tooltip\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"Середня обводка\",\n    \"description\": \"Medium stroke tooltip\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"Тонка обводка\",\n    \"description\": \"Thin stroke tooltip\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"Перемкнути режим picture-in-picture\",\n    \"description\": \"Toggle picture-in-picture mode tooltip\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"Перемкнути заливку\",\n    \"description\": \"Toggle fill tooltip\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"Режим малювання\",\n    \"description\": \"Drawing mode toast\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"Режим розмиття\",\n    \"description\": \"Blur mode toast\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"Очистити всі розмиті елементи\",\n    \"description\": \"Clear all blurred elements tooltip\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"Підсвічувати кліки\",\n    \"description\": \"Highlight clicks tooltip\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"Підсвічувати курсор\",\n    \"description\": \"Highlight cursor tooltip\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"Спрямовувати прожектор на курсор\",\n    \"description\": \"Spotlight cursor tooltip\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"Не показувати знову\",\n    \"description\": \"Permissions modal dismiss button\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"Ви впевнені, що хочете перезапустити запис?\",\n    \"description\": \"Restart recording modal title\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"Ваш поточний відеопрогрес буде втрачено.\",\n    \"description\": \"Restart recording modal description\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"Перезапустити запис\",\n    \"description\": \"Restart recording modal restart button\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"Продовжити запис\",\n    \"description\": \"Restart recording modal resume button\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"Ви впевнені, що хочете відхилити запис?\",\n    \"description\": \"Discard recording modal title\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"Ваш поточний відеопрогрес буде втрачено.\",\n    \"description\": \"Discard recording modal description\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"Відхилити запис\",\n    \"description\": \"Discard recording modal discard button\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"Продовжити запис\",\n    \"description\": \"Discard recording modal resume button\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"Виберіть, що ви хочете записати\",\n    \"description\": \"Title in recording page\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"Запис...\",\n    \"description\": \"Title in recording page while recording\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"Залиште цю вкладку відкритою. Вона закриється, як тільки ваш запис буде збережено.\",\n    \"description\": \"Description in recording page\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"Підготовка до запису...\",\n    \"description\": \"Title in sandbox page while recording / saving\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"Залиште цю вкладку відкритою. Вона оновиться з вашим записом, коли він буде готовий.\",\n    \"description\": \"Description in sandbox page while recording / saving\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"Редактор\",\n    \"description\": \"Title in navigation of the sandbox editor page\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Cancel button in sandbox editor page\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"Скасувати зміни\",\n    \"description\": \"Revert button in sandbox editor page\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"Скинути\",\n    \"description\": \"Reset button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"Зберегти зміни\",\n    \"description\": \"Save button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"Збереження...\",\n    \"description\": \"Save button in sandbox editor page while saving\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"Перетягніть ваш аудіофайл\",\n    \"description\": \"Drag and drop audio file in sandbox editor page\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"Або натисніть, щоб переглянути\",\n    \"description\": \"Or click to browse audio file in sandbox editor page\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"Налаштування\",\n    \"description\": \"Audio settings title in sandbox editor page\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"Гучність\",\n    \"description\": \"Audio volume label in sandbox editor page\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"Оновити\",\n    \"description\": \"Update audio button in sandbox editor page\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"Обрізати\",\n    \"description\": \"Crop title in sandbox editor page\"\n  },\n  \"widthLabel\": {\n    \"message\": \"Ширина\",\n    \"description\": \"Width label for properties\"\n  },\n  \"heightLabel\": {\n    \"message\": \"Висота\",\n    \"description\": \"Height label for properties\"\n  },\n  \"leftLabel\": {\n    \"message\": \"Зліва\",\n    \"description\": \"Left label for properties\"\n  },\n  \"topLabel\": {\n    \"message\": \"Зверху\",\n    \"description\": \"Top label for properties\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"Перетягніть ручки та використовуйте кнопки ліворуч для редагування вибраного розділу.\",\n    \"description\": \"Info about trimming in sandbox editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"Обрізати відео\",\n    \"description\": \"Trim button in sandbox editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"Обрізання...\",\n    \"description\": \"Trim button in sandbox editor while trimming\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"Вирізати секцію\",\n    \"description\": \"Cut button in sandbox editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"Вирізання...\",\n    \"description\": \"Cut button in sandbox editor while cutting\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"Вимкнути звук\",\n    \"description\": \"Mute button in sandbox editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"Вимкнення...\",\n    \"description\": \"Mute button in sandbox editor while muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"Дізнайтеся більше.\",\n    \"description\": \"Learn more with a dot\"\n  },\n  \"undoLabel\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Undo label\"\n  },\n  \"redoLabel\": {\n    \"message\": \"Повторити\",\n    \"description\": \"Redo label\"\n  },\n  \"leaveReview\": {\n    \"message\": \"Залиште відгук\",\n    \"description\": \"Leave a review button\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"Слідкуйте за оновленнями\",\n    \"description\": \"Follow for updates button\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"Ви зараз в автономному режимі\",\n    \"description\": \"Offline label\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"Деякі функції недоступні до повторного підключення\",\n    \"description\": \"Offline label description\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"Спробуйте знову\",\n    \"description\": \"Offline label try again button\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Необхідне оновлення Chrome\",\n    \"description\": \"Update Chrome label\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"Оновлення для отримання доступу до більше функцій\",\n    \"description\": \"Update Chrome label description\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"Дізнатися більше\",\n    \"description\": \"Learn more button\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"Відео занадто довге для обробки локально\",\n    \"description\": \"Мітка обмеження понад 5 хвилин у редакторі\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"Редагування та експорт у форматі MP4 недоступні\",\n    \"description\": \"Опис обмеження понад 5 хвилин у редакторі\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"Відео обробляється...\",\n    \"description\": \"Video processing label in editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"Оновлення для швидшого редагування\",\n    \"description\": \"Video processing description in editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"Редагувати\",\n    \"description\": \"Edit title in sandbox editor page\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"Немає з'єднання\",\n    \"description\": \"No connection label\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"Недоступно\",\n    \"description\": \"Not available label\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"Редагувати відео\",\n    \"description\": \"Trim label button\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"Обрізати, вирізати або вимкнути звук у відео\",\n    \"description\": \"Trim label\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"Підготовка...\",\n    \"description\": \"Preparing label\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"Обрізати\",\n    \"description\": \"Crop label button\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"Обрізати та змінити розмір відео\",\n    \"description\": \"Crop label\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"Додати аудіо\",\n    \"description\": \"Add audio label button\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"Завантажте своє аудіо для додавання до відео\",\n    \"description\": \"Add audio label\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"Зберегти\",\n    \"description\": \"Save title in sandbox editor page\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"Вийти з Диска\",\n    \"description\": \"Sign out of Google Drive label\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"Збереження...\",\n    \"description\": \"Saving to Google Drive label\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"Зберегти на Диск\",\n    \"description\": \"Save to Google Drive button\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"Зберегти поточну версію на Диск Google\",\n    \"description\": \"Save to Google Drive label\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"Увійти та зберегти на Диск\",\n    \"description\": \"Sign in to Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"Експорт\",\n    \"description\": \"Export title in sandbox editor page\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"Завантаження...\",\n    \"description\": \"Downloading label\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"Завантажити як .webm\",\n    \"description\": \"Download WEBM button\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"Експорт відео у форматі .webm\",\n    \"description\": \"Download WEBM label\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"Завантажити як .mp4\",\n    \"description\": \"Download MP4 button\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"Експорт відео у форматі .mp4 (рекомендовано)\",\n    \"description\": \"Download MP4 label\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"Завантажити як .gif\",\n    \"description\": \"Download GIF button\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"Експорт у форматі GIF (максимум 30 секунд)\",\n    \"description\": \"Download GIF label\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"Готовий покращити свої записи?\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"Хмарне поширення, зуми, шаблони... і підтримка інструменту від інді-розробниці, яка поважає твою приватність ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"Дізнатися більше про Pro\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"Вже маєш акаунт? Увійти\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"Поділитися відео\",\n    \"description\": \"Share button in sandbox editor page\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"Замінити існуюче аудіо\",\n    \"description\": \"Replace audio button in editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"Збільшити до курсору\",\n    \"description\": \"Zoom to point popup\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"Залишайтеся на сторінці під час запису\",\n    \"description\": \"Stay in page popup\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"Попередження про вимкнений мікрофон\",\n    \"description\": \"Mic reminder popup\"\n  },\n  \"helpPopup\": {\n    \"message\": \"Довідка\",\n    \"description\": \"Help popup button\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"Запис призупинено. Натисніть кнопку відтворення для продовження.\",\n    \"description\": \"Paused recording modal title\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity не має дозволу на запис вашого екрану\",\n    \"description\": \"Permission modal title\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"Щоб увімкнути запис екрану, вам слід дозволити Screenity захоплювати ваш екран, коли це буде запитано.\",\n    \"description\": \"Permission modal description\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"Увімкнути запис екрану\",\n    \"description\": \"Permission modal action\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Permission modal cancel\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"Ваш мікрофон вимкнено\",\n    \"description\": \"Microphone muted modal title\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"Для включення звуку у вашому записі слід розблокувати мікрофон. Бажаєте продовжити без звуку?\",\n    \"description\": \"Microphone muted modal description\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"Так, продовжити\",\n    \"description\": \"Microphone muted modal continue\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Microphone muted modal cancel\"\n  },\n  \"setupTitle\": {\n    \"message\": \"Почніть використовувати Screenity за три простих кроки:\",\n    \"description\": \"Set up steps title\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- Натисніть \",\n    \"description\": \"Set up step 1, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" значок розширення\",\n    \"description\": \"Set up step 1, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- Натисніть \",\n    \"description\": \"Set up step 2, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" значок закріплення\",\n    \"description\": \"Set up step 2, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- Клацніть на \",\n    \"description\": \"Set up step 3, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" значок Screenity, щоб розпочати\",\n    \"description\": \"Set up step 3, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"Чудово! Ви готові\",\n    \"description\": \"Set up complete title\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"Тепер ви можете почати запис тут або в іншій вкладці.\",\n    \"description\": \"Set up complete description\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"Клацніть тут, щоб малювати\",\n    \"description\": \"Click here onboarding\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"Розслабтеся. Клацніть будь-де, щоб зупинити відлік.\",\n    \"description\": \"Countdown message\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"Редагування може бути неточними для коротких проміжків часу через інтервали між кадрами.\",\n    \"description\": \"Info about the editor being too small\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"Відео обробляється, відтворення може бути неточним або перерваним.\",\n    \"description\": \"Processing banner in editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"Обрізка може зайняти деякий час\",\n    \"description\": \"Cropping info title\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"Оновіть для швидшої обробки\",\n    \"description\": \"Cropping info description\"\n  },\n  \"micOnToast\": {\n    \"message\": \"Мікрофон увімкнено\",\n    \"description\": \"Microphone enabled toast title\"\n  },\n  \"micOffToast\": {\n    \"message\": \"Мікрофон вимкнено\",\n    \"description\": \"Microphone disabled toast title\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"Приховати сповіщення інтерфейсу\",\n    \"description\": \"Hide UI alerts button\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"Не показувати знову\",\n    \"description\": \"Don't show again button\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"Приховати панель інструментів при неактивності\",\n    \"description\": \"Only show toolbar on hover button\"\n  },\n  \"stopRecording\": {\n    \"message\": \"Зупинити запис\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"Ласкаво просимо до нового Screenity!\",\n    \"description\": \"Заголовок оголошення про оновлення для існуючих користувачів\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"Ми представляємо новий вигляд та багато цікавих функцій, але не хвилюйтеся, це все ще та сама безкоштовна, конфіденційна та відкрита розширення, яке ви знаєте і любите.\",\n    \"description\": \"Опис оголошення про оновлення для існуючих користувачів\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"Дізнайтеся більше про оновлення.\",\n    \"description\": \"Посилання 'Дізнатися більше' для оголошення про оновлення для існуючих користувачів\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"Почати\",\n    \"description\": \"Кнопка оголошення про оновлення для існуючих користувачів\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"Помилка при запуску запису\",\n    \"description\": \"Заголовок модального вікна помилки потоку\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"Схоже, що виникла помилка при запуску запису. Перезапустіть свій браузер і спробуйте ще раз. Якщо проблема не зникає, будь ласка, зв'яжіться з нами за адресою support@screenity.io.\",\n    \"description\": \"Опис модального вікна помилки потоку\"\n  },\n  \"highestQuality\": {\n    \"message\": \"Найвища якість\",\n    \"description\": \"Позначка перемикача найвищої якості в спливаючому вікні\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"Відновити останнє запис\",\n    \"description\": \"Кнопка відновлення останнього запису в спливаючому вікні\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"Виникли проблеми?\",\n    \"description\": \"Кнопка проблем в редакторі\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"Не бачите свого запису?\",\n    \"description\": \"Заголовок модального вікна проблем в редакторі\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"Якщо ви завершили запис і опинилися на цій сторінці без можливості переглянути своє відео, ви можете спробувати завантажити сиру відеоінформацію, якщо вона доступна. Ви також можете зв'язатися з нами і подати звіт про помилку.\",\n    \"description\": \"Опис модального вікна проблем в редакторі\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"Завантажити сиру відеоінформацію\",\n    \"description\": \"Кнопка в модальному вікні проблем в редакторі\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"Повідомити про помилку\",\n    \"description\": \"Друга кнопка в модальному вікні проблем в редакторі\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"Запис не знайдено, вибачте :(\",\n    \"description\": \"Попередження про відсутність запису в редакторі\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"Досягнуто ліміт пам'яті\",\n    \"description\": \"Заголовок повідомлення про досягнення ліміту пам'яті\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"Ваш пристрій вичерпав доступну пам'ять для запису, що призвело до автоматичної зупинки запису. Якщо ви хочете записувати довше, спробуйте зменшити якість відео або видалити деякі файли на пристрої.\",\n    \"description\": \"Опис повідомлення про досягнення ліміту пам'яті\"\n  },\n  \"understoodButton\": {\n    \"message\": \"Зрозуміло\",\n    \"description\": \"Кнопка 'Зрозуміло'\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"Недостатньо місця\",\n    \"description\": \"Заголовок повідомлення про недостатньо місця\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Для роботи Screenity потрібно принаймні 1 ГБ вільного місця. Будь ласка, звільніть простір на пристрої і спробуйте ще раз.\",\n    \"description\": \"Опис повідомлення про недостатньо місця\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"Видалити попередні записи\",\n    \"description\": \"Кнопка 'Видалити попередні записи'\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"Розширений\",\n    \"description\": \"Розділ Розширений на сторінці редактора в безпечному середовищі\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"Завантажити сирі відеофайли\",\n    \"description\": \"Кнопка завантаження сирого запису\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"Експорт оригінальних даних запису\",\n    \"description\": \"Мітка завантаження сирого запису\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"Отримайте допомогу з вашим записом\",\n    \"description\": \"Кнопка усунення неполадок\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"Відновіть ваше відео та вирішіть інші проблеми\",\n    \"description\": \"Мітка усунення неполадок\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"Завантажити сирі відеофайли\",\n    \"description\": \"Заголовок модального вікна сирого запису\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"Сирі відеофайли - це оригінальний відеофайл, який був записаний Screenity. Цей відеофайл не оброблявся або редагувався, тому він може бути дуже великим і не всі пристрої можуть його програвати. Якщо у вас є проблеми з обробленим відео, ви можете використовувати цей файл для відновлення відео.\",\n    \"description\": \"Опис модального вікна сирого запису\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"Завантажити\",\n    \"description\": \"Кнопка модального вікна сирого запису\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"Завантажте системну інформацію та дані відео для відправки на усунення неполадок\",\n    \"description\": \"Заголовок модального вікна усунення неполадок\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"Після завантаження надішліть файл на адресу support@screenity.io з будь-якою відповідною інформацією. Ми використовуватимемо ці дані для вирішення будь-яких проблем, які у вас виникли з розширенням, і можливо, відновлення вашого відео.\",\n    \"description\": \"Опис модального вікна усунення неполадок\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"Завантажити\",\n    \"description\": \"Кнопка модального вікна усунення неполадок\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"Відео занадто довге для обробки на вашому пристрої\",\n    \"description\": \"Заголовок модального вікна обмеження понад 5 хвилин\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"Оскільки Screenity цілком приватний і працює локально, він обмежений апаратним забезпеченням вашого пристрою. Обробка відео цієї довжини займе дуже багато часу та буде вимагати значних ресурсів. Однак ви все ще можете завантажити файл WEBM або необроблену запису, але можливості редагування та експорту в MP4 недоступні. Так чи інакше, ви можете спробувати обробити відео, якщо розумієте ризики.\",\n    \"description\": \"Опис модального вікна обмеження понад 5 хвилин\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"Я розумію ризики, все одно спробувати\",\n    \"description\": \"Кнопка модального вікна обмеження понад 5 хвилин\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"Дізнайтеся більше про обробку довгих відео.\",\n    \"description\": \"Посилання на додаткову інформацію в модальному вікні обмеження понад 5 хвилин\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"Режим відновлення\",\n    \"description\": \"Заголовок режиму відновлення\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"Завантажити дані для усунення проблем\",\n    \"description\": \"Опція завантаження для усунення проблем\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"Центр допомоги\",\n    \"description\": \"Елемент навігації для допомоги\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"Запис звуку сторінки\",\n    \"description\": \"Заголовок попередження про аудіо\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"Для запису звуку цієї сторінки використовуйте опцію '$tab$'.\",\n    \"description\": \"Опис попередження про аудіо\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Область вкладки\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"Розширення не підтримується на сторінці\",\n    \"description\": \"Заголовок попередження про непідтримку розширення на вкладці\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity не підтримувався на вашій попередній вкладці. Проте ви можете розпочати запис тут, а потім перейти на вкладку, але ваша камера і панель інструментів не будуть видимими.\",\n    \"description\": \"Опис попередження про непідтримку розширення на вкладці\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"Налаштування локального резервного копіювання для ваших записів в Screenity\",\n    \"description\": \"Заголовок резервного копіювання\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"Зберігайте копії всіх ваших записів. Якщо ви не налаштуєте резервне копіювання, ваші записи збережуться лише тоді, коли ви вирішите їх завантажити.\",\n    \"description\": \"Опис резервного копіювання\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"Можливо, вам спершу потрібно створити нову папку, якщо ви намагаєтеся зберегти їх в Завантаження або інші системні папки.\",\n    \"description\": \"Опис резервного копіювання\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"Вибрати папку\",\n    \"description\": \"Кнопка Вибрати папку\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"Не зараз\",\n    \"description\": \"Кнопка Не зараз\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"Ваші записи локально синхронізовані\",\n    \"description\": \"Заголовок активного резервного копіювання\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"Щоб уникнути запитів на дозвіл кожного разу, коли ви записуєте, ми рекомендуємо залишити цю вкладку відкритою.\",\n    \"description\": \"Опис активного резервного копіювання\"\n  },\n  \"backupsClose\": {\n    \"message\": \"Закрити вкладку все одно\",\n    \"description\": \"Кнопка Закрити вкладку все одно\"\n  },\n  \"backupsStop\": {\n    \"message\": \"Зупинити резервне копіювання всіх записів\",\n    \"description\": \"Кнопка Зупинити резервне копіювання\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"Будь ласка, підтвердіть доступ до вашої локальної папки резервного копіювання Screenity\",\n    \"description\": \"Заголовок підтвердження резервного копіювання\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"Вибачте, але нам потрібен ваш дозвіл ще раз, щоб продовжити створювати локальні копії ваших записів. Нажаль, нам доведеться запитувати кожного разу, коли ви закриєте цю вкладку, що може статися, якщо ви закриєте браузер або перезавантажите комп'ютер.\",\n    \"description\": \"Опис підтвердження резервного копіювання\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"Дозволити Screenity продовжувати резервне копіювання записів\",\n    \"description\": \"Кнопка Дозволити в підтвердженні резервного копіювання\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"Створювати резервні копії записів\",\n    \"description\": \"Етикетка перемикача резервного копіювання\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"Дозвіл на створення резервної копії запису не надано\",\n    \"description\": \"Заголовок помилки дозволу на резервне копіювання\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"Ви не вибрали папку для створення локальної копії ваших записів. Залежно від версії вашого браузера або операційної системи, вас можуть попросити вибрати папку кожного разу, коли ви починаєте записувати. Ви можете спробувати ще раз або повністю вимкнути резервне копіювання, якщо ви не хочете надавати дозволи повторно.\",\n    \"description\": \"Опис помилки дозволу на резервне копіювання\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"Запис аудіо комп'ютера\",\n    \"description\": \"Заголовок попередження про аудіо для macOS\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"На macOS ви можете записувати звук лише з вкладок. Виберіть опцію 'Вкладка Chrome' і переконайтеся, що включена опція 'Також ділитися аудіо вкладки'.\",\n    \"description\": \"Опис попередження про аудіо для macOS\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"Запис аудіо комп'ютера\",\n    \"description\": \"Заголовок попередження про аудіо для інших систем\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"Якщо ви хочете записувати звук комп'ютера, переконайтеся, що опція 'Ділитися аудіо системи' активована. Якщо ви не бачите цю опцію, спробуйте оновити Chrome або перейти до опції запису вкладок.\",\n    \"description\": \"Опис попередження про аудіо для інших систем\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"Максимальний роздільна здатність\",\n    \"description\": \"Максимальний підпис роздільної здатності в меню налаштувань\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"Максимальний кадровий кут\",\n    \"description\": \"Максимальний підпис кадрового кута в меню налаштувань\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"Недостатньо ОПЕРАТИВНОЇ ПАМ'ЯТІ\",\n    \"description\": \"Надпис Недостатньо ОПЕРАТИВНОЇ ПАМ'ЯТІ в меню налаштувань\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"Змінити розмір вікна\",\n    \"description\": \"Мітка зміни розміру вікна в меню налаштувань\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"Екран занадто малий для цієї роздільної здатності\",\n    \"description\": \"Підказка про занадто малий екран в меню налаштувань\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"Не можливо записувати у цій роздільній здатності на вашому пристрої\",\n    \"description\": \"Підказка про максимальну роздільну здатність в меню налаштувань\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"Переглянути дозволи\",\n    \"description\": \"Кнопка перегляду дозволів\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"Додати ще один запис\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"Готуємо все до старту...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"Зачекайте! Ваш запис готується.\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"Панель інструментів приховано. Увімкніть її в налаштуваннях спливаючого вікна.\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"Увійти або зареєструватися\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"Вийти\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"Ви вийшли з облікового запису\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"Досягнуто межі сховища\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"Ви використали всі 50 ГБ сховища. Видаліть старі відео або сцени, щоб продовжити запис.\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"Керувати сховищем\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"Закрити\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"Не вдалося перевірити сховище\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"Сталася помилка під час перевірки сховища. Увійдіть знову та спробуйте записати.\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"Не вдалося перевірити квоту сховища. Спробуйте ще раз за кілька секунд або оновіть сторінку.\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"Спробувати ще раз\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"Сцену додано до Multi-запису\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"Готово до запису нової сцени у відео\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"Запис наближається до 90-хвилинної межі, незабаром завершиться\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"Запис зупинено через 90-хвилинне обмеження\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"Запис вкладки вимкнено на цій сторінці. Перемкнено на запис екрана.\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"Запис відео скасовано\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"Посилання скопійовано в буфер обміну\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"Не вдалося скопіювати посилання\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"Останні\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"Найстаріші\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"Усі відео\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"Відео не знайдено\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"Завантаження відео...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"Перейти до панелі\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"Ласкаво просимо до Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"Без реклами. Без стеження. Без обмежень.\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"Просто натисніть «Запис».\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"Користуйтесь безкоштовно — без реєстрації!\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"Хочете зробити більше зі своїми відео?\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"Хмарне сховище, діліться відео через посилання\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"Об’єднуйте кілька кліпів в одну історію\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"Розумне масштабування при кліку (або додайте своє!)\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"Шаблони, субтитри, макети...\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"Користувацькі фони та макети\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 Відкрити додаткові функції\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ Підтримайте розробку від інді-розробниці!\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"Налаштування облікового запису\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"Підтримка\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"Ваша підписка Pro неактивна\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"Ваша підписка на Screenity Pro неактивна.\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"Будь ласка, активуйте знову, щоб відновити доступ.\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"Ваші відео та дані будуть остаточно видалені \",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"Керувати підпискою\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"Хочете продовжити користування безкоштовною версією?\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"Вийти та перейти на безкоштовну версію\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"Ви вийшли з облікового запису\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"Щоб синхронізувати записи з обліковим записом та отримати доступ до преміум-функцій, увійдіть знову.\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"Увійти знову\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"Ви можете користуватися розширенням без облікового запису — але ваші записи не зберігатимуться і не можуть бути відновлені.\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"Продовжити без входу\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"Режим миттєвого запису\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"Миттєве завантаження, але камеру та макет не можна буде змінити пізніше.\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"Режим миттєвого запису\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"Це записує все в одне відео для миттєвого завантаження та поширення. Розташування камери не можна буде змінити, але інші правки будуть доступні.\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"Зрозуміло\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"Запис вкладки вимкнено на цій сторінці\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"Додається до: \",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"Завершити\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"Хочете зробити більше зі своїми записами?\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"Увійдіть, щоб зберігати відео в хмарі, ділитися посиланням і редагувати з розширеними функціями.\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"Увійти, щоб відкрити платні функції\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"Підтримайте розробку від інді-розробниці\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"Відкрити більше функцій\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"Увійти, щоб поділитися (pro)\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"Відкрийте хмарне поширення, багатосценове редагування, автозуми, субтитри та інше\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity завжди буде безкоштовним, з відкритим кодом та без реклами. Pro покриває витрати на хмару та інфраструктуру й підтримує інді-розробницю! ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"Більше не показувати\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"Спробувати\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Режим Multi\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"Записуйте кілька сцен — екран, камеру або обидва варіанти — одну за одною. Це чудово підходить для кількох дублів, зміни ракурсу або поділу запису на частини. Коли завершите, натисніть «Готово», щоб відкрити редактор з усіма сценами, об’єднаними в одне відео.\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"Активуйте Pro, щоб почати\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"Оформіть підписку, щоб отримати доступ до функцій Pro.\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"Підписатися на Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"З поверненням до Screenity\",\n    \"description\": \"Заголовок для вікна привітання для користувачів, які повернулися\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ Нове: Виведіть свої записи на новий рівень\",\n    \"description\": \"Заголовок для функції хмарного зберігання у вікні привітання\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro — це нова додаткова платформа для збереження відео в хмарі, спільного доступу за посиланням та розблокування розширених інструментів редагування, коли і якщо вони вам потрібні.\",\n    \"description\": \"Опис функції хмарного зберігання у вікні привітання\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"Дізнатися більше про Screenity Pro\",\n    \"description\": \"Кнопка заклику до дії для функції хмарного зберігання у вікні привітання\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"Ласкаво просимо до Screenity Pro\",\n    \"description\": \"Заголовок вікна привітання у Pro\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"Усе готово для початку запису! Ось кілька речей, про які варто пам’ятати:\\n\\n- Під час запису бульбашка з камерою може зникати — це нормально. Вона записується окремо у фоновому режимі, тож пізніше ви зможете розмістити її як завгодно.\\n- Збільшення створюються автоматично лише при кліках у вкладках Chrome — це обмеження браузера. Ви завжди можете додати збільшення вручну після запису.\\n- Для швидких записів зі миттєвим завантаженням спробуйте Режим «Миттєво». Просто пам’ятайте, що можливості редагування обмежені: без фону, макетів або розширених налаштувань.\",\n    \"description\": \"Опис вікна привітання у Pro\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"Зрозуміло\",\n    \"description\": \"Кнопка дії у вікні привітання Pro\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"Fast MP4 (beta)\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"Fast MP4 помилка — вимк\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"Ласкаво просимо до розширення Screenity Pro\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"Кілька швидких порад перед записом.<br/>Автозум працює лише для кліків у <strong>вкладках Chrome</strong>. Пізніше ви завжди можете додати зуми вручну.\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"Панель запису та ефекти\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"Використовуйте цю панель для малювання, ефектів курсора, розмиття та керування записом.<br/><br/><strong>Ця панель буде у відео</strong>, якщо її не приховати.\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"Камера записується окремо\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"Під час запису камера може <strong>зникати або переходити в PiP</strong> - це нормально.<br/><br/>Вона записується окремо, щоб ви могли змінити її позицію пізніше.\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"Миттєвий режим\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"Найкраще для швидкого поширення з <strong>необмеженими завантаженнями</strong>.<br/><br/>У цьому режимі недоступні розширені макети й опції редактора.\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"Зрозуміло\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"Далі\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"Назад\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"Дізнатися більше\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"Скинути онбординг\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"Мало місця на диску. Зберігаємо запис. Все, що було записано, у безпеці.\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"Довгий запис\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"Ваш запис завершено і він у безпеці. Завантажте у форматі WebM нижче.\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"Недоступно для довгих записів\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"Недоступно в режимі відновлення\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"Редагування аудіо занадто довге\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"Це редагування аудіо не підтримується для кліпів довших за 15 хвилин. Оригінал не змінено. Спробуйте спочатку обрізати кліп.\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"Час редагування вичерпано\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"Редагування не завершилось вчасно. Оригінальний запис не змінено. Спробуйте ще раз або спочатку обріжте кліп.\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"Демонстрацію екрана зупинено. Ваш запис збережено. Зупиніть, коли будете готові.\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"Виникла помилка обробки, але ваш запис у безпеці. Ви можете завантажити його нижче.\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"Обробка редагування. Оригінальний запис не змінено.\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"Не вдалося застосувати редагування\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"Щось пішло не так під час обробки. Оригінальний запис у безпеці. Ви можете спробувати знову або завантажити як є.\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"Демонстрацію екрана зупинено. Зберігаємо запис. Все, що було записано, у безпеці.\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"Аудіо від'єднано. Зберігаємо запис. Записане відео у безпеці.\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"Запис збережено. Редактор не відкрився. Натисніть на значок Screenity, щоб спробувати знову.\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"Збереження триває довше, ніж очікувалось. Ваші дані у безпеці. Редактор незабаром відкриється.\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"Скопіювати дані відлагодження\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"Дані відлагодження скопійовано\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"Отримати допомогу\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"Запис занадто короткий для збереження\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"Швидкий запис не вдався\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"Результат швидкого запису не вдалося перевірити на цьому пристрої.\\nВи все одно можете завантажити файл.\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"Завантажити все одно\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"Скасувати\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh_CN/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - 屏幕录像和标注工具\",\n    \"description\": \"Extension name\"\n  },\n  \"extDesc\": {\n    \"message\": \"最佳免费无限制屏幕录制工具。捕捉、标注、缩放、模糊、编辑视频等功能 - 无需登录，保护隐私。\",\n    \"description\": \"Extension description\"\n  },\n  \"recordTab\": {\n    \"message\": \"录制\",\n    \"description\": \"Record tab label on popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"您的视频\",\n    \"description\": \"Videos tab label on popup\"\n  },\n  \"screenType\": {\n    \"message\": \"屏幕\",\n    \"description\": \"Screen recording type label on popup\"\n  },\n  \"tabType\": {\n    \"message\": \"标签区域\",\n    \"description\": \"Tab area recording type label on popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"摄像头\",\n    \"description\": \"Camera recording type label on popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"模拟\",\n    \"description\": \"Mockup recording type label on popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"自定义区域录制已禁用\",\n    \"description\": \"Custom area recording disabled warning on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"请更新Chrome版本至110+\",\n    \"description\": \"Custom area recording disabled warning description on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"更新\",\n    \"description\": \"Custom area recording disabled warning action on popup for Chrome version older than 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"请检查您的权限\",\n    \"description\": \"Camera / microphone permissions warning modal title\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"看起来Screenity无法访问您的摄像头或麦克风。请确保通过单击地址栏中的摄像机图标允许访问。\",\n    \"description\": \"Camera / microphone permissions warning modal description\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"忽略\",\n    \"description\": \"Camera / microphone permissions warning modal dismiss button\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"允许摄像头访问\",\n    \"description\": \"Allow camera access button\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"允许麦克风访问\",\n    \"description\": \"Allow microphone access button\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"按住说话\",\n    \"description\": \"Push to talk switch label\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"设置要录制的区域\",\n    \"description\": \"Custom area switch label\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"翻转摄像头\",\n    \"description\": \"Flip camera switch label\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"背景效果\",\n    \"description\": \"Background effects switch label\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"开始录制\",\n    \"description\": \"Record button label\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"正在开始录制...\",\n    \"description\": \"Record button in progress label\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"设置摄像头以开始录制\",\n    \"description\": \"Record button label when in camera mode and no camera is selected\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"显示更多选项\",\n    \"description\": \"Show more options label\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"包括系统音频\",\n    \"description\": \"选项卡/系统音频标签\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"隐藏工具栏\",\n    \"description\": \"Hide toolbar label\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"倒计时\",\n    \"description\": \"Countdown label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"设置时间限制\",\n    \"description\": \"Alarm label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"模糊背景\",\n    \"description\": \"Blur background label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"无\",\n    \"description\": \"No device selected\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"无摄像头\",\n    \"description\": \"No camera selected\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"无麦克风\",\n    \"description\": \"No microphone selected\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"选择源\",\n    \"description\": \"Select source placeholder\"\n  },\n  \"offLabel\": {\n    \"message\": \"关闭\",\n    \"description\": \"Off label on dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"宽度\",\n    \"description\": \"Region width label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"高度\",\n    \"description\": \"Region height label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"单击放置图像\",\n    \"description\": \"Toast that shows when adding a new image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"结束录制\",\n    \"description\": \"Finish recording tooltip\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"重新开始录制\",\n    \"description\": \"Restart recording tooltip\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"暂停录制\",\n    \"description\": \"Pause recording tooltip\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"恢复录制\",\n    \"description\": \"Resume recording tooltip\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"取消录制\",\n    \"description\": \"Cancel recording tooltip\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"切换绘图工具\",\n    \"description\": \"Toggle drawing tools tooltip\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"切换模糊工具\",\n    \"description\": \"Toggle blur tools tooltip\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"切换光标选项\",\n    \"description\": \"Toggle cursor options tooltip\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"禁用摄像头\",\n    \"description\": \"Disable camera tooltip\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"启用摄像头\",\n    \"description\": \"Enable camera tooltip\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"无摄像头权限\",\n    \"description\": \"No camera permissions tooltip\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"禁用麦克风\",\n    \"description\": \"Disable microphone tooltip\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"启用麦克风\",\n    \"description\": \"Enable microphone tooltip\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"无麦克风权限\",\n    \"description\": \"No microphone permissions tooltip\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"选择\",\n    \"description\": \"Select tool tooltip\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"钢笔\",\n    \"description\": \"Pen tool tooltip\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"荧光笔\",\n    \"description\": \"Highlighter tool tooltip\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"橡皮擦\",\n    \"description\": \"Eraser tool tooltip\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"文本\",\n    \"description\": \"Text tool tooltip\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"形状工具\",\n    \"description\": \"Shape tool tooltip\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"箭头\",\n    \"description\": \"Arrow tool tooltip\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"图像\",\n    \"description\": \"Image tool tooltip\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"撤销\",\n    \"description\": \"Undo tooltip\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"恢复\",\n    \"description\": \"Redo tooltip\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"清空画布\",\n    \"description\": \"Clear canvas tooltip\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"更多颜色\",\n    \"description\": \"More colors tooltip\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"粗画笔\",\n    \"description\": \"Thick stroke tooltip\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"中画笔\",\n    \"description\": \"Medium stroke tooltip\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"细画笔\",\n    \"description\": \"Thin stroke tooltip\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"切换画中画模式\",\n    \"description\": \"Toggle picture-in-picture mode tooltip\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"切换填充\",\n    \"description\": \"Toggle fill tooltip\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"绘图模式\",\n    \"description\": \"Drawing mode toast\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"模糊模式\",\n    \"description\": \"Blur mode toast\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"清除所有模糊元素\",\n    \"description\": \"Clear all blurred elements tooltip\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"突出显示点击\",\n    \"description\": \"Highlight clicks tooltip\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"突出显示光标\",\n    \"description\": \"Highlight cursor tooltip\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"聚焦光标\",\n    \"description\": \"Spotlight cursor tooltip\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"不再显示\",\n    \"description\": \"Permissions modal dismiss button\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"确定要重新开始录制吗？\",\n    \"description\": \"Restart recording modal title\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"您当前的视频进度将丢失。\",\n    \"description\": \"Restart recording modal description\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"重新开始录制\",\n    \"description\": \"Restart recording modal restart button\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"继续录制\",\n    \"description\": \"Restart recording modal resume button\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"确定要放弃录制吗？\",\n    \"description\": \"Discard recording modal title\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"您当前的视频进度将丢失。\",\n    \"description\": \"Discard recording modal description\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"放弃录制\",\n    \"description\": \"Discard recording modal discard button\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"继续录制\",\n    \"description\": \"Discard recording modal resume button\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"选择要录制的内容\",\n    \"description\": \"Title in recording page\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"录制中...\",\n    \"description\": \"Title in recording page while recording\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"保持此选项卡打开。一旦保存录制，它将关闭。\",\n    \"description\": \"Description in recording page\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"准备录制中...\",\n    \"description\": \"Title in sandbox page while recording / saving\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"保持此选项卡打开。录制/保存完成后，它将更新。\",\n    \"description\": \"Description in sandbox page while recording / saving\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"编辑器\",\n    \"description\": \"Title in navigation of the sandbox editor page\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"取消\",\n    \"description\": \"Cancel button in sandbox editor page\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"恢复原始状态\",\n    \"description\": \"Revert button in sandbox editor page\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"重置\",\n    \"description\": \"Reset button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"保存更改\",\n    \"description\": \"Save button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"保存中...\",\n    \"description\": \"Save button in sandbox editor page while saving\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"拖放音频文件\",\n    \"description\": \"Drag and drop audio file in sandbox editor page\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"或点击浏览\",\n    \"description\": \"Or click to browse audio file in sandbox editor page\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"设置\",\n    \"description\": \"Audio settings title in sandbox editor page\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"音量\",\n    \"description\": \"Audio volume label in sandbox editor page\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"更新\",\n    \"description\": \"Update audio button in sandbox editor page\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"裁剪\",\n    \"description\": \"Crop title in sandbox editor page\"\n  },\n  \"widthLabel\": {\n    \"message\": \"宽度\",\n    \"description\": \"Width label for properties\"\n  },\n  \"heightLabel\": {\n    \"message\": \"高度\",\n    \"description\": \"Height label for properties\"\n  },\n  \"leftLabel\": {\n    \"message\": \"左\",\n    \"description\": \"Left label for properties\"\n  },\n  \"topLabel\": {\n    \"message\": \"顶部\",\n    \"description\": \"Top label for properties\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"拖动手柄并使用左侧的按钮编辑所选部分。\",\n    \"description\": \"Info about trimming in sandbox editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"修剪视频\",\n    \"description\": \"Trim button in sandbox editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"正在修剪...\",\n    \"description\": \"Trim button in sandbox editor while trimming\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"剪切部分\",\n    \"description\": \"Cut button in sandbox editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"正在剪切...\",\n    \"description\": \"Cut button in sandbox editor while cutting\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"静音音频\",\n    \"description\": \"Mute button in sandbox editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"正在静音...\",\n    \"description\": \"Mute button in sandbox editor while muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"了解更多。\",\n    \"description\": \"Learn more with a dot\"\n  },\n  \"undoLabel\": {\n    \"message\": \"撤销\",\n    \"description\": \"Undo label\"\n  },\n  \"redoLabel\": {\n    \"message\": \"恢复\",\n    \"description\": \"Redo label\"\n  },\n  \"leaveReview\": {\n    \"message\": \"发表评论\",\n    \"description\": \"Leave a review button\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"关注获取更新\",\n    \"description\": \"Follow for updates button\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"您当前处于离线状态\",\n    \"description\": \"Offline label\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"某些功能在重新连接之前不可用\",\n    \"description\": \"Offline label description\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"重试\",\n    \"description\": \"Offline label try again button\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome 需要更新\",\n    \"description\": \"Update Chrome label\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"更新以访问更多功能\",\n    \"description\": \"Update Chrome label description\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"了解更多\",\n    \"description\": \"Learn more button\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"视频过长，无法在本地处理\",\n    \"description\": \"编辑器中超过5分钟的限制标签\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"编辑和MP4格式导出不可用\",\n    \"description\": \"编辑器中超过5分钟的限制描述\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"视频正在处理中...\",\n    \"description\": \"Video processing label in editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"升级以加快编辑速度\",\n    \"description\": \"Video processing description in editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"编辑\",\n    \"description\": \"Edit title in sandbox editor page\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"无连接\",\n    \"description\": \"No connection label\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"不可用\",\n    \"description\": \"Not available label\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"编辑视频\",\n    \"description\": \"Trim label button\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"剪辑、裁剪或静音视频\",\n    \"description\": \"Trim label\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"正在准备...\",\n    \"description\": \"Preparing label\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"裁剪\",\n    \"description\": \"Crop label button\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"裁剪和调整视频大小\",\n    \"description\": \"Crop label\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"添加音频\",\n    \"description\": \"Add audio label button\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"上传您自己的音频以添加到视频\",\n    \"description\": \"Add audio label\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"保存\",\n    \"description\": \"Save title in sandbox editor page\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"从 Drive 注销\",\n    \"description\": \"Sign out of Google Drive label\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"保存中...\",\n    \"description\": \"Saving to Google Drive label\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"保存到 Drive\",\n    \"description\": \"Save to Google Drive button\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"将当前版本保存到 Google Drive\",\n    \"description\": \"Save to Google Drive label\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"登录并保存到 Drive\",\n    \"description\": \"Sign in to Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"导出\",\n    \"description\": \"Export title in sandbox editor page\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"下载中...\",\n    \"description\": \"Downloading label\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"下载为 .webm\",\n    \"description\": \"Download WEBM button\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"导出为 .webm 视频\",\n    \"description\": \"Download WEBM label\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"下载为 .mp4\",\n    \"description\": \"Download MP4 button\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"导出为 .mp4 视频（推荐）\",\n    \"description\": \"Download MP4 label\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"下载为 .gif\",\n    \"description\": \"Download GIF button\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"导出为 GIF（最长30秒）\",\n    \"description\": \"Download GIF label\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"准备好提升你的录屏了吗？\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"云端分享、缩放、模板功能… 支持一位注重隐私的独立女性开发者 ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"了解 Pro 版\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"已有账号？登录\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"分享视频\",\n    \"description\": \"Share button in sandbox editor page\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"替换现有音频\",\n    \"description\": \"Replace audio button in editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"缩放到光标位置\",\n    \"description\": \"Zoom to point popup\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"录制时保持在页面中\",\n    \"description\": \"Stay in page popup\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"麦克风已关闭警告\",\n    \"description\": \"Mic reminder popup\"\n  },\n  \"helpPopup\": {\n    \"message\": \"帮助\",\n    \"description\": \"Help popup button\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"录制已暂停。按播放按钮继续。\",\n    \"description\": \"Paused recording modal title\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity 没有权限录制您的屏幕\",\n    \"description\": \"Permission modal title\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"为启用屏幕录制，您必须在提示时授予 Screenity 捕获屏幕的权限。\",\n    \"description\": \"Permission modal description\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"启用屏幕录制\",\n    \"description\": \"Permission modal action\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"取消\",\n    \"description\": \"Permission modal cancel\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"您的麦克风已静音\",\n    \"description\": \"Microphone muted modal title\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"要在录制中包含音频，您需要解除麦克风的静音。是否继续不使用音频？\",\n    \"description\": \"Microphone muted modal description\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"是的，继续\",\n    \"description\": \"Microphone muted modal continue\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"取消\",\n    \"description\": \"Microphone muted modal cancel\"\n  },\n  \"setupTitle\": {\n    \"message\": \"在三个简单步骤中开始使用Screenity：\",\n    \"description\": \"Set up steps title\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- 点击 \",\n    \"description\": \"Set up step 1, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" 扩展图标\",\n    \"description\": \"Set up step 1, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- 按下 \",\n    \"description\": \"Set up step 2, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" 固定图标\",\n    \"description\": \"Set up step 2, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- 点击 \",\n    \"description\": \"Set up step 3, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity图标以启动\",\n    \"description\": \"Set up step 3, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"太棒了！您已经准备好了\",\n    \"description\": \"Set up complete title\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"您现在可以在这里开始录制，或在任何其他标签中。\",\n    \"description\": \"Set up complete description\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"单击此处进行绘制\",\n    \"description\": \"Click here onboarding\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"放松。单击任何地方停止倒计时。\",\n    \"description\": \"Countdown message\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"由于帧之间的间隔，对于短时间范围，编辑可能不准确。\",\n    \"description\": \"Info about the editor being too small\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"视频正在处理，播放可能不准确或中断。\",\n    \"description\": \"Processing banner in the editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"裁剪可能需要一些时间\",\n    \"description\": \"Cropping info title\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"升级以获得更快的处理速度\",\n    \"description\": \"Cropping info description\"\n  },\n  \"micOnToast\": {\n    \"message\": \"麦克风已启用\",\n    \"description\": \"Microphone enabled toast title\"\n  },\n  \"micOffToast\": {\n    \"message\": \"麦克风已禁用\",\n    \"description\": \"Microphone disabled toast title\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"隐藏UI通知\",\n    \"description\": \"Hide UI alerts button\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"不再显示\",\n    \"description\": \"Don't show again button\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"在不使用时隐藏工具栏\",\n    \"description\": \"Only show toolbar on hover button\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"欢迎使用新版 Screenity！\",\n    \"description\": \"现有用户的更新公告标题\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"我们进行了设计更新并增加了许多新功能，但请放心，它仍然是您熟悉和喜爱的免费、私密且开源的扩展。\",\n    \"description\": \"现有用户的更新公告描述\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"了解有关更新的更多信息。\",\n    \"description\": \"现有用户的更新公告了解更多链接\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"开始使用\",\n    \"description\": \"现有用户的更新公告按钮\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"启动录制时出错\",\n    \"description\": \"流错误模态框标题\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"看起来启动录制时出现了错误。请重新启动您的浏览器并重试。如果问题仍然存在，请通过 support@screenity.io 与我们联系。\",\n    \"description\": \"流错误模态框描述\"\n  },\n  \"highestQuality\": {\n    \"message\": \"最高质量\",\n    \"description\": \"弹出窗口中的最高质量标签\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"恢复最近的录音\",\n    \"description\": \"弹出窗口中恢复最近的录音按钮\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"遇到问题了吗？\",\n    \"description\": \"编辑器中的问题按钮\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"无法查看您的录像？\",\n    \"description\": \"编辑器中的问题模态框标题\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"如果您已经完成录制并发现自己在此页面上看不到您的视频，您可以尝试下载原始视频数据（如果可用）。您还可以联系我们并提交错误报告。\",\n    \"description\": \"编辑器中的问题模态框描述\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"下载原始视频数据\",\n    \"description\": \"编辑器中的问题模态框按钮\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"报告问题\",\n    \"description\": \"编辑器中的问题模态框第二个按钮\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"未找到录制，抱歉 :(\",\n    \"description\": \"编辑器中的未找到录制警告\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"内存限制达到\",\n    \"description\": \"内存限制达到标题\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"您的设备已经用尽了可用内存进行录制，导致录制自动停止。如果您想要录制更长时间，可以尝试降低视频质量或清理设备上的一些空间。\",\n    \"description\": \"内存限制达到描述\"\n  },\n  \"understoodButton\": {\n    \"message\": \"知道了\",\n    \"description\": \"知道了按钮\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"空间不足\",\n    \"description\": \"空间不足标题\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity需要至少1GB的空闲空间进行录制。请释放设备上的空间并重试。\",\n    \"description\": \"空间不足描述\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"清除之前的录制\",\n    \"description\": \"清除之前的录制按钮\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"高级\",\n    \"description\": \"沙盒编辑器页面的高级部分\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"下载原始视频文件\",\n    \"description\": \"下载原始录制按钮\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"导出原始录制数据\",\n    \"description\": \"下载原始录制标签\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"获取有关您的录制的帮助\",\n    \"description\": \"故障排除按钮\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"恢复您的视频并解决其他问题\",\n    \"description\": \"故障排除标签\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"下载原始视频文件\",\n    \"description\": \"原始录制模态框标题\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"原始视频文件是由Screenity录制的原始视频文件。此视频未经处理或编辑，因此可能非常大且可能无法在所有设备上播放。如果您在处理后的视频方面遇到问题，可以使用此文件恢复您的视频。\",\n    \"description\": \"原始录制模态框描述\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"下载\",\n    \"description\": \"原始录制模态框按钮\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"下载系统信息和视频数据并发送进行故障排除\",\n    \"description\": \"故障排除模态框标题\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"下载后，请将文件发送至support@screenity.io，附上任何相关信息。我们将使用此数据帮助您解决扩展程序的任何问题，并在可能的情况下恢复您的视频。\",\n    \"description\": \"故障排除模态框描述\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"下载\",\n    \"description\": \"故障排除模态框按钮\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"视频过长，无法在您的设备上处理\",\n    \"description\": \"超过5分钟限制的模态框标题\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"由于Screenity完全私密且在本地运行，它受到您设备硬件的限制。处理这样长度的视频需要很长时间，而且资源消耗很大。但您仍然可以下载WEBM文件或原始录制文件，但无法进行编辑和MP4导出。尽管如此，如果您了解风险，仍然可以尝试处理该视频。\",\n    \"description\": \"超过5分钟限制的模态框描述\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"我理解风险，仍然尝试\",\n    \"description\": \"超过5分钟限制的模态框按钮\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"了解有关处理长视频的更多信息。\",\n    \"description\": \"超过5分钟限制的模态框了解更多链接（带有句号）\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"恢复模式\",\n    \"description\": \"恢复模式标题\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"下载故障排除数据\",\n    \"description\": \"故障排除选项\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"帮助中心\",\n    \"description\": \"帮助导航元素\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"录制页面音频\",\n    \"description\": \"音频警告标题\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"要录制此页面的音频，请使用'$tab$'选项。\",\n    \"description\": \"音频警告说明\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"选项卡区域\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"页面不支持扩展\",\n    \"description\": \"选项卡上不支持的扩展标题\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity在您之前的选项卡上不受支持。尽管如此，您可以在此开始录制，然后切换到选项卡，但您的摄像头和工具栏将不可见。\",\n    \"description\": \"选项卡上不支持扩展的说明\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"为Screenity录制设置本地备份\",\n    \"description\": \"备份标题\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"保留您进行的所有录制的备份。如果您不设置备份，只有在决定下载它们时才会保存您的录制。\",\n    \"description\": \"备份说明\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"如果您尝试保存到下载或其他系统文件夹，则可能需要首先创建一个新文件夹。\",\n    \"description\": \"备份说明\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"选择文件夹\",\n    \"description\": \"选择文件夹按钮\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"现在不\",\n    \"description\": \"现在不按钮\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"您的录制已本地同步\",\n    \"description\": \"已启用的备份标题\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"为了避免每次录制时都请求权限，我们建议保持此选项卡打开。\",\n    \"description\": \"已启用的备份说明\"\n  },\n  \"backupsClose\": {\n    \"message\": \"仍然关闭选项卡\",\n    \"description\": \"仍然关闭选项卡按钮\"\n  },\n  \"backupsStop\": {\n    \"message\": \"停止所有录制的备份\",\n    \"description\": \"停止备份按钮\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"请重新确认对Screenity本地备份文件夹的访问权限\",\n    \"description\": \"备份确认标题\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"很抱歉，我们需要再次获得您的许可，以继续创建您的录制的本地副本。不幸的是，每次关闭此选项卡时，我们都需要询问，这可能会在您关闭浏览器或重新启动计算机时发生。\",\n    \"description\": \"备份确认说明\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"允许Screenity继续备份录制\",\n    \"description\": \"备份确认中的允许按钮\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"备份录制\",\n    \"description\": \"备份切换标签\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"未授予备份录制的权限\",\n    \"description\": \"备份权限失败标题\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"您未选择要创建您的录制的本地副本的文件夹。根据浏览器或操作系统的版本，每次开始录制时可能会要求您选择文件夹。您可以再次尝试或完全禁用备份，如果您不想重复授予权限。\",\n    \"description\": \"备份权限失败说明\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"录制计算机音频\",\n    \"description\": \"macOS音频警告标题\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"在macOS上，您只能录制选项卡的音频。请选择'Chrome选项卡'选项，并确保启用'同时共享选项卡音频'选项。\",\n    \"description\": \"macOS音频警告说明\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"录制计算机音频\",\n    \"description\": \"其他系统音频警告标题\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"如果您想录制计算机音频，请确保启用'共享系统音频'选项。如果您看不到此选项，请尝试更新Chrome或切换到选项卡录制选项。\",\n    \"description\": \"其他系统音频警告说明\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"最高分辨率\",\n    \"description\": \"设置菜单中的最高分辨率标签\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"最高帧率\",\n    \"description\": \"设置菜单中的最高帧率标签\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"内存不足\",\n    \"description\": \"设置菜单中的内存不足标签\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"调整窗口大小\",\n    \"description\": \"设置菜单中的调整窗口大小标签\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"屏幕尺寸不足以支持此分辨率\",\n    \"description\": \"设置菜单中的屏幕尺寸不足提示\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"您的设备无法以此分辨率录制\",\n    \"description\": \"设置菜单中的最大分辨率提示\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"审查权限\",\n    \"description\": \"审查权限按钮\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"添加另一段录制\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"准备中…\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"请稍等，正在准备录制。\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"工具栏已隐藏。可在弹出窗口中重新启用。\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"登录或注册\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"退出登录\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"你已退出登录\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"已达存储上限\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"你已使用完全部 100 GB 存储空间。请删除旧的视频或片段以继续录制。\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"管理存储空间\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"关闭\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"无法检查存储空间\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"检查存储空间时出现问题。请重新登录并尝试录制。\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"我们无法验证你的存储额度。请稍后再试或刷新页面。\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"重试\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"片段已添加至 Multi 录制\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"准备好录制视频中的新片段\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"录制即将达到 90 分钟上限，将很快停止\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"录制已因超过 90 分钟限制而停止\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"此页面已禁用标签页录制，已默认切换为屏幕录制。\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"视频录制已取消\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"链接已复制到剪贴板\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"复制链接失败\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"最新\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"最早\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"所有视频\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"未找到任何视频\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"正在加载视频…\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"前往控制面板\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"欢迎使用 Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"无广告，无追踪，无限制。\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"只需点击录制。\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"免费使用 — 无需注册！\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"想让你的视频更强大？\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"云端存储，一键分享链接\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"将多个片段合成一个故事\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"点击智能缩放（或自定义缩放）\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"添加模板、字幕、布局……\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"自定义背景和样机\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 解锁更多功能\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ 支持独立开发者的持续开发！\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"账户设置\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"支持\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"你的 Pro 订阅已失效\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"你的 Screenity Pro 订阅已失效。\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"请重新激活以恢复访问权限。\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"你的所有视频和数据将于以下日期被永久删除：\",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"管理订阅\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"想继续使用免费版？\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"退出并切换为免费版\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"你已退出登录\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"要将录制内容同步到你的账户并访问高级功能，请重新登录。\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"重新登录\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"你可以在未登录状态下继续使用扩展程序，但录制将不会保存，也无法恢复。\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"继续使用（无需登录）\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"即时录制模式\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"可即时下载，但摄像头和布局之后无法编辑。\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"即时录制模式\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"会将所有内容录制成一个视频以便即时下载和分享。你将无法在之后更改摄像头布局，但仍可进行其他编辑。\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"知道了\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"此页面禁用了标签页录制\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"正在添加至：\",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"完成\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"想充分利用你的录制内容？\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"登录即可将视频保存到云端、生成分享链接并使用高级编辑功能。\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"登录以解锁付费功能\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"支持独立开发者的持续开发\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"解锁更多功能\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"登录后分享（Pro）\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"解锁云端分享、多片段编辑、自动缩放、字幕等功能\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity 永远免费、开源且无广告。Pro 帮助我们承担云服务和基础设施成本，并支持独立开发者的持续开发！❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"不再显示\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"试试看\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Multi 模式\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"连续录制多个场景，例如你的屏幕、摄像头或两者兼有。非常适合多次拍摄、切换视角或将录制内容分段。完成后，点击“完成”以打开编辑器，所有场景将合并为一个视频。\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"启用 Pro 开始使用\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"请订阅以访问 Pro 功能。\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"订阅 Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"本地使用 Screenity？欢迎支持后续开发！\",\n    \"description\": \"在编辑器中自托管横幅标题\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity 是由一位独立女性开发者打造和维护的。自托管是免费的，但如果你觉得它对你有帮助，欢迎通过 Pro 版支持她的开发工作 ❤️\",\n    \"description\": \"在编辑器中自托管横幅描述\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"欢迎回到 Screenity\",\n    \"description\": \"返回用户欢迎弹窗的标题\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ 新功能：让录制更进一步\",\n    \"description\": \"欢迎弹窗中云存储功能的标题\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro 是一个可选的新平台，用于将视频保存到云端、通过链接分享，并在需要时解锁高级编辑工具。\",\n    \"description\": \"欢迎弹窗中云存储功能的描述\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"了解 Screenity Pro\",\n    \"description\": \"欢迎弹窗中云存储功能的号召性按钮\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"欢迎使用 Screenity Pro\",\n    \"description\": \"Pro 欢迎弹窗标题\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"你已准备好开始录制！以下是一些注意事项：\\n\\n- 录制时相机气泡可能会消失，这是正常现象。它会在后台单独捕捉，因此你可以稍后自由调整其位置。\\n- 缩放功能仅在点击 Chrome 标签页时自动创建，这是浏览器的限制。你可以在录制后手动添加更多缩放。\\n- 想要快速录制并立即下载？可以尝试即时模式。但请注意，编辑功能有限：无法使用背景、布局或高级调整。\",\n    \"description\": \"Pro 欢迎弹窗描述\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"知道了\",\n    \"description\": \"Pro 欢迎弹窗确认按钮\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"极速 MP4（beta）\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"极速 MP4 失败 — 关闭\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"欢迎使用 Screenity Pro 扩展\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"开始录制前，先看几个快速提示。<br/>自动缩放仅对 <strong>Chrome 标签页</strong> 内的点击生效。你之后仍可手动添加缩放。\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"录制工具栏与效果\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"可用此工具栏进行绘制、光标效果、模糊和录制控制。<br/><br/><strong>若不隐藏，该工具栏会出现在视频中</strong>。\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"摄像头会单独采集\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"录制时摄像头可能会<strong>隐藏或切换到 PiP</strong>，这是正常现象。<br/><br/>它会被单独采集，方便你之后调整位置。\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"即时模式\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"适合快速分享，支持<strong>不限次数下载</strong>。<br/><br/>该模式下无法使用高级布局和编辑选项。\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"知道了\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"下一步\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"返回\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"了解更多\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"重置引导\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"磁盘空间不足，正在保存录制内容。已录制的内容是安全的。\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"长录制\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"您的录制已完成且安全。请在下方下载 WebM 文件。\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"长录制不可用\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"恢复模式下不可用\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"音频编辑内容过长\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"此音频编辑不支持超过15分钟的片段。原始文件未被修改。请先尝试裁剪片段。\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"编辑超时\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"编辑未能及时完成。原始录制内容未被修改。请重试，或先裁剪片段。\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"屏幕共享已停止。您的录制内容已保存，准备好后即可停止。\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"处理时出现问题，但您的录制内容是安全的。您可以在下方下载。\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"正在处理编辑。原始录制内容未受影响。\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"无法应用编辑\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"处理过程中出现问题。原始录制内容是安全的。您可以重试或直接下载。\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"屏幕共享已停止，正在保存录制内容。已录制的内容是安全的。\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"音频已断开，正在保存录制内容。已录制的视频是安全的。\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"录制内容已保存。编辑器未打开，请点击 Screenity 图标重试。\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"保存时间比预期长。您的数据是安全的，编辑器即将打开。\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"复制调试信息\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"调试信息已复制\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"获取帮助\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"录制时间太短，无法保存\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"快速录制器失败\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"快速录制器的输出无法在此设备上验证。\\n您仍可以下载该文件。\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"仍然下载\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"取消\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/_locales/zh_TW/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Screenity - 螢幕錄製與標註工具\",\n    \"description\": \"Extension name\"\n  },\n  \"extDesc\": {\n    \"message\": \"免費且保護隱私的螢幕錄製工具，沒有任何限制。錄製、標註、編輯影片等等 - 全部無需註冊。\",\n    \"description\": \"Extension description\"\n  },\n  \"recordTab\": {\n    \"message\": \"錄製\",\n    \"description\": \"Record tab label on popup\"\n  },\n  \"videosTab\": {\n    \"message\": \"您的影片\",\n    \"description\": \"Videos tab label on popup\"\n  },\n  \"screenType\": {\n    \"message\": \"螢幕\",\n    \"description\": \"Screen recording type label on popup\"\n  },\n  \"tabType\": {\n    \"message\": \"分頁區域\",\n    \"description\": \"Tab area recording type label on popup\"\n  },\n  \"cameraType\": {\n    \"message\": \"攝影機\",\n    \"description\": \"Camera recording type label on popup\"\n  },\n  \"mockupType\": {\n    \"message\": \"Mockup\",\n    \"description\": \"Mockup recording type label on popup\"\n  },\n  \"customAreaRecordingDisabledTitle\": {\n    \"message\": \"自訂區域錄製已停用\",\n    \"description\": \"Custom area recording disabled warning on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledDescription\": {\n    \"message\": \"請將 Chrome 更新到 110 版以上\",\n    \"description\": \"Custom area recording disabled warning description on popup for Chrome version older than 104\"\n  },\n  \"customAreaRecordingDisabledAction\": {\n    \"message\": \"更新\",\n    \"description\": \"Custom area recording disabled warning action on popup for Chrome version older than 104\"\n  },\n  \"permissionsModalTitle\": {\n    \"message\": \"請檢查您的權限\",\n    \"description\": \"Camera / microphone permissions warning modal title\"\n  },\n  \"permissionsModalDescription\": {\n    \"message\": \"Screenity 似乎無法存取您的攝影機或麥克風。請確保您點擊位址列當中的相機圖示來允許存取。\",\n    \"description\": \"Camera / microphone permissions warning modal description\"\n  },\n  \"permissionsModalDismiss\": {\n    \"message\": \"忽略\",\n    \"description\": \"Camera / microphone permissions warning modal dismiss button\"\n  },\n  \"allowCameraAccessButton\": {\n    \"message\": \"允許攝影機存取\",\n    \"description\": \"Allow camera access button\"\n  },\n  \"allowMicrophoneAccessButton\": {\n    \"message\": \"允許麥克風存取\",\n    \"description\": \"Allow microphone access button\"\n  },\n  \"pushToTalkLabel\": {\n    \"message\": \"按鍵發話\",\n    \"description\": \"Push to talk switch label\"\n  },\n  \"customAreaLabel\": {\n    \"message\": \"設定要錄製的區域\",\n    \"description\": \"Custom area switch label\"\n  },\n  \"flipCameraLabel\": {\n    \"message\": \"翻轉攝影機\",\n    \"description\": \"Flip camera switch label\"\n  },\n  \"backgroundEffectsLabel\": {\n    \"message\": \"背景效果\",\n    \"description\": \"Background effects switch label\"\n  },\n  \"recordButtonLabel\": {\n    \"message\": \"開始錄製\",\n    \"description\": \"Record button label\"\n  },\n  \"recordButtonInProgressLabel\": {\n    \"message\": \"正在開始...\",\n    \"description\": \"Record button in progress label\"\n  },\n  \"recordButtonNoCameraLabel\": {\n    \"message\": \"設定要錄製的攝影機\",\n    \"description\": \"Record button label when in camera mode and no camera is selected\"\n  },\n  \"showMoreOptionsLabel\": {\n    \"message\": \"顯示更多選項\",\n    \"description\": \"Show more options label\"\n  },\n  \"systemAudioLabel\": {\n    \"message\": \"包含系統音訊\",\n    \"description\": \"Tab/system audio label\"\n  },\n  \"hideToolbarLabel\": {\n    \"message\": \"隱藏工具列\",\n    \"description\": \"Hide toolbar label\"\n  },\n  \"countdownLabel\": {\n    \"message\": \"倒數計時\",\n    \"description\": \"Countdown label\"\n  },\n  \"alarmLabel\": {\n    \"message\": \"設定時間限制\",\n    \"description\": \"Alarm label\"\n  },\n  \"blurTypeLabel\": {\n    \"message\": \"模糊\",\n    \"description\": \"Blur background label\"\n  },\n  \"noneDropdownLabel\": {\n    \"message\": \"無\",\n    \"description\": \"No device selected\"\n  },\n  \"noCameraDropdownLabel\": {\n    \"message\": \"無攝影機\",\n    \"description\": \"No camera selected\"\n  },\n  \"noMicrophoneDropdownLabel\": {\n    \"message\": \"無麥克風\",\n    \"description\": \"No microphone selected\"\n  },\n  \"selectSourceDropdownPlaceholder\": {\n    \"message\": \"選擇來源\",\n    \"description\": \"Select source placeholder\"\n  },\n  \"offLabel\": {\n    \"message\": \"關閉\",\n    \"description\": \"Off label on dropdown\"\n  },\n  \"regionWidthLabel\": {\n    \"message\": \"寬度\",\n    \"description\": \"Region width label\"\n  },\n  \"regionHeightLabel\": {\n    \"message\": \"高度\",\n    \"description\": \"Region height label\"\n  },\n  \"addImageToastTitle\": {\n    \"message\": \"點擊以放置圖片\",\n    \"description\": \"Toast that shows when adding a new image\"\n  },\n  \"finishRecordingTooltip\": {\n    \"message\": \"完成錄製\",\n    \"description\": \"Finish recording tooltip\"\n  },\n  \"restartRecordingTooltip\": {\n    \"message\": \"重新開始錄製\",\n    \"description\": \"Restart recording tooltip\"\n  },\n  \"pauseRecordingTooltip\": {\n    \"message\": \"暫停錄製\",\n    \"description\": \"Pause recording tooltip\"\n  },\n  \"resumeRecordingTooltip\": {\n    \"message\": \"繼續錄製\",\n    \"description\": \"Resume recording tooltip\"\n  },\n  \"cancelRecordingTooltip\": {\n    \"message\": \"取消錄製\",\n    \"description\": \"Cancel recording tooltip\"\n  },\n  \"toggleDrawingToolsTooltip\": {\n    \"message\": \"切換繪圖工具\",\n    \"description\": \"Toggle drawing tools tooltip\"\n  },\n  \"toggleBlurToolTooltip\": {\n    \"message\": \"切換模糊工具\",\n    \"description\": \"Toggle blur tools tooltip\"\n  },\n  \"toggleCursorOptionsTooltip\": {\n    \"message\": \"切換游標選項\",\n    \"description\": \"Toggle cursor options tooltip\"\n  },\n  \"disableCameraTooltip\": {\n    \"message\": \"停用攝影機\",\n    \"description\": \"Disable camera tooltip\"\n  },\n  \"enableCameraTooltip\": {\n    \"message\": \"啟用攝影機\",\n    \"description\": \"Enable camera tooltip\"\n  },\n  \"noCameraPermissionsTooltip\": {\n    \"message\": \"沒有攝影機權限\",\n    \"description\": \"No camera permissions tooltip\"\n  },\n  \"disableMicrophoneTooltip\": {\n    \"message\": \"停用麥克風\",\n    \"description\": \"Disable microphone tooltip\"\n  },\n  \"enableMicrophoneTooltip\": {\n    \"message\": \"啟用麥克風\",\n    \"description\": \"Enable microphone tooltip\"\n  },\n  \"noMicrophonePermissionsTooltip\": {\n    \"message\": \"沒有麥克風權限\",\n    \"description\": \"No microphone permissions tooltip\"\n  },\n  \"selectToolTooltip\": {\n    \"message\": \"選擇工具\",\n    \"description\": \"Select tool tooltip\"\n  },\n  \"penToolTooltip\": {\n    \"message\": \"鋼筆工具\",\n    \"description\": \"Pen tool tooltip\"\n  },\n  \"highlighterToolTooltip\": {\n    \"message\": \"螢光筆工具\",\n    \"description\": \"Highlighter tool tooltip\"\n  },\n  \"eraserToolTooltip\": {\n    \"message\": \"橡皮擦工具\",\n    \"description\": \"Eraser tool tooltip\"\n  },\n  \"textToolTooltip\": {\n    \"message\": \"文字工具\",\n    \"description\": \"Text tool tooltip\"\n  },\n  \"shapeToolTooltip\": {\n    \"message\": \"形狀工具\",\n    \"description\": \"Shape tool tooltip\"\n  },\n  \"arrowToolTooltip\": {\n    \"message\": \"箭頭工具\",\n    \"description\": \"Arrow tool tooltip\"\n  },\n  \"imageToolTooltip\": {\n    \"message\": \"圖片工具\",\n    \"description\": \"Image tool tooltip\"\n  },\n  \"undoTooltip\": {\n    \"message\": \"復原\",\n    \"description\": \"Undo tooltip\"\n  },\n  \"redoTooltip\": {\n    \"message\": \"重做\",\n    \"description\": \"Redo tooltip\"\n  },\n  \"clearCanvasTooltip\": {\n    \"message\": \"清除畫布\",\n    \"description\": \"Clear canvas tooltip\"\n  },\n  \"moreColorsTooltip\": {\n    \"message\": \"更多顏色\",\n    \"description\": \"More colors tooltip\"\n  },\n  \"thickStrokeTooltip\": {\n    \"message\": \"粗畫筆\",\n    \"description\": \"Thick stroke tooltip\"\n  },\n  \"mediumStrokeTooltip\": {\n    \"message\": \"中畫筆\",\n    \"description\": \"Medium stroke tooltip\"\n  },\n  \"thinStrokeTooltip\": {\n    \"message\": \"細畫筆\",\n    \"description\": \"Thin stroke tooltip\"\n  },\n  \"togglePictureinPictureModeTooltip\": {\n    \"message\": \"切換子母畫面模式\",\n    \"description\": \"Toggle picture-in-picture mode tooltip\"\n  },\n  \"toggleFillTooltip\": {\n    \"message\": \"切換填充\",\n    \"description\": \"Toggle fill tooltip\"\n  },\n  \"drawingModeToast\": {\n    \"message\": \"繪圖模式\",\n    \"description\": \"Drawing mode toast\"\n  },\n  \"blurModeToast\": {\n    \"message\": \"模糊模式\",\n    \"description\": \"Blur mode toast\"\n  },\n  \"clearBlurredElementsTooltip\": {\n    \"message\": \"清除所有模糊的元素\",\n    \"description\": \"Clear all blurred elements tooltip\"\n  },\n  \"highlightClicksTooltip\": {\n    \"message\": \"突顯點擊\",\n    \"description\": \"Highlight clicks tooltip\"\n  },\n  \"highlightCursorTooltip\": {\n    \"message\": \"突顯游標\",\n    \"description\": \"Highlight cursor tooltip\"\n  },\n  \"spotlightCursorTooltip\": {\n    \"message\": \"聚焦游標\",\n    \"description\": \"Spotlight cursor tooltip\"\n  },\n  \"permissionsModalNoShowAgain\": {\n    \"message\": \"不要再顯示\",\n    \"description\": \"Permissions modal dismiss button\"\n  },\n  \"restartModalTitle\": {\n    \"message\": \"您確定要重新開始錄製嗎？\",\n    \"description\": \"Restart recording modal title\"\n  },\n  \"restartModalDescription\": {\n    \"message\": \"將會喪失您目前影片的進度。\",\n    \"description\": \"Restart recording modal description\"\n  },\n  \"restartModalRestart\": {\n    \"message\": \"重新開始錄製\",\n    \"description\": \"Restart recording modal restart button\"\n  },\n  \"restartModalResume\": {\n    \"message\": \"繼續錄製\",\n    \"description\": \"Restart recording modal resume button\"\n  },\n  \"discardModalTitle\": {\n    \"message\": \"您確定要捨棄錄影嗎？\",\n    \"description\": \"Discard recording modal title\"\n  },\n  \"discardModalDescription\": {\n    \"message\": \"將會喪失您目前影片的進度。\",\n    \"description\": \"Discard recording modal description\"\n  },\n  \"discardModalDiscard\": {\n    \"message\": \"捨棄錄製\",\n    \"description\": \"Discard recording modal discard button\"\n  },\n  \"discardModalResume\": {\n    \"message\": \"繼續錄製\",\n    \"description\": \"Discard recording modal resume button\"\n  },\n  \"recorderSelectTitle\": {\n    \"message\": \"選擇您要錄製的內容\",\n    \"description\": \"Title in recording page\"\n  },\n  \"recorderSelectProgressTitle\": {\n    \"message\": \"錄製中...\",\n    \"description\": \"Title in recording page while recording\"\n  },\n  \"recorderSelectDescription\": {\n    \"message\": \"請維持這個分頁開啟。您的錄影完成儲存之後它便會關閉。\",\n    \"description\": \"Description in recording page\"\n  },\n  \"sandboxProgressTitle\": {\n    \"message\": \"正在準備錄製...\",\n    \"description\": \"Title in sandbox page while recording / saving\"\n  },\n  \"sandboxProgressDescription\": {\n    \"message\": \"請維持這個分頁開啟。您的錄影就緒後它便會更新並顯示您的錄影。Keep this tab open. It will update with your recording when it's ready.\",\n    \"description\": \"Description in sandbox page while recording / saving\"\n  },\n  \"sandboxEditorMainTitle\": {\n    \"message\": \"編輯器\",\n    \"description\": \"Title in navigation of the sandbox editor page\"\n  },\n  \"sandboxEditorCancelButton\": {\n    \"message\": \"取消\",\n    \"description\": \"Cancel button in sandbox editor page\"\n  },\n  \"sandboxEditorRevertButton\": {\n    \"message\": \"恢復原始狀態\",\n    \"description\": \"Revert button in sandbox editor page\"\n  },\n  \"sandboxEditorResetButton\": {\n    \"message\": \"重設\",\n    \"description\": \"Reset button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveButton\": {\n    \"message\": \"儲存變更\",\n    \"description\": \"Save button in sandbox editor page\"\n  },\n  \"sandboxEditorSaveProgressButton\": {\n    \"message\": \"儲存中...\",\n    \"description\": \"Save button in sandbox editor page while saving\"\n  },\n  \"sandboxAudioDragAndDrop\": {\n    \"message\": \"拖放音訊檔案至此\",\n    \"description\": \"Drag and drop audio file in sandbox editor page\"\n  },\n  \"sandboxAudioOrBrowse\": {\n    \"message\": \"或是點擊以瀏覽檔案\",\n    \"description\": \"Or click to browse audio file in sandbox editor page\"\n  },\n  \"sandboxAudioSettingsTitle\": {\n    \"message\": \"設定\",\n    \"description\": \"Audio settings title in sandbox editor page\"\n  },\n  \"sandboxAudioVolumeLabel\": {\n    \"message\": \"音量\",\n    \"description\": \"Audio volume label in sandbox editor page\"\n  },\n  \"sandboxAudioUpdateButton\": {\n    \"message\": \"更新\",\n    \"description\": \"Update audio button in sandbox editor page\"\n  },\n  \"sandboxCropTitle\": {\n    \"message\": \"裁剪\",\n    \"description\": \"Crop title in sandbox editor page\"\n  },\n  \"widthLabel\": {\n    \"message\": \"寬度\",\n    \"description\": \"Width label for properties\"\n  },\n  \"heightLabel\": {\n    \"message\": \"高度\",\n    \"description\": \"Height label for properties\"\n  },\n  \"leftLabel\": {\n    \"message\": \"左側\",\n    \"description\": \"Left label for properties\"\n  },\n  \"topLabel\": {\n    \"message\": \"頂部\",\n    \"description\": \"Top label for properties\"\n  },\n  \"sandboxEditorTrimInfo\": {\n    \"message\": \"拖曳控制柄然後用左側按鈕來編輯所選範圍。\",\n    \"description\": \"Info about trimming in sandbox editor\"\n  },\n  \"sandboxEditorTrimButton\": {\n    \"message\": \"修剪影片\",\n    \"description\": \"Trim button in sandbox editor\"\n  },\n  \"sandboxEditorTrimProgressButton\": {\n    \"message\": \"修剪中...\",\n    \"description\": \"Trim button in sandbox editor while trimming\"\n  },\n  \"sandboxEditorCutButton\": {\n    \"message\": \"分割範圍\",\n    \"description\": \"Cut button in sandbox editor\"\n  },\n  \"sandboxEditorCutProgressButton\": {\n    \"message\": \"分割中...\",\n    \"description\": \"Cut button in sandbox editor while cutting\"\n  },\n  \"sandboxEditorMuteButton\": {\n    \"message\": \"靜音音訊\",\n    \"description\": \"Mute button in sandbox editor\"\n  },\n  \"sandboxEditorMuteProgressButton\": {\n    \"message\": \"靜音中...\",\n    \"description\": \"Mute button in sandbox editor while muting\"\n  },\n  \"learnMoreDot\": {\n    \"message\": \"瞭解更多。\",\n    \"description\": \"Learn more with a dot\"\n  },\n  \"undoLabel\": {\n    \"message\": \"復原\",\n    \"description\": \"Undo label\"\n  },\n  \"redoLabel\": {\n    \"message\": \"重做\",\n    \"description\": \"Redo label\"\n  },\n  \"leaveReview\": {\n    \"message\": \"留下評論，這很有幫助！\",\n    \"description\": \"Leave a review button\"\n  },\n  \"followForUpdates\": {\n    \"message\": \"跟隨以取得更新資訊\",\n    \"description\": \"Follow for updates button\"\n  },\n  \"offlineLabelTitle\": {\n    \"message\": \"您目前離線中\",\n    \"description\": \"Offline label\"\n  },\n  \"offlineLabelDescription\": {\n    \"message\": \"有些功能需要連線後才能使用\",\n    \"description\": \"Offline label description\"\n  },\n  \"offlineLabelTryAgain\": {\n    \"message\": \"再試一次\",\n    \"description\": \"Offline label try again button\"\n  },\n  \"updateChromeLabelTitle\": {\n    \"message\": \"Chrome 需要更新\",\n    \"description\": \"Update Chrome label\"\n  },\n  \"updateChromeLabelDescription\": {\n    \"message\": \"更新來使用更多功能\",\n    \"description\": \"Update Chrome label description\"\n  },\n  \"learnMoreLabel\": {\n    \"message\": \"瞭解更多\",\n    \"description\": \"Learn more button\"\n  },\n  \"overLimitLabelTitle\": {\n    \"message\": \"影片在裝置上花費太久\",\n    \"description\": \"Over 5 minutes limit label in editor\"\n  },\n  \"overLimitLabelDescription\": {\n    \"message\": \"編輯功能與 MP4 匯出無法使用\",\n    \"description\": \"Over 5 minutes limit description in editor\"\n  },\n  \"videoProcessingLabelTitle\": {\n    \"message\": \"影片正在於本機處理中...\",\n    \"description\": \"Video processing label in editor\"\n  },\n  \"videoProcessingLabelDescription\": {\n    \"message\": \"瞭解更多關於更快速的雲端版本的資訊\",\n    \"description\": \"Video processing description in editor\"\n  },\n  \"sandboxEditTitle\": {\n    \"message\": \"編輯\",\n    \"description\": \"Edit title in sandbox editor page\"\n  },\n  \"noConnectionLabel\": {\n    \"message\": \"沒有連線\",\n    \"description\": \"No connection label\"\n  },\n  \"notAvailableLabel\": {\n    \"message\": \"不可用\",\n    \"description\": \"Not available label\"\n  },\n  \"editButtonTitle\": {\n    \"message\": \"編輯影片\",\n    \"description\": \"Trim label button\"\n  },\n  \"editButtonDescription\": {\n    \"message\": \"修剪、分割或靜音影片\",\n    \"description\": \"Trim label\"\n  },\n  \"preparingLabel\": {\n    \"message\": \"正在準備...\",\n    \"description\": \"Preparing label\"\n  },\n  \"cropButtonTitle\": {\n    \"message\": \"裁剪\",\n    \"description\": \"Crop label button\"\n  },\n  \"cropButtonDescription\": {\n    \"message\": \"裁剪並調整影片大小\",\n    \"description\": \"Crop label\"\n  },\n  \"addAudioButtonTitle\": {\n    \"message\": \"加入音訊\",\n    \"description\": \"Add audio label button\"\n  },\n  \"addAudioButtonDescription\": {\n    \"message\": \"上傳您要加到影片中的音訊\",\n    \"description\": \"Add audio label\"\n  },\n  \"sandboxSaveTitle\": {\n    \"message\": \"儲存\",\n    \"description\": \"Save title in sandbox editor page\"\n  },\n  \"signOutDriveLabel\": {\n    \"message\": \"登出雲端硬碟\",\n    \"description\": \"Sign out of Google Drive label\"\n  },\n  \"savingDriveLabel\": {\n    \"message\": \"儲存中...\",\n    \"description\": \"Saving to Google Drive label\"\n  },\n  \"saveDriveButtonTitle\": {\n    \"message\": \"儲存到雲端硬碟\",\n    \"description\": \"Save to Google Drive button\"\n  },\n  \"saveDriveButtonDescription\": {\n    \"message\": \"將目前版本儲存到 Google 雲端硬碟\",\n    \"description\": \"Save to Google Drive label\"\n  },\n  \"signInDriveLabel\": {\n    \"message\": \"登入並儲存到雲端硬碟\",\n    \"description\": \"Sign in to Google Drive label\"\n  },\n  \"sandboxExportTitle\": {\n    \"message\": \"匯出\",\n    \"description\": \"Export title in sandbox editor page\"\n  },\n  \"downloadingLabel\": {\n    \"message\": \"下載中...\",\n    \"description\": \"Downloading label\"\n  },\n  \"downloadWEBMButtonTitle\": {\n    \"message\": \"下載成 .webm\",\n    \"description\": \"Download WEBM button\"\n  },\n  \"downloadWEBMButtonDescription\": {\n    \"message\": \"匯出成 .webm 影片\",\n    \"description\": \"Download WEBM label\"\n  },\n  \"downloadMP4ButtonTitle\": {\n    \"message\": \"下載成 .mp4\",\n    \"description\": \"Download MP4 button\"\n  },\n  \"downloadMP4ButtonDescription\": {\n    \"message\": \"匯出成 .mp4 影片（建議使用）\",\n    \"description\": \"Download MP4 label\"\n  },\n  \"downloadGIFButtonTitle\": {\n    \"message\": \"下載成 .gif\",\n    \"description\": \"Download GIF button\"\n  },\n  \"downloadGIFButtonDescription\": {\n    \"message\": \"匯出 GIF 檔（最長 30 秒）\",\n    \"description\": \"Download GIF label\"\n  },\n  \"shareModalSandboxTitle\": {\n    \"message\": \"準備好升級你的錄影了嗎？\",\n    \"description\": \"Share modal title in sandbox editor page\"\n  },\n  \"shareModalSandboxDescription\": {\n    \"message\": \"雲端分享、縮放、模板功能……支持一位重視隱私的獨立女性開發者 ❤️\",\n    \"description\": \"Share modal description in sandbox editor page\"\n  },\n  \"shareModalSandboxButton\": {\n    \"message\": \"了解 Pro 版\",\n    \"description\": \"Share modal email placeholder in sandbox editor page\"\n  },\n  \"shareModalSandboxLogin\": {\n    \"message\": \"已有帳號？登入\",\n    \"description\": \"Share modal login link in sandbox editor page\"\n  },\n  \"shareSandboxButton\": {\n    \"message\": \"分享影片\",\n    \"description\": \"Share button in sandbox editor page\"\n  },\n  \"replaceAudioEditor\": {\n    \"message\": \"取代現有音訊\",\n    \"description\": \"Replace audio button in editor\"\n  },\n  \"zoomToPointPopup\": {\n    \"message\": \"縮放到游標位置\",\n    \"description\": \"Zoom to point popup\"\n  },\n  \"stayInPagePopup\": {\n    \"message\": \"錄製時留在頁面中\",\n    \"description\": \"Stay in page popup\"\n  },\n  \"micReminderPopup\": {\n    \"message\": \"麥克風關閉警告\",\n    \"description\": \"Mic reminder popup\"\n  },\n  \"helpPopup\": {\n    \"message\": \"說明\",\n    \"description\": \"Help popup button\"\n  },\n  \"pausedRecordingToast\": {\n    \"message\": \"錄製已暫停。請按播放按鈕來繼續。\",\n    \"description\": \"Paused recording modal title\"\n  },\n  \"chromePermissionsModalTitle\": {\n    \"message\": \"Screenity 沒有權限錄製您的螢幕\",\n    \"description\": \"Permission modal title\"\n  },\n  \"chromePermissionsModalDescription\": {\n    \"message\": \"要啟用螢幕錄製，您必須在向您詢問時提供 Screenity 擷取您的螢幕內容的權限。\",\n    \"description\": \"Permission modal description\"\n  },\n  \"chromePermissionsModalAction\": {\n    \"message\": \"啟用螢幕錄製\",\n    \"description\": \"Permission modal action\"\n  },\n  \"chromePermissionsModalCancel\": {\n    \"message\": \"取消\",\n    \"description\": \"Permission modal cancel\"\n  },\n  \"micMutedModalTitle\": {\n    \"message\": \"您的麥克風已靜音\",\n    \"description\": \"Microphone muted modal title\"\n  },\n  \"micMutedModalDescription\": {\n    \"message\": \"要在錄影當中包含音訊，您會需要解除麥克風的靜音。還是您要不包含音訊直接繼續操作？\",\n    \"description\": \"Microphone muted modal description\"\n  },\n  \"micMutedModalAction\": {\n    \"message\": \"是，繼續\",\n    \"description\": \"Microphone muted modal continue\"\n  },\n  \"micMutedModalCancel\": {\n    \"message\": \"取消\",\n    \"description\": \"Microphone muted modal cancel\"\n  },\n  \"setupTitle\": {\n    \"message\": \"要開始使用 Screenity 只要三個簡單步驟：\",\n    \"description\": \"Set up steps title\"\n  },\n  \"setupStep1Before\": {\n    \"message\": \"1- 點擊 \",\n    \"description\": \"Set up step 1, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep1After\": {\n    \"message\": \" 擴充功能圖示\",\n    \"description\": \"Set up step 1, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep2Before\": {\n    \"message\": \"2- 按下 \",\n    \"description\": \"Set up step 2, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep2After\": {\n    \"message\": \" 固定圖示\",\n    \"description\": \"Set up step 2, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupStep3Before\": {\n    \"message\": \"3- 點擊 \",\n    \"description\": \"Set up step 3, before the icon. It needs a space at the end.\"\n  },\n  \"setupStep3After\": {\n    \"message\": \" Screenity 圖示來開始使用\",\n    \"description\": \"Set up step 3, after the icon. It needs a space at the beginning.\"\n  },\n  \"setupCompleteTitle\": {\n    \"message\": \"太好了！您已準備就緒\",\n    \"description\": \"Set up complete title\"\n  },\n  \"setupCompleteDescription\": {\n    \"message\": \"您現在可以從這裡開始錄製，也可以從其他任何分頁開始。\",\n    \"description\": \"Set up complete description\"\n  },\n  \"clickHereDrawOnboarding\": {\n    \"message\": \"點擊此處以開始繪圖\",\n    \"description\": \"Click here onboarding\"\n  },\n  \"countdownMessage\": {\n    \"message\": \"放鬆吧。要停止倒數計時點擊任何地方即可。\",\n    \"description\": \"Countdown message\"\n  },\n  \"sandboxEditorTooSmallInfo\": {\n    \"message\": \"由於影格之間的間隔，短時間範圍的編輯可能不準確。Edits may be inaccurate for short time ranges due to the intervals between frames.\",\n    \"description\": \"Info about the editor being too small\"\n  },\n  \"processingBannerEditor\": {\n    \"message\": \"影片處理中，播放可能不準確或被中斷。\",\n    \"description\": \"Processing banner in editor\"\n  },\n  \"croppingInfoTitle\": {\n    \"message\": \"裁剪可能需要一段時間\",\n    \"description\": \"Cropping info title\"\n  },\n  \"croppingInfoDescription\": {\n    \"message\": \"升級來獲取更快的處理速度\",\n    \"description\": \"Cropping info description\"\n  },\n  \"micOnToast\": {\n    \"message\": \"麥克風已啟用\",\n    \"description\": \"Microphone enabled toast title\"\n  },\n  \"micOffToast\": {\n    \"message\": \"麥克風已停用\",\n    \"description\": \"Microphone disabled toast title\"\n  },\n  \"hideUIAlerts\": {\n    \"message\": \"隱藏介面內通知\",\n    \"description\": \"Hide UI alerts button\"\n  },\n  \"noShowAgain\": {\n    \"message\": \"不要再顯示\",\n    \"description\": \"Don't show again button\"\n  },\n  \"toolbarHoverOnly\": {\n    \"message\": \"未使用時隱藏工具列\",\n    \"description\": \"Only show toolbar on hover button\"\n  },\n  \"stopRecording\": {\n    \"message\": \"停止錄製\",\n    \"description\": \"Stop recording button in recording screen\"\n  },\n  \"updateAnnouncementTitle\": {\n    \"message\": \"歡迎來到新版 Screenity！\",\n    \"description\": \"Update announcement title for existing users\"\n  },\n  \"updateAnnouncementDescription\": {\n    \"message\": \"我們為您帶來新的外觀與許多有趣的新功能，但請放心，Screenity 仍然是您所熟悉且喜愛的免費、保護隱私且開源的擴充元件。\",\n    \"description\": \"Update announcement description for existing users\"\n  },\n  \"updateAnnouncementLearnMore\": {\n    \"message\": \"瞭解關於更新的更多資訊\",\n    \"description\": \"Update announcement learn more link for existing users\"\n  },\n  \"updateAnnouncementButton\": {\n    \"message\": \"開始使用\",\n    \"description\": \"Update announcement button for existing users\"\n  },\n  \"streamErrorModalTitle\": {\n    \"message\": \"開始錄製時發生錯誤\",\n    \"description\": \"Stream error modal title\"\n  },\n  \"streamErrorModalDescription\": {\n    \"message\": \"開始錄製時似乎發生了錯誤。請重新開啟您的瀏覽器後再試一次。如果問題還是存在，請寄信到 support@screenity.io 與我們聯繫。\",\n    \"description\": \"Stream error modal description\"\n  },\n  \"highestQuality\": {\n    \"message\": \"最高品質\",\n    \"description\": \"Highest quality toggle label in popup\"\n  },\n  \"restoreRecording\": {\n    \"message\": \"回復上次的錄影\",\n    \"description\": \"Restore last recording button in popup\"\n  },\n  \"havingIssuesButton\": {\n    \"message\": \"遇到問題了嗎？\",\n    \"description\": \"Having issues button in editor\"\n  },\n  \"havingIssuesModalTitle\": {\n    \"message\": \"看不到您的錄影嗎？\",\n    \"description\": \"Having issues modal title in editor\"\n  },\n  \"havingIssuesModalDescription\": {\n    \"message\": \"如果您完成錄製之後來到這個頁面但卻沒看到您的影片，您可以嘗試下載原始影片資料（如果可用的話）。也可以考慮與我們聯絡進行錯誤回報。\",\n    \"description\": \"Having issues modal description in editor\"\n  },\n  \"havingIssuesModalButton\": {\n    \"message\": \"下載原始影片資料\",\n    \"description\": \"Having issues modal button in editor\"\n  },\n  \"havingIssuesModalButton2\": {\n    \"message\": \"回報問題\",\n    \"description\": \"Having issues modal button 2 in editor\"\n  },\n  \"noRecordingFound\": {\n    \"message\": \"沒有找到錄影，不好意思 :(\",\n    \"description\": \"No recording found alert in editor\"\n  },\n  \"memoryLimitTitle\": {\n    \"message\": \"已達記憶體上限\",\n    \"description\": \"Memory limit reached title\"\n  },\n  \"memoryLimitDescription\": {\n    \"message\": \"您的裝置記憶體已用盡而不足錄製，因此錄製已自動停止。如果您需要錄製更久，可以試着降低影片品質或是在裝置上清出一些空間。Your device has run out of memory to record, so the recording has stopped automatically. If you'd like to record for longer, you can try lowering the quality of the video, or clearing up some space on your device.\",\n    \"description\": \"Memory limit reached description\"\n  },\n  \"understoodButton\": {\n    \"message\": \"瞭解\",\n    \"description\": \"Understood button\"\n  },\n  \"notEnoughSpaceTitle\": {\n    \"message\": \"空間不足\",\n    \"description\": \"Not enough space title\"\n  },\n  \"notEnoughSpaceDescription\": {\n    \"message\": \"Screenity 需要至少 1GB 的空間才能錄製。請清出一些空間之後再試一次。\",\n    \"description\": \"Not enough space description\"\n  },\n  \"clearSpaceButton\": {\n    \"message\": \"清除先前的錄影\",\n    \"description\": \"Clear previous recordings button\"\n  },\n  \"sandboxAdvancedTitle\": {\n    \"message\": \"進階\",\n    \"description\": \"Advanced section in sandbox editor page\"\n  },\n  \"rawRecordingButtonTitle\": {\n    \"message\": \"下載原始影片檔案\",\n    \"description\": \"Download raw recording button\"\n  },\n  \"rawRecordingButtonDescription\": {\n    \"message\": \"匯出原本的錄影資料\",\n    \"description\": \"Download raw recording label\"\n  },\n  \"troubleshootButtonTitle\": {\n    \"message\": \"取得關於錄製的說明\",\n    \"description\": \"Troubleshoot button\"\n  },\n  \"troubleShootButtonDescription\": {\n    \"message\": \"復原您的影片或解決其它問題\",\n    \"description\": \"Troubleshoot label\"\n  },\n  \"rawRecordingModalTitle\": {\n    \"message\": \"下載原始影片檔案\",\n    \"description\": \"Raw recording modal title\"\n  },\n  \"rawRecordingModalDescription\": {\n    \"message\": \"原始影片檔案是 Screenity 錄製時原本所產生的影片檔。這個影片沒有處理過或編輯過，所以可能會非常大或不被所有裝置支援。如果您在使用處理後的影片時有發生問題，您可以使用這個檔案來復原您的影片。\",\n    \"description\": \"Raw recording modal description\"\n  },\n  \"rawRecordingModalButton\": {\n    \"message\": \"下載\",\n    \"description\": \"Raw recording modal button\"\n  },\n  \"troubleshootModalTitle\": {\n    \"message\": \"下載系統資訊與影片資料並傳送以進行問題排除\",\n    \"description\": \"Troubleshoot modal title\"\n  },\n  \"troubleshootModalDescription\": {\n    \"message\": \"下載之後請將檔案寄到 support@screenity.io，如果有相關的資訊也請包含在信件中。我們會使用該資料幫助您排解使用這個擴充元件的問題，並試著復原您的影片。\",\n    \"description\": \"Troubleshoot modal description\"\n  },\n  \"troubleshootModalButton\": {\n    \"message\": \"下載\",\n    \"description\": \"Troubleshoot modal button\"\n  },\n  \"overLimitModalTitle\": {\n    \"message\": \"影片在您的裝置上花太長時間處理了\",\n    \"description\": \"Over 5 minutes limit modal title\"\n  },\n  \"overLimitModalDescription\": {\n    \"message\": \"由於 Screenity 完全在本機執行來保護隱私，它也受您的裝置硬體限制。處理這個長度的影片會需要太久，並需要大量電腦資源。不用擔心，您還是可以下載 WEBM 檔案或是原始錄影，只是編輯功能與 MP4 匯出會無法使用。雖然是這樣說，如果您瞭解風險您還是可以決定嘗試處理影片。\",\n    \"description\": \"Over 5 minutes limit modal description\"\n  },\n  \"overLimitModalButton\": {\n    \"message\": \"我瞭解風險，仍要嘗試\",\n    \"description\": \"Over 5 minutes limit modal button\"\n  },\n  \"overLimitModalLearnMore\": {\n    \"message\": \"瞭解更多關於處理長影片的資訊。\",\n    \"description\": \"Over 5 minutes limit modal learn more link with a dot\"\n  },\n  \"recoveryModeTitle\": {\n    \"message\": \"復原模式\",\n    \"description\": \"Recovery mode title\"\n  },\n  \"downloadForTroubleshootingOption\": {\n    \"message\": \"下載問題排除用的資料\",\n    \"description\": \"Download for troubleshooting option\"\n  },\n  \"getHelpNav\": {\n    \"message\": \"說明中心\",\n    \"description\": \"Get help nav item\"\n  },\n  \"audioWarningTitle\": {\n    \"message\": \"錄製電腦音訊\",\n    \"description\": \"Audio warning title\"\n  },\n  \"audioWarningDescription\": {\n    \"message\": \"要錄製此頁面的音訊，您需要切換到「$tab$」。\",\n    \"description\": \"Audio warning description\",\n    \"placeholders\": {\n      \"tab\": {\n        \"content\": \"$1\",\n        \"example\": \"Tab area\"\n      }\n    }\n  },\n  \"extensionNotSupportedTitle\": {\n    \"message\": \"擴充功能在該分頁中不支援\",\n    \"description\": \"Extension not supported on tab\"\n  },\n  \"extensionNotSupportedDescription\": {\n    \"message\": \"Screenity 不支援您先前的分頁。您還是可以從這裡開始錄製然後再切換回該分頁，但是攝影機和工具列會沒辦法顯示。\",\n    \"description\": \"Extension not supported on tab description\"\n  },\n  \"backupsTitle\": {\n    \"message\": \"為您的 Screenity 錄影建立本機備份\",\n    \"description\": \"Backups title\"\n  },\n  \"backupsDescription1\": {\n    \"message\": \"為您每個錄影保留副本。如果您不設定備份，您的錄影只會在您選擇下載時才會被儲存。\",\n    \"description\": \"Backups description\"\n  },\n  \"backupsDescription2\": {\n    \"message\": \"您可能會需要先建立新資料夾才能儲存到《下載》或是其他系統資料夾當中。\",\n    \"description\": \"Backups description\"\n  },\n  \"backupsSelectFolder\": {\n    \"message\": \"選擇資料夾\",\n    \"description\": \"Select a folder button\"\n  },\n  \"backupsNotNow\": {\n    \"message\": \"現在不要\",\n    \"description\": \"Not now button\"\n  },\n  \"backupsOnTitle\": {\n    \"message\": \"您的錄製已在本機備份\",\n    \"description\": \"Backups on title\"\n  },\n  \"backupsOnDescription\": {\n    \"message\": \"要防止每次錄製的時候都被要求權限，我們建議維持這個分頁開啟。\",\n    \"description\": \"Backups on description\"\n  },\n  \"backupsClose\": {\n    \"message\": \"還是要關閉分頁\",\n    \"description\": \"Close tab anyway button\"\n  },\n  \"backupsStop\": {\n    \"message\": \"停止備份所有錄影\",\n    \"description\": \"Stop backups button\"\n  },\n  \"backupsConfirmTitle\": {\n    \"message\": \"請重新確認對於您的 Screenity 本機備份資料夾的存取權\",\n    \"description\": \"Backups confirm title\"\n  },\n  \"backupsConfirmDescription\": {\n    \"message\": \"不好意思，我們需要重新要求您的權限來繼續備份您的錄影。不幸的，每次這個分頁關閉（包含瀏覽器關閉或是電腦重啟）的時候我們都需要再重新詢問權限。\",\n    \"description\": \"Backups confirm description\"\n  },\n  \"backupsConfirmAllow\": {\n    \"message\": \"允許 Screenity 繼續備份錄影\",\n    \"description\": \"Backups confirm allow button\"\n  },\n  \"backupsToggle\": {\n    \"message\": \"備份錄影\",\n    \"description\": \"Backups toggle label\"\n  },\n  \"backupPermissionFailTitle\": {\n    \"message\": \"未提供權限以備份錄影\",\n    \"description\": \"Backup permission fail title\"\n  },\n  \"backupPermissionFailDescription\": {\n    \"message\": \"您沒有選擇備份錄影的資料夾。依您的瀏覽器版本或是作業系統，每次開始錄製的時候可能都會請您再次選擇資料夾。您可以再試一次，或是如果不希望一天到晚重複提供權限的話也可以選擇完全停用備份。\",\n    \"description\": \"Backup permission fail description\"\n  },\n  \"recordAudioWarningMacTitle\": {\n    \"message\": \"錄製電腦音訊\",\n    \"description\": \"Record audio warning title\"\n  },\n  \"recordAudioWarningMacDescription\": {\n    \"message\": \"在 macOS 上，您只能錄製分頁音訊。請選擇「Chrome 分頁」選項然後確保「也分享分頁音訊」有啟用。\",\n    \"description\": \"macOS audio warning description\"\n  },\n  \"recordAudioWarningOtherTitle\": {\n    \"message\": \"錄製電腦音訊\",\n    \"description\": \"Record audio warning title\"\n  },\n  \"recordAudioWarningOtherDescription\": {\n    \"message\": \"如果您要錄製電腦音訊請確保您啟用「分享系統音訊」選項。如果您沒有看到這個選項，請嘗試更新 Chrome 或是改為使用分頁錄製。\",\n    \"description\": \"Other audio warning description\"\n  },\n  \"maxResolutionLabel\": {\n    \"message\": \"最大解析度\",\n    \"description\": \"Max resolution label in settings menu\"\n  },\n  \"maxFPSLabel\": {\n    \"message\": \"最大幀率\",\n    \"description\": \"Max FPS label in settings menu\"\n  },\n  \"notEnoughRAM\": {\n    \"message\": \"記憶體不足\",\n    \"description\": \"Not enough RAM label in settings menu\"\n  },\n  \"resizeWindowLabel\": {\n    \"message\": \"調整視窗大小\",\n    \"description\": \"Resize window label in settings menu\"\n  },\n  \"screenTooSmallTooltip\": {\n    \"message\": \"螢幕太小而無法使用此解析度\",\n    \"description\": \"Screen too small tooltip in settings menu\"\n  },\n  \"maxResolutionTooltip\": {\n    \"message\": \"您的裝置無法使用此解析度進行錄製\",\n    \"description\": \"Maxed out resolution tooltip in settings menu\"\n  },\n  \"permissionsModalReview\": {\n    \"message\": \"審閱權限\",\n    \"description\": \"Review permissions button\"\n  },\n  \"recordButtonMultiLabel\": {\n    \"message\": \"新增錄製\",\n    \"description\": \"Record button label (multi mode)\"\n  },\n  \"recorderSetupTitle\": {\n    \"message\": \"準備中...\",\n    \"description\": \"Title in recording page while setting up project before countdown starts\"\n  },\n  \"recorderSetupDescription\": {\n    \"message\": \"請稍候，正在準備您的錄製。\",\n    \"description\": \"Description in recording page before countdown starts\"\n  },\n  \"reopenToolbarToast\": {\n    \"message\": \"工具列已隱藏，可在彈出式選單中重新啟用。\",\n    \"description\": \"Toast message explaining to reopen the toolbar in the popup options\"\n  },\n  \"loginButtonLabel\": {\n    \"message\": \"登入或註冊\",\n    \"description\": \"Login button label in popup settings menu\"\n  },\n  \"logoutButtonLabel\": {\n    \"message\": \"登出\",\n    \"description\": \"Logout button label in popup settings menu\"\n  },\n  \"loggedOutToastTitle\": {\n    \"message\": \"您已登出\",\n    \"description\": \"Toast message shown when the user logs out\"\n  },\n  \"storageLimitReachedTitle\": {\n    \"message\": \"已達儲存上限\",\n    \"description\": \"Modal message shown when the storage limit is reached\"\n  },\n  \"storageLimitReachedDescription\": {\n    \"message\": \"您已使用完 100 GB 的儲存空間。請刪除舊影片或場景以繼續錄製。\",\n    \"description\": \"Modal description shown when the storage limit is reached\"\n  },\n  \"manageStorageButtonLabel\": {\n    \"message\": \"管理儲存空間\",\n    \"description\": \"Button label to manage storage in the popup settings menu\"\n  },\n  \"closeModalLabel\": {\n    \"message\": \"關閉\",\n    \"description\": \"Agnostic close modal button label\"\n  },\n  \"storageCheckFailTitle\": {\n    \"message\": \"無法檢查儲存空間\",\n    \"description\": \"Title shown when the storage check fails\"\n  },\n  \"storageCheckFailAuthDescription\": {\n    \"message\": \"檢查儲存空間時發生問題。請重新登入後再嘗試錄製。\",\n    \"description\": \"Description shown when the storage check fails due to authentication\"\n  },\n  \"storageCheckFailDescription\": {\n    \"message\": \"我們無法驗證您的儲存配額。請稍後再試或重新整理頁面。\",\n    \"description\": \"Description shown when the storage check fails\"\n  },\n  \"retryButtonLabel\": {\n    \"message\": \"再試一次\",\n    \"description\": \"Agnostic button label to try again\"\n  },\n  \"addedToMultiToast\": {\n    \"message\": \"場景已加入 Multi 錄製\",\n    \"description\": \"Toast message shown when a scene is added to multi-recording\"\n  },\n  \"readyRecordSceneToast\": {\n    \"message\": \"已準備好錄製影片中的新場景\",\n    \"description\": \"Toast message shown when the user is in an existing project and ready to record a new scene\"\n  },\n  \"reachingRecordingLimitToast\": {\n    \"message\": \"錄製即將達到 90 分鐘上限，將會自動停止\",\n    \"description\": \"Toast message shown when the recording is reaching the 90 minute limit\"\n  },\n  \"recordingLimitReachedToast\": {\n    \"message\": \"已達 90 分鐘上限，錄製已停止\",\n    \"description\": \"Toast message shown when the recording limit is reached\"\n  },\n  \"tabRecordingDisabledToast\": {\n    \"message\": \"此頁面停用分頁錄製，已自動切換為螢幕錄製。\",\n    \"description\": \"Toast message shown when tab recording is disabled on a page\"\n  },\n  \"projectRecordingCancelledToast\": {\n    \"message\": \"影片錄製已取消\",\n    \"description\": \"Toast message shown when the user cancels project recording\"\n  },\n  \"copiedToClipboardToast\": {\n    \"message\": \"連結已複製到剪貼簿\",\n    \"description\": \"Toast message shown when link has been copied to clipboard\"\n  },\n  \"failedToCopyToClipboardToast\": {\n    \"message\": \"無法複製連結\",\n    \"description\": \"Toast message shown when failed to copy link to clipboard\"\n  },\n  \"newestSortLabel\": {\n    \"message\": \"最新\",\n    \"description\": \"Label for the newest sort option\"\n  },\n  \"oldestSortLabel\": {\n    \"message\": \"最舊\",\n    \"description\": \"Label for the oldest sort option\"\n  },\n  \"allVideosHeading\": {\n    \"message\": \"所有影片\",\n    \"description\": \"Heading for the all videos section in the popup\"\n  },\n  \"noVideosFound\": {\n    \"message\": \"找不到影片\",\n    \"description\": \"Message shown when no videos are found in the popup\"\n  },\n  \"loadingVideosLabel\": {\n    \"message\": \"正在載入影片...\",\n    \"description\": \"Label shown when videos are loading in the popup\"\n  },\n  \"goToDashboardButtonLabel\": {\n    \"message\": \"前往儀表板\",\n    \"description\": \"Button label to go to the dashboard in the popup\"\n  },\n  \"welcomePopupTitle\": {\n    \"message\": \"歡迎使用 Screenity\",\n    \"description\": \"Title for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionTop\": {\n    \"message\": \"無廣告。無追蹤。無限制。\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupDescriptionBottom\": {\n    \"message\": \"只要點下錄製。\",\n    \"description\": \"Description for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCTA\": {\n    \"message\": \"免費使用 — 無需註冊！\",\n    \"description\": \"Call to action button for the welcome popup shown to new users\"\n  },\n  \"welcomePopupCloudTitle\": {\n    \"message\": \"想讓影片發揮更多效用？\",\n    \"description\": \"Title for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature1\": {\n    \"message\": \"雲端儲存，一鍵分享影片連結\",\n    \"description\": \"Feature 1 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature2\": {\n    \"message\": \"將多個片段合併成完整影片\",\n    \"description\": \"Feature 2 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature3\": {\n    \"message\": \"點擊智慧縮放（也可以自訂）\",\n    \"description\": \"Feature 3 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature4\": {\n    \"message\": \"加入範本、字幕、版面配置⋯\",\n    \"description\": \"Feature 4 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudFeature5\": {\n    \"message\": \"自訂背景與模擬圖\",\n    \"description\": \"Feature 5 for the cloud storage in the welcome popup\"\n  },\n  \"welcomePopupCloudCTA\": {\n    \"message\": \"🔓 解鎖進階功能\",\n    \"description\": \"Call to action button for the cloud storage feature in the welcome popup\"\n  },\n  \"welcomePopupSupport\": {\n    \"message\": \"+ 支持獨立創作者的開發工作！\",\n    \"description\": \"Message for supporting the indie maker in the welcome popup\"\n  },\n  \"accountSettingsOption\": {\n    \"message\": \"帳號設定\",\n    \"description\": \"Label for the account settings option in the popup\"\n  },\n  \"supportSettingsOption\": {\n    \"message\": \"支援\",\n    \"description\": \"Label for the support option in the popup\"\n  },\n  \"inactiveSubscriptionTitle\": {\n    \"message\": \"您的 Pro 訂閱已失效\",\n    \"description\": \"Title shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionDescription\": {\n    \"message\": \"您的 Screenity Pro 訂閱目前已失效。\",\n    \"description\": \"Description shown when the user's subscription is inactive\"\n  },\n  \"inactiveSubscriptionReactivation\": {\n    \"message\": \"請重新啟用以恢復存取。\",\n    \"description\": \"Message prompting the user to reactivate their subscription\"\n  },\n  \"inactiveSubscriptionDeletionWarning\": {\n    \"message\": \"您的影片與資料將在以下時間被永久刪除：\",\n    \"description\": \"Warning message about the deletion of videos and data\"\n  },\n  \"manageSubscriptionButton\": {\n    \"message\": \"管理您的訂閱\",\n    \"description\": \"Button to manage the user's subscription\"\n  },\n  \"inactiveSubscriptionFreeVersion\": {\n    \"message\": \"想繼續使用免費版本嗎？\",\n    \"description\": \"Message prompting the user to continue using the free version\"\n  },\n  \"downgradeToFreeButton\": {\n    \"message\": \"登出並降級\",\n    \"description\": \"Button to log out and downgrade to the free version\"\n  },\n  \"loggedOutTitle\": {\n    \"message\": \"您已登出\",\n    \"description\": \"Title shown when the user is logged out\"\n  },\n  \"loggedOutDescription\": {\n    \"message\": \"請重新登入以同步錄製內容並存取進階功能。\",\n    \"description\": \"Description shown when the user is logged out\"\n  },\n  \"logBackInButton\": {\n    \"message\": \"重新登入\",\n    \"description\": \"Button to log back in after being logged out\"\n  },\n  \"loggedOutFreeVersion\": {\n    \"message\": \"您仍可在未登入狀態下使用擴充功能，但錄製內容不會被保存且無法恢復。\",\n    \"description\": \"Message shown when the user is logged out and using the free version\"\n  },\n  \"continueWithoutLogin\": {\n    \"message\": \"繼續使用（不登入）\",\n    \"description\": \"Button to continue using the extension without logging in\"\n  },\n  \"instantRecordingModeLabel\": {\n    \"message\": \"即時錄製模式\",\n    \"description\": \"Label for the instant recording mode\"\n  },\n  \"instantRecordingModeTooltip\": {\n    \"message\": \"可立即下載，但相機與版面日後無法編輯。\",\n    \"description\": \"Tooltip for the instant recording mode\"\n  },\n  \"instantRecordingModeTitle\": {\n    \"message\": \"即時錄製模式\",\n    \"description\": \"Title for the instant recording mode\"\n  },\n  \"instantRecordingModeDescription\": {\n    \"message\": \"所有內容將錄製為單一影片，便於即時下載與分享。相機配置將無法再更改，但仍可進行其他編輯。\",\n    \"description\": \"Description for the instant recording mode\"\n  },\n  \"instantRecordingModeAction\": {\n    \"message\": \"了解了\",\n    \"description\": \"Action button for the instant recording mode\"\n  },\n  \"tabRecordingDisabledTooltip\": {\n    \"message\": \"此頁面停用了分頁錄製\",\n    \"description\": \"Tooltip shown when tab recording is disabled on a page\"\n  },\n  \"addingToLabel\": {\n    \"message\": \"新增至：\",\n    \"description\": \"Label shown when a scene is being recorded to a project\"\n  },\n  \"multiLabel\": {\n    \"message\": \"Multi\",\n    \"description\": \"Label for the multi-recording mode\"\n  },\n  \"finishLabelMulti\": {\n    \"message\": \"完成\",\n    \"description\": \"Label for the finish multi-recording button\"\n  },\n  \"welcomeProTitle\": {\n    \"message\": \"想讓錄製發揮更多效用？\",\n    \"description\": \"Title for the welcome Pro popup\"\n  },\n  \"welcomeProDescription\": {\n    \"message\": \"登入即可將影片儲存至雲端、使用連結分享，並存取進階編輯功能。\",\n    \"description\": \"Description for the welcome Pro popup\"\n  },\n  \"welcomeProButton\": {\n    \"message\": \"登入以解鎖付費功能\",\n    \"description\": \"Button to sign in to Screenity Pro\"\n  },\n  \"welcomeProSupport\": {\n    \"message\": \"支持獨立創作者的開發工作\",\n    \"description\": \"Message for supporting the indie maker in the welcome Pro popup\"\n  },\n  \"unlockMoreFeatures\": {\n    \"message\": \"解鎖更多功能\",\n    \"description\": \"Button to unlock more features in the editor\"\n  },\n  \"shareUnlockButton\": {\n    \"message\": \"登入以分享（Pro）\",\n    \"description\": \"Button to sign in to share in the editor\"\n  },\n  \"proBannerTitle\": {\n    \"message\": \"解鎖雲端分享、多場景編輯、自動縮放、字幕等功能\",\n    \"description\": \"Pro banner title in the editor\"\n  },\n  \"proBannerDescription\": {\n    \"message\": \"Screenity 永遠免費、開源且無廣告。Pro 協助支付雲端與基礎建設費用，並支持一人開發的努力 ❤️\",\n    \"description\": \"Pro banner description in the editor\"\n  },\n  \"proBannerAltButton\": {\n    \"message\": \"不要再顯示\",\n    \"description\": \"Pro banner don't show again button in the editor\"\n  },\n  \"proBannerButton\": {\n    \"message\": \"立即試用\",\n    \"description\": \"Pro banner button in the editor\"\n  },\n  \"multiRecordingModeTitle\": {\n    \"message\": \"Multi 模式\",\n    \"description\": \"Title for the multi-recording mode modal\"\n  },\n  \"multiRecordingModeDescription\": {\n    \"message\": \"依序錄製多個場景，例如螢幕、攝影機或兩者皆錄。非常適合多次拍攝、切換視角或將錄製內容分段。完成後，點選「完成」以開啟編輯器，所有場景將合併為單一影片。\",\n    \"description\": \"Description for the multi-recording mode modal\"\n  },\n  \"noSubscriptionYetTitle\": {\n    \"message\": \"啟用 Pro 開始使用\",\n    \"description\": \"Title shown when the user has no active subscription\"\n  },\n  \"noSubscriptionYetDescription\": {\n    \"message\": \"請訂閱以存取 Pro 功能。\",\n    \"description\": \"Message prompting the user to subscribe to reactivate their Pro features\"\n  },\n  \"upgradeToProButton\": {\n    \"message\": \"訂閱 Pro\",\n    \"description\": \"Button to upgrade to Pro\"\n  },\n  \"hostBannerTitle\": {\n    \"message\": \"喜歡本地使用 Screenity 嗎？歡迎支持未來開發！\",\n    \"description\": \"編輯器中的自架設橫幅標題\"\n  },\n  \"hostBannerDescription\": {\n    \"message\": \"Screenity 是由一位獨立女性開發者打造並維護的。自架設是免費的，但如果你覺得它對你有幫助，請考慮透過 Pro 版支持她的開發工作 ❤️\",\n    \"description\": \"編輯器中的自架設橫幅描述\"\n  },\n  \"welcomeBackPopupTitle\": {\n    \"message\": \"歡迎回到 Screenity\",\n    \"description\": \"回訪使用者的歡迎彈窗標題\"\n  },\n  \"welcomeBackProTitle\": {\n    \"message\": \"✨ 新功能：讓錄製體驗更升級\",\n    \"description\": \"歡迎彈窗中雲端儲存功能的標題\"\n  },\n  \"welcomeBackProDescription\": {\n    \"message\": \"Screenity Pro 是一個全新可選平台，可將影片儲存到雲端、透過連結分享，並在需要時解鎖進階編輯工具。\",\n    \"description\": \"歡迎彈窗中雲端儲存功能的描述\"\n  },\n  \"welcomeBackProCTA\": {\n    \"message\": \"了解更多 Screenity Pro\",\n    \"description\": \"歡迎彈窗中雲端儲存功能的號召性按鈕\"\n  },\n  \"welcomeToProTitleModal\": {\n    \"message\": \"歡迎使用 Screenity Pro\",\n    \"description\": \"Pro 歡迎視窗標題\"\n  },\n  \"welcomeToProDescriptionModal\": {\n    \"message\": \"你已準備好開始錄製！請注意以下幾點：\\n\\n- 錄製時，相機泡泡可能會消失，這是正常現象。它會在背景中單獨捕捉，讓你之後可以自由調整位置。\\n- 縮放功能僅在點擊 Chrome 分頁時自動建立，這是瀏覽器的限制。你仍然可以在錄製後手動新增縮放效果。\\n- 想快速錄製並立即下載？可以試試即時模式。但請注意，編輯功能有限：無法使用背景、版面或進階調整。\",\n    \"description\": \"Pro 歡迎視窗描述\"\n  },\n  \"welcomeToProActionModal\": {\n    \"message\": \"了解\",\n    \"description\": \"Pro 歡迎視窗確認按鈕\"\n  },\n  \"webcodecsToggleLabel\": {\n    \"message\": \"快速 MP4（beta）\",\n    \"description\": \"WebCodecs recorder toggle label\"\n  },\n  \"webcodecsFailedOffToast\": {\n    \"message\": \"快速 MP4 失敗 — 關閉\",\n    \"description\": \"WebCodecs failure auto-disable toast\"\n  },\n  \"proOnboardingWelcomeTitle\": {\n    \"message\": \"歡迎使用 Screenity Pro 擴充功能\",\n    \"description\": \"Pro onboarding welcome step title\"\n  },\n  \"proOnboardingWelcomeDescription\": {\n    \"message\": \"開始錄製前，先看幾個快速提示。<br/>自動縮放只會在 <strong>Chrome 分頁</strong>內點擊時生效。之後仍可手動新增縮放。\",\n    \"description\": \"Pro onboarding welcome step description\"\n  },\n  \"proOnboardingToolbarTitle\": {\n    \"message\": \"錄製工具列與效果\",\n    \"description\": \"Pro onboarding toolbar step title\"\n  },\n  \"proOnboardingToolbarDescription\": {\n    \"message\": \"可使用此工具列進行繪圖、游標效果、模糊與錄製控制。<br/><br/><strong>若未隱藏，這個工具列會出現在影片中</strong>。\",\n    \"description\": \"Pro onboarding toolbar step description\"\n  },\n  \"proOnboardingCameraTitle\": {\n    \"message\": \"攝影機會分開擷取\",\n    \"description\": \"Pro onboarding camera step title\"\n  },\n  \"proOnboardingCameraDescription\": {\n    \"message\": \"錄製時攝影機可能會<strong>隱藏或切換到 PiP</strong>，這是正常的。<br/><br/>它會分開擷取，方便你之後調整位置。\",\n    \"description\": \"Pro onboarding camera step description\"\n  },\n  \"proOnboardingInstantTitle\": {\n    \"message\": \"即時模式\",\n    \"description\": \"Pro onboarding instant mode step title\"\n  },\n  \"proOnboardingInstantDescription\": {\n    \"message\": \"最適合快速分享，並提供<strong>無限制下載</strong>。<br/><br/>此模式不提供進階版面與編輯選項。\",\n    \"description\": \"Pro onboarding instant mode step description\"\n  },\n  \"proOnboardingDone\": {\n    \"message\": \"知道了\",\n    \"description\": \"Pro onboarding done button text\"\n  },\n  \"proOnboardingNext\": {\n    \"message\": \"下一步\",\n    \"description\": \"Pro onboarding next button text\"\n  },\n  \"proOnboardingBack\": {\n    \"message\": \"返回\",\n    \"description\": \"Pro onboarding back button text\"\n  },\n  \"proOnboardingToolbarLearnMore\": {\n    \"message\": \"瞭解更多\",\n    \"description\": \"Pro onboarding toolbar learn more link label\"\n  },\n  \"resetOnboardingOption\": {\n    \"message\": \"重設導覽\",\n    \"description\": \"Reset pro onboarding option label in popup settings menu\"\n  },\n  \"recoverLastRecordingOption\": {\n    \"message\": \"Recover last recording\",\n    \"description\": \"Settings menu item to manually trigger cloud recording recovery\"\n  },\n  \"toastRecoveredSession\": {\n    \"message\": \"Recovered unsaved recording from the previous session.\",\n    \"description\": \"Toast shown when a crashed recording session is auto-recovered on startup\"\n  },\n  \"toastStorageCritical\": {\n    \"message\": \"磁碟空間不足，正在儲存錄製內容。已錄製的內容是安全的。\",\n    \"description\": \"Toast shown when available storage drops below the critical threshold during recording; reassures that captured data is preserved\"\n  },\n  \"toastStorageLow\": {\n    \"message\": \"Storage is running low. Upload reliability may degrade for long recordings.\",\n    \"description\": \"Toast shown when available storage drops below the warning threshold during recording\"\n  },\n  \"toastAudioCaptureDegraded\": {\n    \"message\": \"Transcription capture paused due to memory pressure. Video upload continues.\",\n    \"description\": \"Toast shown when the audio recorder is stopped early because the audio buffer limit was reached\"\n  },\n  \"toastNetworkOffline\": {\n    \"message\": \"Network offline. Upload paused. Screen chunks are saved locally.\",\n    \"description\": \"Toast shown when the browser goes offline during a cloud recording upload\"\n  },\n  \"longRecordingTitle\": {\n    \"message\": \"長時間錄製\",\n    \"description\": \"Banner title shown when a recording exceeds the local processing limit but is otherwise successful\"\n  },\n  \"longRecordingDescription\": {\n    \"message\": \"您的錄製已完成且安全。請在下方下載 WebM 檔案。\",\n    \"description\": \"Banner description for long recordings that can be downloaded but not edited locally\"\n  },\n  \"notAvailableLongRecording\": {\n    \"message\": \"長時間錄製無法使用\",\n    \"description\": \"Disabled button label for features not available due to recording length\"\n  },\n  \"notAvailableRecoveryMode\": {\n    \"message\": \"復原模式下無法使用\",\n    \"description\": \"Disabled button label for features not available in recovery/fallback mode\"\n  },\n  \"editTooLongTitle\": {\n    \"message\": \"音訊編輯內容過長\",\n    \"description\": \"Alert title when an audio mute/mix edit is rejected because the clip exceeds 15 minutes\"\n  },\n  \"editTooLongDescription\": {\n    \"message\": \"此音訊編輯不支援超過15分鐘的片段。原始檔案未被修改。請先嘗試裁剪片段。\",\n    \"description\": \"Alert body explaining the 15-minute limit and suggesting trimming first\"\n  },\n  \"editTimeoutTitle\": {\n    \"message\": \"編輯逾時\",\n    \"description\": \"Alert title when an edit operation does not complete within the 5-minute watchdog period\"\n  },\n  \"editTimeoutDescription\": {\n    \"message\": \"編輯未能及時完成。原始錄製內容未被修改。請重試，或先裁剪片段。\",\n    \"description\": \"Alert body for the timeout case, prompting retry or shorter clip\"\n  },\n  \"streamEndedWarningToast\": {\n    \"message\": \"螢幕分享已停止。您的錄製內容已儲存，準備好後即可停止。\",\n    \"description\": \"Toast shown when screen share track ends mid-recording. Recording continues; removes false urgency about data loss.\"\n  },\n  \"recoveryModeDescription\": {\n    \"message\": \"處理時出現問題，但您的錄製內容是安全的。您可以在下方下載。\",\n    \"description\": \"Recovery mode banner description — reassures user their recording is accessible despite the processing failure\"\n  },\n  \"editProcessingSafeDescription\": {\n    \"message\": \"正在處理編輯。原始錄製內容未受影響。\",\n    \"description\": \"Shown in the editor processing alert when a user-initiated edit is running, to reassure the original is safe\"\n  },\n  \"editFailedTitle\": {\n    \"message\": \"無法套用編輯\",\n    \"description\": \"Alert title when an edit operation fails due to an encoding or processing error\"\n  },\n  \"editFailedDescription\": {\n    \"message\": \"處理過程中出現問題。原始錄製內容是安全的。您可以重試或直接下載。\",\n    \"description\": \"Alert body for edit failure; explicitly confirms the original recording is untouched\"\n  },\n  \"videoTrackEndedToast\": {\n    \"message\": \"螢幕分享已停止，正在儲存錄製內容。已錄製的內容是安全的。\",\n    \"description\": \"Toast shown when the video track ends mid-recording (e.g. monitor disconnected, screen share revoked)\"\n  },\n  \"audioTrackEndedToast\": {\n    \"message\": \"音訊已中斷，正在儲存錄製內容。已錄製的影片是安全的。\",\n    \"description\": \"Toast shown when the audio track ends mid-recording (e.g. mic unplugged, permission revoked)\"\n  },\n  \"editorRecoveryToast\": {\n    \"message\": \"錄製內容已儲存。編輯器未開啟，請點擊 Screenity 圖示重試。\",\n    \"description\": \"Toast shown on the active tab when the post-recording editor tab fails to open\"\n  },\n  \"stopAckTimeoutToast\": {\n    \"message\": \"儲存時間比預期長。您的資料是安全的，編輯器即將開啟。\",\n    \"description\": \"Toast shown when the recorder does not acknowledge the stop request within a few seconds\"\n  },\n  \"copyDebugInfoButton\": {\n    \"message\": \"複製除錯資訊\",\n    \"description\": \"Button to copy technical debug info to clipboard for support\"\n  },\n  \"debugInfoCopiedToast\": {\n    \"message\": \"除錯資訊已複製\",\n    \"description\": \"Toast shown after debug info is copied\"\n  },\n  \"getHelpButton\": {\n    \"message\": \"取得幫助\",\n    \"description\": \"Side button on error modals that opens a prefilled support form\"\n  },\n  \"recordingTooShortToast\": {\n    \"message\": \"錄製時間太短，無法儲存\",\n    \"description\": \"Toast shown when a region recording is stopped by navigation but no chunks were saved\"\n  },\n  \"fastRecorderFailedTitle\": {\n    \"message\": \"快速錄製器失敗\",\n    \"description\": \"Title for the modal shown when the fast MP4 recorder output could not be validated\"\n  },\n  \"fastRecorderFailedDescription\": {\n    \"message\": \"快速錄製器的輸出無法在此裝置上驗證。\\n您仍可以下載該檔案。\",\n    \"description\": \"Body text for the fast recorder failure modal\"\n  },\n  \"downloadAnywayButton\": {\n    \"message\": \"仍然下載\",\n    \"description\": \"Button to download the recording file despite validation failure\"\n  },\n  \"cancelButton\": {\n    \"message\": \"取消\",\n    \"description\": \"Generic cancel button label\"\n  }\n}\n"
  },
  {
    "path": "src/assets/editor/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"Web site created using create-react-app\"\n    />\n\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n\t\t    <!--\n\t\t<script src=\"%PUBLIC_URL%/coi-serviceworker.min.js\"></script>\n\t\t    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n\t\t<script src=\"%PUBLIC_URL%/ffmpeg.min.js\"></script>\n\t\t<script src=\"%PUBLIC_URL%/ffmpeg-core.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/assets/fonts/fonts.css",
    "content": "@font-face {\n  font-family: Satoshi-Light;\n  src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: Satoshi-Medium;\n  src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: Satoshi-Bold;\n  src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf) format('truetype');\n  font-weight: normal;\n  font-style: normal;\n}\n\n@font-face {\n\tfont-family: Gloria-Hallelujah;\n\tsrc: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular.ttf) format('truetype');\n}"
  },
  {
    "path": "src/assets/mediapipeVision/vision_wasm_internal.js",
    "content": "var ModuleFactory=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!=\"renderer\";var arguments_=[];var thisProgram=\"./this.program\";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!=\"undefined\"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory=\"\";function locateFile(path){if(Module[\"locateFile\"]){return Module[\"locateFile\"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require(\"fs\");scriptDirectory=__dirname+\"/\";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:\"utf8\");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\\\/g,\"/\")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(\".\",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,false);xhr.responseType=\"arraybuffer\";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,true);xhr.responseType=\"arraybuffer\";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:\"same-origin\"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+\" : \"+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var isFileURI=filename=>filename.startsWith(\"file://\");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module[\"HEAPU8\"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module[\"HEAPU32\"]=HEAPU32=new Uint32Array(b);Module[\"HEAPF32\"]=HEAPF32=new Float32Array(b);Module[\"HEAPF64\"]=HEAPF64=new Float64Array(b)}function preRun(){if(Module[\"preRun\"]){if(typeof Module[\"preRun\"]==\"function\")Module[\"preRun\"]=[Module[\"preRun\"]];while(Module[\"preRun\"].length){addOnPreRun(Module[\"preRun\"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module[\"noFSInit\"]&&!FS.initialized)FS.init();TTY.init();wasmExports[\"jd\"]();FS.ignorePermissions=false}function postRun(){if(Module[\"postRun\"]){if(typeof Module[\"postRun\"]==\"function\")Module[\"postRun\"]=[Module[\"postRun\"]];while(Module[\"postRun\"].length){addOnPostRun(Module[\"postRun\"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module[\"onAbort\"]?.(what);what=\"Aborted(\"+what+\")\";err(what);ABORT=true;what+=\". Build with -sASSERTIONS for more info.\";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile(\"vision_wasm_internal.wasm\")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw\"both async and sync fetching of the wasm failed\"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:\"same-origin\"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err(\"falling back to ArrayBuffer instantiation\")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result[\"instance\"])}var info=getWasmImports();if(Module[\"instantiateWasm\"]){return new Promise((resolve,reject)=>{Module[\"instantiateWasm\"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}var tempDouble;var tempI64;var handleException=e=>{if(e instanceof ExitStatus||e==\"unwind\"){return EXITSTATUS}quit_(1,e)};class ExitStatus{name=\"ExitStatus\";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module[\"onExit\"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};function getFullscreenElement(){return document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement||document.msFullscreenElement}var safeSetTimeout=(func,timeout)=>setTimeout(()=>{callUserCallback(func)},timeout);var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text=\"warning: \"+text;err(text)}};var preloadPlugins=[];var Browser={useWebGL:false,isFullscreen:false,pointerLock:false,moduleContextCreatedCallbacks:[],workers:[],preloadedImages:{},preloadedAudios:{},getCanvas:()=>Module[\"canvas\"],init(){if(Browser.initted)return;Browser.initted=true;var imagePlugin={};imagePlugin[\"canHandle\"]=function imagePlugin_canHandle(name){return!Module[\"noImageDecoding\"]&&/\\.(jpg|jpeg|png|bmp|webp)$/i.test(name)};imagePlugin[\"handle\"]=async function imagePlugin_handle(byteArray,name){var b=new Blob([byteArray],{type:Browser.getMimetype(name)});if(b.size!==byteArray.length){b=new Blob([new Uint8Array(byteArray).buffer],{type:Browser.getMimetype(name)})}var url=URL.createObjectURL(b);return new Promise((resolve,reject)=>{var img=new Image;img.onload=()=>{var canvas=document.createElement(\"canvas\");canvas.width=img.width;canvas.height=img.height;var ctx=canvas.getContext(\"2d\");ctx.drawImage(img,0,0);Browser.preloadedImages[name]=canvas;URL.revokeObjectURL(url);resolve(byteArray)};img.onerror=event=>{err(`Image ${url} could not be decoded`);reject()};img.src=url})};preloadPlugins.push(imagePlugin);var audioPlugin={};audioPlugin[\"canHandle\"]=function audioPlugin_canHandle(name){return!Module[\"noAudioDecoding\"]&&name.slice(-4)in{\".ogg\":1,\".wav\":1,\".mp3\":1}};audioPlugin[\"handle\"]=async function audioPlugin_handle(byteArray,name){return new Promise((resolve,reject)=>{var done=false;function finish(audio){if(done)return;done=true;Browser.preloadedAudios[name]=audio;resolve(byteArray)}var b=new Blob([byteArray],{type:Browser.getMimetype(name)});var url=URL.createObjectURL(b);var audio=new Audio;audio.addEventListener(\"canplaythrough\",()=>finish(audio),false);audio.onerror=function audio_onerror(event){if(done)return;err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`);function encode64(data){var BASE=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";var PAD=\"=\";var ret=\"\";var leftchar=0;var leftbits=0;for(var i=0;i<data.length;i++){leftchar=leftchar<<8|data[i];leftbits+=8;while(leftbits>=6){var curr=leftchar>>leftbits-6&63;leftbits-=6;ret+=BASE[curr]}}if(leftbits==2){ret+=BASE[(leftchar&3)<<4];ret+=PAD+PAD}else if(leftbits==4){ret+=BASE[(leftchar&15)<<2];ret+=PAD}return ret}audio.src=\"data:audio/x-\"+name.slice(-3)+\";base64,\"+encode64(byteArray);finish(audio)};audio.src=url;safeSetTimeout(()=>{finish(audio)},1e4)})};preloadPlugins.push(audioPlugin);function pointerLockChange(){var canvas=Browser.getCanvas();Browser.pointerLock=document.pointerLockElement===canvas}var canvas=Browser.getCanvas();if(canvas){document.addEventListener(\"pointerlockchange\",pointerLockChange,false);if(Module[\"elementPointerLock\"]){canvas.addEventListener(\"click\",ev=>{if(!Browser.pointerLock&&Browser.getCanvas().requestPointerLock){Browser.getCanvas().requestPointerLock();ev.preventDefault()}},false)}}},createContext(canvas,useWebGL,setInModule,webGLContextAttributes){if(useWebGL&&Module[\"ctx\"]&&canvas==Browser.getCanvas())return Module[\"ctx\"];var ctx;var contextHandle;if(useWebGL){var contextAttributes={antialias:false,alpha:false,majorVersion:typeof WebGL2RenderingContext!=\"undefined\"?2:1};if(webGLContextAttributes){for(var attribute in webGLContextAttributes){contextAttributes[attribute]=webGLContextAttributes[attribute]}}if(typeof GL!=\"undefined\"){contextHandle=GL.createContext(canvas,contextAttributes);if(contextHandle){ctx=GL.getContext(contextHandle).GLctx}}}else{ctx=canvas.getContext(\"2d\")}if(!ctx)return null;if(setInModule){Module[\"ctx\"]=ctx;if(useWebGL)GL.makeContextCurrent(contextHandle);Browser.useWebGL=useWebGL;Browser.moduleContextCreatedCallbacks.forEach(callback=>callback());Browser.init()}return ctx},fullscreenHandlersInstalled:false,lockPointer:undefined,resizeCanvas:undefined,requestFullscreen(lockPointer,resizeCanvas){Browser.lockPointer=lockPointer;Browser.resizeCanvas=resizeCanvas;if(typeof Browser.lockPointer==\"undefined\")Browser.lockPointer=true;if(typeof Browser.resizeCanvas==\"undefined\")Browser.resizeCanvas=false;var canvas=Browser.getCanvas();function fullscreenChange(){Browser.isFullscreen=false;var canvasContainer=canvas.parentNode;if(getFullscreenElement()===canvasContainer){canvas.exitFullscreen=Browser.exitFullscreen;if(Browser.lockPointer)canvas.requestPointerLock();Browser.isFullscreen=true;if(Browser.resizeCanvas){Browser.setFullscreenCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}else{canvasContainer.parentNode.insertBefore(canvas,canvasContainer);canvasContainer.parentNode.removeChild(canvasContainer);if(Browser.resizeCanvas){Browser.setWindowedCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}Module[\"onFullScreen\"]?.(Browser.isFullscreen);Module[\"onFullscreen\"]?.(Browser.isFullscreen)}if(!Browser.fullscreenHandlersInstalled){Browser.fullscreenHandlersInstalled=true;document.addEventListener(\"fullscreenchange\",fullscreenChange,false);document.addEventListener(\"mozfullscreenchange\",fullscreenChange,false);document.addEventListener(\"webkitfullscreenchange\",fullscreenChange,false);document.addEventListener(\"MSFullscreenChange\",fullscreenChange,false)}var canvasContainer=document.createElement(\"div\");canvas.parentNode.insertBefore(canvasContainer,canvas);canvasContainer.appendChild(canvas);canvasContainer.requestFullscreen=canvasContainer[\"requestFullscreen\"]||canvasContainer[\"mozRequestFullScreen\"]||canvasContainer[\"msRequestFullscreen\"]||(canvasContainer[\"webkitRequestFullscreen\"]?()=>canvasContainer[\"webkitRequestFullscreen\"](Element[\"ALLOW_KEYBOARD_INPUT\"]):null)||(canvasContainer[\"webkitRequestFullScreen\"]?()=>canvasContainer[\"webkitRequestFullScreen\"](Element[\"ALLOW_KEYBOARD_INPUT\"]):null);canvasContainer.requestFullscreen()},exitFullscreen(){if(!Browser.isFullscreen){return false}var CFS=document[\"exitFullscreen\"]||document[\"cancelFullScreen\"]||document[\"mozCancelFullScreen\"]||document[\"msExitFullscreen\"]||document[\"webkitCancelFullScreen\"]||(()=>{});CFS.apply(document,[]);return true},safeSetTimeout(func,timeout){return safeSetTimeout(func,timeout)},getMimetype(name){return{jpg:\"image/jpeg\",jpeg:\"image/jpeg\",png:\"image/png\",bmp:\"image/bmp\",ogg:\"audio/ogg\",wav:\"audio/wav\",mp3:\"audio/mpeg\"}[name.slice(name.lastIndexOf(\".\")+1)]},getUserMedia(func){window.getUserMedia||=navigator[\"getUserMedia\"]||navigator[\"mozGetUserMedia\"];window.getUserMedia(func)},getMovementX(event){return event[\"movementX\"]||event[\"mozMovementX\"]||event[\"webkitMovementX\"]||0},getMovementY(event){return event[\"movementY\"]||event[\"mozMovementY\"]||event[\"webkitMovementY\"]||0},getMouseWheelDelta(event){var delta=0;switch(event.type){case\"DOMMouseScroll\":delta=event.detail/3;break;case\"mousewheel\":delta=event.wheelDelta/120;break;case\"wheel\":delta=event.deltaY;switch(event.deltaMode){case 0:delta/=100;break;case 1:delta/=3;break;case 2:delta*=80;break;default:abort(\"unrecognized mouse wheel delta mode: \"+event.deltaMode)}break;default:abort(\"unrecognized mouse wheel event: \"+event.type)}return delta},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseCoords(pageX,pageY){var canvas=Browser.getCanvas();var rect=canvas.getBoundingClientRect();var scrollX=typeof window.scrollX!=\"undefined\"?window.scrollX:window.pageXOffset;var scrollY=typeof window.scrollY!=\"undefined\"?window.scrollY:window.pageYOffset;var adjustedX=pageX-(scrollX+rect.left);var adjustedY=pageY-(scrollY+rect.top);adjustedX=adjustedX*(canvas.width/rect.width);adjustedY=adjustedY*(canvas.height/rect.height);return{x:adjustedX,y:adjustedY}},setMouseCoords(pageX,pageY){const{x,y}=Browser.calculateMouseCoords(pageX,pageY);Browser.mouseMovementX=x-Browser.mouseX;Browser.mouseMovementY=y-Browser.mouseY;Browser.mouseX=x;Browser.mouseY=y},calculateMouseEvent(event){if(Browser.pointerLock){if(event.type!=\"mousemove\"&&\"mozMovementX\"in event){Browser.mouseMovementX=Browser.mouseMovementY=0}else{Browser.mouseMovementX=Browser.getMovementX(event);Browser.mouseMovementY=Browser.getMovementY(event)}Browser.mouseX+=Browser.mouseMovementX;Browser.mouseY+=Browser.mouseMovementY}else{if(event.type===\"touchstart\"||event.type===\"touchend\"||event.type===\"touchmove\"){var touch=event.touch;if(touch===undefined){return}var coords=Browser.calculateMouseCoords(touch.pageX,touch.pageY);if(event.type===\"touchstart\"){Browser.lastTouches[touch.identifier]=coords;Browser.touches[touch.identifier]=coords}else if(event.type===\"touchend\"||event.type===\"touchmove\"){var last=Browser.touches[touch.identifier];last||=coords;Browser.lastTouches[touch.identifier]=last;Browser.touches[touch.identifier]=coords}return}Browser.setMouseCoords(event.pageX,event.pageY)}},resizeListeners:[],updateResizeListeners(){var canvas=Browser.getCanvas();Browser.resizeListeners.forEach(listener=>listener(canvas.width,canvas.height))},setCanvasSize(width,height,noUpdates){var canvas=Browser.getCanvas();Browser.updateCanvasDimensions(canvas,width,height);if(!noUpdates)Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize(){if(typeof SDL!=\"undefined\"){var flags=HEAPU32[SDL.screen>>2];flags=flags|8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Browser.getCanvas());Browser.updateResizeListeners()},setWindowedCanvasSize(){if(typeof SDL!=\"undefined\"){var flags=HEAPU32[SDL.screen>>2];flags=flags&~8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Browser.getCanvas());Browser.updateResizeListeners()},updateCanvasDimensions(canvas,wNative,hNative){if(wNative&&hNative){canvas.widthNative=wNative;canvas.heightNative=hNative}else{wNative=canvas.widthNative;hNative=canvas.heightNative}var w=wNative;var h=hNative;if(Module[\"forcedAspectRatio\"]>0){if(w/h<Module[\"forcedAspectRatio\"]){w=Math.round(h*Module[\"forcedAspectRatio\"])}else{h=Math.round(w/Module[\"forcedAspectRatio\"])}}if(getFullscreenElement()===canvas.parentNode&&typeof screen!=\"undefined\"){var factor=Math.min(screen.width/w,screen.height/h);w=Math.round(w*factor);h=Math.round(h*factor)}if(Browser.resizeCanvas){if(canvas.width!=w)canvas.width=w;if(canvas.height!=h)canvas.height=h;if(typeof canvas.style!=\"undefined\"){canvas.style.removeProperty(\"width\");canvas.style.removeProperty(\"height\")}}else{if(canvas.width!=wNative)canvas.width=wNative;if(canvas.height!=hNative)canvas.height=hNative;if(typeof canvas.style!=\"undefined\"){if(w!=wNative||h!=hNative){canvas.style.setProperty(\"width\",w+\"px\",\"important\");canvas.style.setProperty(\"height\",h+\"px\",\"important\")}else{canvas.style.removeProperty(\"width\");canvas.style.removeProperty(\"height\")}}}}};var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)===\"/\",splitPath:filename=>{var splitPathRe=/^(\\/?|)([\\s\\S]*?)((?:\\.{1,2}|[^\\/]+?|)(\\.[^.\\/]*|))(?:[\\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last===\".\"){parts.splice(i,1)}else if(last===\"..\"){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift(\"..\")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)===\"/\";path=PATH.normalizeArray(path.split(\"/\").filter(p=>!!p),!isAbsolute).join(\"/\");if(!path&&!isAbsolute){path=\".\"}if(path&&trailingSlash){path+=\"/\"}return(isAbsolute?\"/\":\"\")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return\".\"}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\\/]+|\\/)\\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join(\"/\")),join2:(l,r)=>PATH.normalize(l+\"/\"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require(\"crypto\");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath=\"\",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!=\"string\"){throw new TypeError(\"Arguments to path.resolve must be strings\")}else if(!path){return\"\"}resolvedPath=path+\"/\"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split(\"/\").filter(p=>!!p),!resolvedAbsolute).join(\"/\");return(resolvedAbsolute?\"/\":\"\")+resolvedPath||\".\"},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[start]!==\"\")break}var end=arr.length-1;for(;end>=0;end--){if(arr[end]!==\"\")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split(\"/\"));var toParts=trim(to.split(\"/\"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i<length;i++){if(fromParts[i]!==toParts[i]){samePartsLength=i;break}}var outputParts=[];for(var i=samePartsLength;i<fromParts.length;i++){outputParts.push(\"..\")}outputParts=outputParts.concat(toParts.slice(samePartsLength));return outputParts.join(\"/\")}};var UTF8Decoder=new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(heapOrArray.buffer?heapOrArray.subarray(idx,endPtr):new Uint8Array(heapOrArray.slice(idx,endPtr)))};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes(\"EOF\"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString(\"utf-8\")}}else if(globalThis.window?.prompt){result=window.prompt(\"Input: \");if(result!==null){result+=\"\\n\"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i<length;i++){var result;try{result=stream.tty.ops.get_char(stream.tty)}catch(e){throw new FS.ErrnoError(29)}if(result===undefined&&bytesRead===0){throw new FS.ErrnoError(6)}if(result===null||result===undefined)break;bytesRead++;buffer[offset+i]=result}if(bytesRead){stream.node.atime=Date.now()}return bytesRead},write(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.put_char){throw new FS.ErrnoError(60)}try{for(var i=0;i<length;i++){stream.tty.ops.put_char(stream.tty,buffer[offset+i])}}catch(e){throw new FS.ErrnoError(29)}if(length){stream.node.mtime=stream.node.ctime=Date.now()}return i}},default_tty_ops:{get_char(tty){return FS_stdin_getChar()},put_char(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,\"/\",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity<CAPACITY_DOUBLING_MAX?2:1.125)>>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of[\"mode\",\"atime\",\"mtime\",\"ctime\"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=\"<generic error, no stack>\"}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[\".\",\"..\",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i<size;i++)buffer[offset+i]=contents[position+i]}return size},write(stream,buffer,offset,length,position,canOwn){if(buffer.buffer===HEAP8.buffer){canOwn=false}if(!length)return 0;var node=stream.node;node.mtime=node.ctime=Date.now();if(buffer.subarray&&(!node.contents||node.contents.subarray)){if(canOwn){node.contents=buffer.subarray(offset,offset+length);node.usedBytes=length;return length}else if(node.usedBytes===0&&position===0){node.contents=buffer.slice(offset,offset+length);node.usedBytes=length;return length}else if(position+length<=node.usedBytes){node.contents.set(buffer.subarray(offset,offset+length),position);return length}}MEMFS.expandFileStorage(node,position+length);if(node.contents.subarray&&buffer.subarray){node.contents.set(buffer.subarray(offset,offset+length),position)}else{for(var i=0;i<length;i++){node.contents[position+i]=buffer[offset+i]}}node.usedBytes=Math.max(node.usedBytes,position+length);return length},llseek(stream,offset,whence){var position=offset;if(whence===1){position+=stream.position}else if(whence===2){if(FS.isFile(stream.node.mode)){position+=stream.node.usedBytes}}if(position<0){throw new FS.ErrnoError(28)}return position},mmap(stream,length,position,prot,flags){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}var ptr;var allocated;var contents=stream.node.contents;if(!(flags&2)&&contents&&contents.buffer===HEAP8.buffer){allocated=false;ptr=contents.byteOffset}else{allocated=true;ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}if(contents){if(position>0||position+length<contents.length){if(contents.subarray){contents=contents.subarray(position,position+length)}else{contents=Array.prototype.slice.call(contents,position,position+length)}}HEAP8.set(contents,ptr)}}return{ptr,allocated}},msync(stream,buffer,offset,length,mmapFlags){MEMFS.stream_ops.write(stream,buffer,0,length,offset,false);return 0}}};var FS_modeStringToFlags=str=>{var flagModes={r:0,\"r+\":2,w:512|64|1,\"w+\":512|64|2,a:1024|64|1,\"a+\":1024|64|2};var flags=flagModes[str];if(typeof flags==\"undefined\"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module[\"monitorRunDependencies\"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module[\"monitorRunDependencies\"]?.(runDependencies)};var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!=\"undefined\")Browser.init();for(var plugin of preloadPlugins){if(plugin[\"canHandle\"](fullname)){return plugin[\"handle\"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url==\"string\"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:\"/\",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name=\"ErrnoError\";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+\"/\"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split(\"/\").filter(p=>!!p);var current=FS.root;var current_path=\"/\";for(var i=0;i<parts.length;i++){var islast=i===parts.length-1;if(islast&&opts.parent){break}if(parts[i]===\".\"){continue}if(parts[i]===\"..\"){current_path=PATH.dirname(current_path);if(FS.isRoot(current)){path=current_path+\"/\"+parts.slice(i+1).join(\"/\");nlinks--;continue linkloop}else{current=current.parent}continue}current_path=PATH.join2(current_path,parts[i]);try{current=FS.lookupNode(current,parts[i])}catch(e){if(e?.errno===44&&islast&&opts.noent_okay){return{path:current_path}}throw e}if(FS.isMountpoint(current)&&(!islast||opts.follow_mount)){current=current.mounted.root}if(FS.isLink(current.mode)&&(!islast||opts.follow)){if(!current.node_ops.readlink){throw new FS.ErrnoError(52)}var link=current.node_ops.readlink(current);if(!PATH.isAbs(link)){link=PATH.dirname(current_path)+\"/\"+link}path=link+\"/\"+parts.slice(i+1).join(\"/\");continue linkloop}}return{path:current_path,node:current}}throw new FS.ErrnoError(32)},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!==\"/\"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i<name.length;i++){hash=(hash<<5)-hash+name.charCodeAt(i)|0}return(parentid+hash>>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=[\"r\",\"w\",\"rw\"][flag&3];if(flag&512){perms+=\"w\"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes(\"r\")&&!(node.mode&292)){return 2}else if(perms.includes(\"w\")&&!(node.mode&146)){return 2}else if(perms.includes(\"x\")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,\"x\");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,\"wx\")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,\"wx\");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!==\"r\"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate==\"function\"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint===\"/\";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name===\".\"||name===\"..\"){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split(\"/\");var d=\"\";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+=\"/\";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev==\"undefined\"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!==\".\"){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!==\".\"){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,\"w\");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,\"w\");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===\"\"){throw new FS.ErrnoError(44)}flags=typeof flags==\"string\"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path==\"object\"){node=path}else{isDirPath=path.endsWith(\"/\");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module[\"logReadFiles\"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!=\"undefined\";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!=\"undefined\";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||\"binary\";if(opts.encoding!==\"utf8\"&&opts.encoding!==\"binary\"){abort(`Invalid encoding type \"${opts.encoding}\"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding===\"utf8\"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data==\"string\"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort(\"Unsupported data type\")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,\"x\");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir(\"/tmp\");FS.mkdir(\"/home\");FS.mkdir(\"/home/web_user\")},createDefaultDevices(){FS.mkdir(\"/dev\");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev(\"/dev/null\",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev(\"/dev/tty\",FS.makedev(5,0));FS.mkdev(\"/dev/tty1\",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice(\"/dev\",\"random\",randomByte);FS.createDevice(\"/dev\",\"urandom\",randomByte);FS.mkdir(\"/dev/shm\");FS.mkdir(\"/dev/shm/tmp\")},createSpecialDirectories(){FS.mkdir(\"/proc\");var proc_self=FS.mkdir(\"/proc/self\");FS.mkdir(\"/proc/self/fd\");FS.mount({mount(){var node=FS.createNode(proc_self,\"fd\",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:\"fake\"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},\"/proc/self/fd\")},createStandardStreams(input,output,error){if(input){FS.createDevice(\"/dev\",\"stdin\",input)}else{FS.symlink(\"/dev/tty\",\"/dev/stdin\")}if(output){FS.createDevice(\"/dev\",\"stdout\",null,output)}else{FS.symlink(\"/dev/tty\",\"/dev/stdout\")}if(error){FS.createDevice(\"/dev\",\"stderr\",null,error)}else{FS.symlink(\"/dev/tty1\",\"/dev/stderr\")}var stdin=FS.open(\"/dev/stdin\",0);var stdout=FS.open(\"/dev/stdout\",1);var stderr=FS.open(\"/dev/stderr\",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},\"/\");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module[\"stdin\"];output??=Module[\"stdout\"];error??=Module[\"stderr\"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path===\"/\"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent==\"string\"?parent:FS.getPath(parent);var parts=path.split(\"/\").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent==\"string\"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent==\"string\"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data==\"string\"){var arr=new Array(data.length);for(var i=0,len=data.length;i<len;++i)arr[i]=data.charCodeAt(i);data=arr}FS.chmod(node,mode|146);var stream=FS.open(node,577);FS.write(stream,data,0,data.length,0,canOwn);FS.close(stream);FS.chmod(node,mode)}},createDevice(parent,name,input,output){var path=PATH.join2(typeof parent==\"string\"?parent:FS.getPath(parent),name);var mode=FS_getMode(!!input,!!output);FS.createDevice.major??=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open(stream){stream.seekable=false},close(stream){if(output?.buffer?.length){output(10)}},read(stream,buffer,offset,length,pos){var bytesRead=0;for(var i=0;i<length;i++){var result;try{result=input()}catch(e){throw new FS.ErrnoError(29)}if(result===undefined&&bytesRead===0){throw new FS.ErrnoError(6)}if(result===null||result===undefined)break;bytesRead++;buffer[offset+i]=result}if(bytesRead){stream.node.atime=Date.now()}return bytesRead},write(stream,buffer,offset,length,pos){for(var i=0;i<length;i++){try{output(buffer[offset+i])}catch(e){throw new FS.ErrnoError(29)}}if(length){stream.node.mtime=stream.node.ctime=Date.now()}return i}});return FS.mkdev(path,mode,dev)},forceLoadFile(obj){if(obj.isDevice||obj.isFolder||obj.link||obj.contents)return true;if(globalThis.XMLHttpRequest){abort(\"Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.\")}else{try{obj.contents=readBinary(obj.url)}catch(e){throw new FS.ErrnoError(29)}}},createLazyFile(parent,name,url,canRead,canWrite){class LazyUint8Array{lengthKnown=false;chunks=[];get(idx){if(idx>this.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open(\"HEAD\",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort(\"Couldn't load \"+url+\". Status: \"+xhr.status);var datalength=Number(xhr.getResponseHeader(\"Content-length\"));var header;var hasByteServing=(header=xhr.getResponseHeader(\"Accept-Ranges\"))&&header===\"bytes\";var usesGzip=(header=xhr.getResponseHeader(\"Content-Encoding\"))&&header===\"gzip\";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort(\"invalid range (\"+from+\", \"+to+\") or no bytes requested!\");if(to>datalength-1)abort(\"only \"+datalength+\" bytes available! programmer error!\");var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,false);if(datalength!==chunkSize)xhr.setRequestHeader(\"Range\",\"bytes=\"+from+\"-\"+to);xhr.responseType=\"arraybuffer\";if(xhr.overrideMimeType){xhr.overrideMimeType(\"text/plain; charset=x-user-defined\")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort(\"Couldn't load \"+url+\". Status: \"+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||\"\",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]==\"undefined\"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]==\"undefined\")abort(\"doXHR failed!\");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out(\"LazyFiles on gzip forces download of the whole file when length is accessed\")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort(\"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc\");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i<size;i++){buffer[offset+i]=contents[position+i]}}else{for(var i=0;i<size;i++){buffer[offset+i]=contents.get(position+i)}}return size}stream_ops.read=(stream,buffer,offset,length,position)=>{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>{if(!ptr)return\"\";var end=findStringEnd(HEAPU8,ptr,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(HEAPU8.subarray(ptr,end))};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+\"/\"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;tempI64=[stats.blocks>>>0,(tempDouble=stats.blocks,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+8>>2]=tempI64[0],HEAP32[buf+12>>2]=tempI64[1];tempI64=[stats.bfree>>>0,(tempDouble=stats.bfree,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+16>>2]=tempI64[0],HEAP32[buf+20>>2]=tempI64[1];tempI64=[stats.bavail>>>0,(tempDouble=stats.bavail,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];tempI64=[stats.files>>>0,(tempDouble=stats.files,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+32>>2]=tempI64[0],HEAP32[buf+36>>2]=tempI64[1];tempI64=[stats.ffree>>>0,(tempDouble=stats.ffree,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_dup(fd){try{var old=SYSCALLS.getStreamFromFD(fd);return FS.dupStream(old).fd}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms=\"\";if(amode&4)perms+=\"r\";if(amode&2)perms+=\"w\";if(amode&1)perms+=\"x\";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function ___syscall_ftruncate64(fd,length_low,length_high){var length=convertI32PairToI53Checked(length_low,length_high);try{if(isNaN(length))return-61;FS.ftruncate(fd,length);return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var __abort_js=()=>abort(\"\");var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{};var AsciiToString=ptr=>{var str=\"\";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var BindingError=class BindingError extends Error{constructor(message){super(message);this.name=\"BindingError\"}};var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type \"${name}\" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var EmValType={name:\"emscripten::val\",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<<bitshift>>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str=\"\";for(var i=0;i<length;++i){str+=String.fromCharCode(HEAPU8[payload+i])}}_free(value);return str},toWireType(destructors,value){if(value instanceof ArrayBuffer){value=new Uint8Array(value)}var length;var valueIsOfTypeString=typeof value==\"string\";if(!(valueIsOfTypeString||ArrayBuffer.isView(value)&&value.BYTES_PER_ELEMENT==1)){throwBindingError(\"Cannot pass non-string to std::string\")}if(stdStringIsUTF8&&valueIsOfTypeString){length=lengthBytesUTF8(value)}else{length=value.length}var base=_malloc(4+length+1);var ptr=base+4;HEAPU32[base>>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i<length;++i){var charCode=value.charCodeAt(i);if(charCode>255){_free(base);throwBindingError(\"String has UTF-16 code units that do not fit in 8 bits\")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=new TextDecoder(\"utf-16le\");var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx))};var stringToUTF16=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite<str.length*2?maxBytesToWrite/2:str.length;for(var i=0;i<numCharsToWrite;++i){var codeUnit=str.charCodeAt(i);HEAP16[outPtr>>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str=\"\";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i<str.length;++i){var codePoint=str.codePointAt(i);if(codePoint>65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i<str.length;++i){var codePoint=str.codePointAt(i);if(codePoint>65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value==\"string\")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i<argCount;++i){a[i]=requireRegisteredType(HEAPU32[argTypes+i*4>>2],`parameter ${i}`)}return a};var createNamedFunction=(name,func)=>Object.defineProperty(func,\"name\",{value:name});var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;i<argCount;++i){argN[i]=argFromPtr[i](args+offset);offset+=GenericWireTypeSize}var rv;switch(kind){case 0:rv=Emval.toValue(handle).apply(null,argN);break;case 2:rv=Reflect.construct(Emval.toValue(handle),argN);break;case 3:rv=argN[0];break;case 1:rv=Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN);break}return emval_returnValue(toReturnWire,destructorsRef,rv)};var functionName=`methodCaller<(${argTypes.map(t=>t.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_get_global=name=>{if(!name){return Emval.toHandle(globalThis)}name=getStringOrSymbol(name);return Emval.toHandle(globalThis[name])};var __emval_get_property=(handle,key)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);return Emval.toHandle(handle[key])};var __emval_incref=handle=>{if(handle>9){emval_handles[handle+1]+=1}};var __emval_instanceof=(object,constructor)=>{object=Emval.toValue(object);constructor=Emval.toValue(constructor);return object instanceof constructor};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var __emval_typeof=handle=>{handle=Emval.toValue(handle);return Emval.toHandle(typeof handle)};function __gmtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};function __localtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var setTempRet0=val=>__emscripten_tempret_set(val);var __mktime_js=function(tmPtr){var ret=(()=>{var date=new Date(HEAP32[tmPtr+20>>2]+1900,HEAP32[tmPtr+16>>2],HEAP32[tmPtr+12>>2],HEAP32[tmPtr+8>>2],HEAP32[tmPtr+4>>2],HEAP32[tmPtr>>2],0);var dst=HEAP32[tmPtr+32>>2];var guessedOffset=date.getTimezoneOffset();var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dstOffset=Math.min(winterOffset,summerOffset);if(dst<0){HEAP32[tmPtr+32>>2]=Number(summerOffset!=winterOffset&&dstOffset==guessedOffset)}else if(dst>0!=(dstOffset==guessedOffset)){var nonDstOffset=Math.max(winterOffset,summerOffset);var trueOffset=dst>0?dstOffset:nonDstOffset;date.setTime(date.getTime()+(trueOffset-guessedOffset)*6e4)}HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getYear();var timeMs=date.getTime();if(isNaN(timeMs)){return-1}return timeMs/1e3})();return setTempRet0((tempDouble=ret,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)),ret>>>0};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?\"-\":\"+\";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,\"0\");var minutes=String(absOffset%60).padStart(2,\"0\");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffset<winterOffset){stringToUTF8(winterName,std_name,17);stringToUTF8(summerName,dst_name,17)}else{stringToUTF8(winterName,dst_name,17);stringToUTF8(summerName,std_name,17)}};var _emscripten_get_now=()=>performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision_low,ignored_precision_high,ptime){var ignored_precision=convertI32PairToI53Checked(ignored_precision_low,ignored_precision_high);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);tempI64=[nsec>>>0,(tempDouble=nsec,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[ptime>>2]=tempI64[0],HEAP32[ptime+4>>2]=tempI64[1];return 0}var readEmAsmArgsArray=[];var readEmAsmArgs=(sigPtr,buf)=>{readEmAsmArgsArray.length=0;var ch;while(ch=HEAPU8[sigPtr++]){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?HEAPU32[buf>>2]:ch==105?HEAP32[buf>>2]:HEAPF64[buf>>3]);buf+=wide?8:4}return readEmAsmArgsArray};var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};var _emscripten_asm_const_int=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var _emscripten_asm_const_ptr=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var _emscripten_errn=(str,len)=>err(UTF8ToString(str,len));var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_has_asyncify=()=>0;var _emscripten_outn=(str,len)=>out(UTF8ToString(str,len));var UNWIND_CACHE={};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var convertFrameToPC=frame=>{var match;if(match=/\\bwasm-function\\[\\d+\\]:(0x[0-9a-f]+)/.exec(frame)){return+match[1]}else if(match=/:(\\d+):\\d+(?:\\)|$)/.exec(frame)){return 2147483648|+match[1]}return 0};var saveInUnwindCache=callstack=>{for(var line of callstack){var pc=convertFrameToPC(line);if(pc){UNWIND_CACHE[pc]=line}}};var jsStackTrace=()=>(new Error).stack.toString();var _emscripten_stack_snapshot=()=>{var callstack=jsStackTrace().split(\"\\n\");if(callstack[0]==\"Error\"){callstack.shift()}saveInUnwindCache(callstack);UNWIND_CACHE.last_addr=convertFrameToPC(callstack[3]);UNWIND_CACHE.last_stack=callstack;return UNWIND_CACHE.last_addr};var _emscripten_pc_get_function=pc=>{var frame=UNWIND_CACHE[pc];if(!frame)return 0;var name;var match;if(match=/^\\s+at .*\\.wasm\\.(.*) \\(.*\\)$/.exec(frame)){name=match[1]}else if(match=/^\\s+at (.*) \\(.*\\)$/.exec(frame)){name=match[1]}else if(match=/^(.+?)@/.exec(frame)){name=match[1]}else{return 0}_free(_emscripten_pc_get_function.ret??0);_emscripten_pc_get_function.ret=stringToNewUTF8(name);return _emscripten_pc_get_function.ret};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _emscripten_stack_unwind_buffer=(addr,buffer,count)=>{var stack;if(UNWIND_CACHE.last_addr==addr){stack=UNWIND_CACHE.last_stack}else{stack=jsStackTrace().split(\"\\n\");if(stack[0]==\"Error\"){stack.shift()}saveInUnwindCache(stack)}var offset=3;while(stack[offset]&&convertFrameToPC(stack[offset])!=addr){++offset}for(var i=0;i<count&&stack[i+offset];++i){HEAP32[buffer+i*4>>2]=convertFrameToPC(stack[i+offset])}return i};var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension(\"ANGLE_instanced_arrays\");if(ext){ctx[\"vertexAttribDivisor\"]=(index,divisor)=>ext[\"vertexAttribDivisorANGLE\"](index,divisor);ctx[\"drawArraysInstanced\"]=(mode,first,count,primcount)=>ext[\"drawArraysInstancedANGLE\"](mode,first,count,primcount);ctx[\"drawElementsInstanced\"]=(mode,count,type,indices,primcount)=>ext[\"drawElementsInstancedANGLE\"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension(\"OES_vertex_array_object\");if(ext){ctx[\"createVertexArray\"]=()=>ext[\"createVertexArrayOES\"]();ctx[\"deleteVertexArray\"]=vao=>ext[\"deleteVertexArrayOES\"](vao);ctx[\"bindVertexArray\"]=vao=>ext[\"bindVertexArrayOES\"](vao);ctx[\"isVertexArray\"]=vao=>ext[\"isVertexArrayOES\"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension(\"WEBGL_draw_buffers\");if(ext){ctx[\"drawBuffers\"]=(n,bufs)=>ext[\"drawBuffersWEBGL\"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension(\"WEBGL_draw_instanced_base_vertex_base_instance\"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension(\"WEBGL_multi_draw_instanced_base_vertex_base_instance\"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension(\"EXT_polygon_offset_clamp\"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension(\"EXT_clip_control\"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension(\"WEBGL_polygon_mode\"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension(\"WEBGL_multi_draw\"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=[\"ANGLE_instanced_arrays\",\"EXT_blend_minmax\",\"EXT_disjoint_timer_query\",\"EXT_frag_depth\",\"EXT_shader_texture_lod\",\"EXT_sRGB\",\"OES_element_index_uint\",\"OES_fbo_render_mipmap\",\"OES_standard_derivatives\",\"OES_texture_float\",\"OES_texture_half_float\",\"OES_texture_half_float_linear\",\"OES_vertex_array_object\",\"WEBGL_color_buffer_float\",\"WEBGL_depth_texture\",\"WEBGL_draw_buffers\",\"EXT_color_buffer_float\",\"EXT_conservative_depth\",\"EXT_disjoint_timer_query_webgl2\",\"EXT_texture_norm16\",\"NV_shader_noperspective_interpolation\",\"WEBGL_clip_cull_distance\",\"EXT_clip_control\",\"EXT_color_buffer_half_float\",\"EXT_depth_clamp\",\"EXT_float_blend\",\"EXT_polygon_offset_clamp\",\"EXT_texture_compression_bptc\",\"EXT_texture_compression_rgtc\",\"EXT_texture_filter_anisotropic\",\"KHR_parallel_shader_compile\",\"OES_texture_float_linear\",\"WEBGL_blend_func_extended\",\"WEBGL_compressed_texture_astc\",\"WEBGL_compressed_texture_etc\",\"WEBGL_compressed_texture_etc1\",\"WEBGL_compressed_texture_s3tc\",\"WEBGL_compressed_texture_s3tc_srgb\",\"WEBGL_debug_renderer_info\",\"WEBGL_debug_shaders\",\"WEBGL_lose_context\",\"WEBGL_multi_draw\",\"WEBGL_polygon_mode\"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var registerPreMainLoop=f=>{typeof MainLoop!=\"undefined\"&&MainLoop.preMainLoop.push(f)};var GL={counter:1,buffers:[],mappedBuffers:{},programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],byteSizeByTypeRoot:5120,byteSizeByType:[1,1,2,2,4,4,4,2,3,4,8],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i<ret;i++){table[i]=null}while(table[ret]){ret=GL.counter++}return ret},genObject:(n,buffers,createFunction,objectTable)=>{for(var i=0;i<n;i++){var buffer=GLctx[createFunction]();var id=buffer&&GL.getNewId(objectTable);if(buffer){buffer.name=id;objectTable[id]=buffer}else{GL.recordError(1282)}HEAP32[buffers+i*4>>2]=id}},MAX_TEMP_BUFFER_SIZE:2097152,numTempVertexBuffersPerSize:64,log2ceilLookup:i=>32-Math.clz32(i===0?0:i-1),generateTempBuffers:(quads,context)=>{var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);context.tempVertexBufferCounters1=[];context.tempVertexBufferCounters2=[];context.tempVertexBufferCounters1.length=context.tempVertexBufferCounters2.length=largestIndex+1;context.tempVertexBuffers1=[];context.tempVertexBuffers2=[];context.tempVertexBuffers1.length=context.tempVertexBuffers2.length=largestIndex+1;context.tempIndexBuffers=[];context.tempIndexBuffers.length=largestIndex+1;for(var i=0;i<=largestIndex;++i){context.tempIndexBuffers[i]=null;context.tempVertexBufferCounters1[i]=context.tempVertexBufferCounters2[i]=0;var ringbufferLength=GL.numTempVertexBuffersPerSize;context.tempVertexBuffers1[i]=[];context.tempVertexBuffers2[i]=[];var ringbuffer1=context.tempVertexBuffers1[i];var ringbuffer2=context.tempVertexBuffers2[i];ringbuffer1.length=ringbuffer2.length=ringbufferLength;for(var j=0;j<ringbufferLength;++j){ringbuffer1[j]=ringbuffer2[j]=null}}if(quads){context.tempQuadIndexBuffer=GLctx.createBuffer();context.GLctx.bindBuffer(34963,context.tempQuadIndexBuffer);var numIndexes=GL.MAX_TEMP_BUFFER_SIZE>>1;var quadIndexes=new Uint16Array(numIndexes);var i=0,v=0;while(1){quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+1;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v+3;if(i>=numIndexes)break;v+=4}context.GLctx.bufferData(34963,quadIndexes,35044);context.GLctx.bindBuffer(34963,null)}},getTempVertexBuffer:sizeBytes=>{var idx=GL.log2ceilLookup(sizeBytes);var ringbuffer=GL.currentContext.tempVertexBuffers1[idx];var nextFreeBufferIndex=GL.currentContext.tempVertexBufferCounters1[idx];GL.currentContext.tempVertexBufferCounters1[idx]=GL.currentContext.tempVertexBufferCounters1[idx]+1&GL.numTempVertexBuffersPerSize-1;var vbo=ringbuffer[nextFreeBufferIndex];if(vbo){return vbo}var prevVBO=GLctx.getParameter(34964);ringbuffer[nextFreeBufferIndex]=GLctx.createBuffer();GLctx.bindBuffer(34962,ringbuffer[nextFreeBufferIndex]);GLctx.bufferData(34962,1<<idx,35048);GLctx.bindBuffer(34962,prevVBO);return ringbuffer[nextFreeBufferIndex]},getTempIndexBuffer:sizeBytes=>{var idx=GL.log2ceilLookup(sizeBytes);var ibo=GL.currentContext.tempIndexBuffers[idx];if(ibo){return ibo}var prevIBO=GLctx.getParameter(34965);GL.currentContext.tempIndexBuffers[idx]=GLctx.createBuffer();GLctx.bindBuffer(34963,GL.currentContext.tempIndexBuffers[idx]);GLctx.bufferData(34963,1<<idx,35048);GLctx.bindBuffer(34963,prevIBO);return GL.currentContext.tempIndexBuffers[idx]},newRenderingFrameStarted:()=>{if(!GL.currentContext){return}var vb=GL.currentContext.tempVertexBuffers1;GL.currentContext.tempVertexBuffers1=GL.currentContext.tempVertexBuffers2;GL.currentContext.tempVertexBuffers2=vb;vb=GL.currentContext.tempVertexBufferCounters1;GL.currentContext.tempVertexBufferCounters1=GL.currentContext.tempVertexBufferCounters2;GL.currentContext.tempVertexBufferCounters2=vb;var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);for(var i=0;i<=largestIndex;++i){GL.currentContext.tempVertexBufferCounters1[i]=0}},getSource:(shader,count,string,length)=>{var source=\"\";for(var i=0;i<count;++i){var len=length?HEAPU32[length+i*4>>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},calcBufLength:(size,type,stride,count)=>{if(stride>0){return count*stride}var typeSize=GL.byteSizeByType[type-GL.byteSizeByTypeRoot];return size*typeSize*count},usedTempBuffers:[],preDrawHandleClientVertexAttribBindings:count=>{GL.resetBufferBinding=false;for(var i=0;i<GL.currentContext.maxVertexAttribs;++i){var cb=GL.currentContext.clientBuffers[i];if(!cb.clientside||!cb.enabled)continue;GL.resetBufferBinding=true;var size=GL.calcBufLength(cb.size,cb.type,cb.stride,count);var buf=GL.getTempVertexBuffer(size);GLctx.bindBuffer(34962,buf);GLctx.bufferSubData(34962,0,HEAPU8.subarray(cb.ptr,cb.ptr+size));cb.vertexAttribPointerAdaptor.call(GLctx,i,cb.size,cb.type,cb.normalized,cb.stride,0)}},postDrawHandleClientVertexAttribBindings:()=>{if(GL.resetBufferBinding){GLctx.bindBuffer(34962,GL.buffers[GLctx.currentArrayBufferBinding])}},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver==\"webgl\"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext(\"webgl2\",webGLContextAttributes):canvas.getContext(\"webgl\",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault==\"undefined\"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}context.maxVertexAttribs=context.GLctx.getParameter(34921);context.clientBuffers=[];for(var i=0;i<context.maxVertexAttribs;i++){context.clientBuffers[i]={enabled:false,clientside:false,size:0,type:0,normalized:0,stride:0,ptr:0,vertexAttribPointerAdaptor:null}}GL.generateTempBuffers(false,context);return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module[\"ctx\"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents==\"object\"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension(\"EXT_disjoint_timer_query_webgl2\")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension(\"EXT_disjoint_timer_query\")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes(\"lose_context\")&&!ext.includes(\"debug\")){GLctx.getExtension(ext)}})}};var webglPowerPreferences=[\"default\",\"low-power\",\"high-performance\"];var specialHTMLTargets=[0,typeof document!=\"undefined\"?document:0,typeof window!=\"undefined\"?window:0];var findEventTarget=target=>{if(!target)return window;if(typeof target==\"number\")target=specialHTMLTargets[target]||UTF8ToString(target);if(target===\"#window\")return window;else if(target===\"#document\")return document;else if(target===\"#screen\")return screen;else if(target===\"#canvas\")return Module[\"canvas\"];else if(typeof target==\"string\")return typeof document!=\"undefined\"?document.getElementById(target):null;return target};var findCanvasEventTarget=target=>{if(typeof target==\"number\")target=UTF8ToString(target);if(!target||target===\"#canvas\"){if(typeof GL!=\"undefined\"&&GL.offscreenCanvases[\"canvas\"])return GL.offscreenCanvases[\"canvas\"];return Module[\"canvas\"]}if(typeof GL!=\"undefined\"&&GL.offscreenCanvases[target])return GL.offscreenCanvases[target];return findEventTarget(target)};var _emscripten_webgl_do_create_context=(target,attributes)=>{var attr32=attributes>>2;var powerPreference=HEAP32[attr32+(8>>2)];var contextAttributes={alpha:!!HEAP8[attributes+0],depth:!!HEAP8[attributes+1],stencil:!!HEAP8[attributes+2],antialias:!!HEAP8[attributes+3],premultipliedAlpha:!!HEAP8[attributes+4],preserveDrawingBuffer:!!HEAP8[attributes+5],powerPreference:webglPowerPreferences[powerPreference],failIfMajorPerformanceCaveat:!!HEAP8[attributes+12],majorVersion:HEAP32[attr32+(16>>2)],minorVersion:HEAP32[attr32+(20>>2)],enableExtensionsByDefault:HEAP8[attributes+24],explicitSwapControl:HEAP8[attributes+25],proxyContextToMainThread:HEAP32[attr32+(28>>2)],renderViaOffscreenBackBuffer:HEAP8[attributes+32]};var canvas=findCanvasEventTarget(target);if(!canvas){return 0}if(contextAttributes.explicitSwapControl){return 0}var contextHandle=GL.createContext(canvas,contextAttributes);return contextHandle};var _emscripten_webgl_create_context=_emscripten_webgl_do_create_context;var _emscripten_webgl_destroy_context=contextHandle=>{if(GL.currentContext==contextHandle)GL.currentContext=0;GL.deleteContext(contextHandle)};var _emscripten_webgl_get_context_attributes=(c,a)=>{if(!a)return-5;c=GL.contexts[c];if(!c)return-3;var t=c.GLctx?.getContextAttributes();if(!t)return-3;HEAP8[a]=t.alpha;HEAP8[a+1]=t.depth;HEAP8[a+2]=t.stencil;HEAP8[a+3]=t.antialias;HEAP8[a+4]=t.premultipliedAlpha;HEAP8[a+5]=t.preserveDrawingBuffer;var power=t[\"powerPreference\"]&&webglPowerPreferences.indexOf(t[\"powerPreference\"]);HEAP32[a+8>>2]=power;HEAP8[a+12]=t.failIfMajorPerformanceCaveat;HEAP32[a+16>>2]=c.version;HEAP32[a+20>>2]=0;HEAP8[a+24]=c.attributes.enableExtensionsByDefault;return 0};var _emscripten_webgl_do_get_current_context=()=>GL.currentContext?GL.currentContext.handle:0;var _emscripten_webgl_get_current_context=_emscripten_webgl_do_get_current_context;var _emscripten_webgl_make_context_current=contextHandle=>{var success=GL.makeContextCurrent(contextHandle);return success?0:-5};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var readI53FromI64=ptr=>HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*4294967296;var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var WebGPU={Internals:{jsObjects:[],jsObjectInsert:(ptr,jsObject)=>{ptr>>>=0;WebGPU.Internals.jsObjects[ptr]=jsObject},bufferOnUnmaps:[],futures:[],futureInsert:(futureId,promise)=>{}},getJsObject:ptr=>{if(!ptr)return undefined;ptr>>>=0;return WebGPU.Internals.jsObjects[ptr]},importJsAdapter:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateAdapter(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroup:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroup(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroupLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroupLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBuffer:(buffer,parentPtr=0)=>{assert(buffer.mapState!=\"pending\");var mapState=buffer.mapState==\"mapped\"?3:1;var bufferPtr=_emwgpuCreateBuffer(parentPtr,mapState);WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(buffer.mapState==\"mapped\"){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return bufferPtr},importJsCommandBuffer:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandBuffer(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsCommandEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsDevice:(device,parentPtr=0)=>{var queuePtr=_emwgpuCreateQueue(parentPtr);var devicePtr=_emwgpuCreateDevice(parentPtr,queuePtr);WebGPU.Internals.jsObjectInsert(queuePtr,device.queue);WebGPU.Internals.jsObjectInsert(devicePtr,device);return devicePtr},importJsExternalTexture:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateExternalTexture(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsPipelineLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreatePipelineLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQuerySet:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQuerySet(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQueue:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQueue(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundle:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundle(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundleEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundleEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSampler:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSampler(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsShaderModule:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateShaderModule(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSurface:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSurface(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTexture:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTexture(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTextureView:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTextureView(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},errorCallback:(callback,type,message,userdata)=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(message);getWasmTableEntry(callback)(type,messagePtr,userdata);stackRestore(sp)},iterateExtensions:(root,handlers)=>{for(var ptr=HEAPU32[root>>2];ptr;ptr=HEAPU32[ptr>>2]){var sType=HEAP32[ptr+4>>2];var handler=handlers[sType](ptr)}},setStringView:(ptr,data,length)=>{HEAPU32[ptr>>2]=data;HEAPU32[ptr+4>>2]=length},makeStringFromStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>2];var length=HEAPU32[stringViewPtr+4>>2];return UTF8ToString(ptr,length)},makeStringFromOptionalStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>2];var length=HEAPU32[stringViewPtr+4>>2];if(!ptr){if(length===0){return\"\"}return undefined}return UTF8ToString(ptr,length)},makeColor:ptr=>({r:HEAPF64[ptr>>3],g:HEAPF64[ptr+8>>3],b:HEAPF64[ptr+16>>3],a:HEAPF64[ptr+24>>3]}),makeExtent3D:ptr=>({width:HEAPU32[ptr>>2],height:HEAPU32[ptr+4>>2],depthOrArrayLayers:HEAPU32[ptr+8>>2]}),makeOrigin3D:ptr=>({x:HEAPU32[ptr>>2],y:HEAPU32[ptr+4>>2],z:HEAPU32[ptr+8>>2]}),makeTexelCopyTextureInfo:ptr=>({texture:WebGPU.getJsObject(HEAPU32[ptr>>2]),mipLevel:HEAPU32[ptr+4>>2],origin:WebGPU.makeOrigin3D(ptr+8),aspect:WebGPU.TextureAspect[HEAP32[ptr+20>>2]]}),makeTexelCopyBufferLayout:ptr=>{var bytesPerRow=HEAPU32[ptr+8>>2];var rowsPerImage=HEAPU32[ptr+12>>2];return{offset:readI53FromI64(ptr),bytesPerRow:bytesPerRow===4294967295?undefined:bytesPerRow,rowsPerImage:rowsPerImage===4294967295?undefined:rowsPerImage}},makeTexelCopyBufferInfo:ptr=>{var layoutPtr=ptr+0;var bufferCopyView=WebGPU.makeTexelCopyBufferLayout(layoutPtr);bufferCopyView[\"buffer\"]=WebGPU.getJsObject(HEAPU32[ptr+16>>2]);return bufferCopyView},makePassTimestampWrites:ptr=>{if(ptr===0)return undefined;return{querySet:WebGPU.getJsObject(HEAPU32[ptr+4>>2]),beginningOfPassWriteIndex:HEAPU32[ptr+8>>2],endOfPassWriteIndex:HEAPU32[ptr+12>>2]}},makePipelineConstants:(constantCount,constantsPtr)=>{if(!constantCount)return;var constants={};for(var i=0;i<constantCount;++i){var entryPtr=constantsPtr+24*i;var key=WebGPU.makeStringFromStringView(entryPtr+4);constants[key]=HEAPF64[entryPtr+16>>3]}return constants},makePipelineLayout:layoutPtr=>{if(!layoutPtr)return\"auto\";return WebGPU.getJsObject(layoutPtr)},makeComputeState:ptr=>{if(!ptr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[ptr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[ptr+16>>2],HEAPU32[ptr+20>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(ptr+8)};return desc},makeComputePipelineDesc:descriptor=>{var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>2]),compute:WebGPU.makeComputeState(descriptor+16)};return desc},makeRenderPipelineDesc:descriptor=>{function makePrimitiveState(psPtr){if(!psPtr)return undefined;return{topology:WebGPU.PrimitiveTopology[HEAP32[psPtr+4>>2]],stripIndexFormat:WebGPU.IndexFormat[HEAP32[psPtr+8>>2]],frontFace:WebGPU.FrontFace[HEAP32[psPtr+12>>2]],cullMode:WebGPU.CullMode[HEAP32[psPtr+16>>2]],unclippedDepth:!!HEAPU32[psPtr+20>>2]}}function makeBlendComponent(bdPtr){if(!bdPtr)return undefined;return{operation:WebGPU.BlendOperation[HEAP32[bdPtr>>2]],srcFactor:WebGPU.BlendFactor[HEAP32[bdPtr+4>>2]],dstFactor:WebGPU.BlendFactor[HEAP32[bdPtr+8>>2]]}}function makeBlendState(bsPtr){if(!bsPtr)return undefined;return{alpha:makeBlendComponent(bsPtr+12),color:makeBlendComponent(bsPtr+0)}}function makeColorState(csPtr){var format=WebGPU.TextureFormat[HEAP32[csPtr+4>>2]];return format?{format,blend:makeBlendState(HEAPU32[csPtr+8>>2]),writeMask:HEAPU32[csPtr+16>>2]}:undefined}function makeColorStates(count,csArrayPtr){var states=[];for(var i=0;i<count;++i){states.push(makeColorState(csArrayPtr+24*i))}return states}function makeStencilStateFace(ssfPtr){return{compare:WebGPU.CompareFunction[HEAP32[ssfPtr>>2]],failOp:WebGPU.StencilOperation[HEAP32[ssfPtr+4>>2]],depthFailOp:WebGPU.StencilOperation[HEAP32[ssfPtr+8>>2]],passOp:WebGPU.StencilOperation[HEAP32[ssfPtr+12>>2]]}}function makeDepthStencilState(dssPtr){if(!dssPtr)return undefined;return{format:WebGPU.TextureFormat[HEAP32[dssPtr+4>>2]],depthWriteEnabled:!!HEAPU32[dssPtr+8>>2],depthCompare:WebGPU.CompareFunction[HEAP32[dssPtr+12>>2]],stencilFront:makeStencilStateFace(dssPtr+16),stencilBack:makeStencilStateFace(dssPtr+32),stencilReadMask:HEAPU32[dssPtr+48>>2],stencilWriteMask:HEAPU32[dssPtr+52>>2],depthBias:HEAP32[dssPtr+56>>2],depthBiasSlopeScale:HEAPF32[dssPtr+60>>2],depthBiasClamp:HEAPF32[dssPtr+64>>2]}}function makeVertexAttribute(vaPtr){return{format:WebGPU.VertexFormat[HEAP32[vaPtr+4>>2]],offset:readI53FromI64(vaPtr+8),shaderLocation:HEAPU32[vaPtr+16>>2]}}function makeVertexAttributes(count,vaArrayPtr){var vas=[];for(var i=0;i<count;++i){vas.push(makeVertexAttribute(vaArrayPtr+i*24))}return vas}function makeVertexBuffer(vbPtr){if(!vbPtr)return undefined;var stepMode=WebGPU.VertexStepMode[HEAP32[vbPtr+4>>2]];var attributeCount=HEAPU32[vbPtr+16>>2];if(!stepMode&&!attributeCount){return null}return{arrayStride:readI53FromI64(vbPtr+8),stepMode,attributes:makeVertexAttributes(attributeCount,HEAPU32[vbPtr+20>>2])}}function makeVertexBuffers(count,vbArrayPtr){if(!count)return undefined;var vbs=[];for(var i=0;i<count;++i){vbs.push(makeVertexBuffer(vbArrayPtr+i*24))}return vbs}function makeVertexState(viPtr){if(!viPtr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[viPtr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[viPtr+16>>2],HEAPU32[viPtr+20>>2]),buffers:makeVertexBuffers(HEAPU32[viPtr+24>>2],HEAPU32[viPtr+28>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(viPtr+8)};return desc}function makeMultisampleState(msPtr){if(!msPtr)return undefined;return{count:HEAPU32[msPtr+4>>2],mask:HEAPU32[msPtr+8>>2],alphaToCoverageEnabled:!!HEAPU32[msPtr+12>>2]}}function makeFragmentState(fsPtr){if(!fsPtr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[fsPtr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[fsPtr+16>>2],HEAPU32[fsPtr+20>>2]),targets:makeColorStates(HEAPU32[fsPtr+24>>2],HEAPU32[fsPtr+28>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(fsPtr+8)};return desc}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>2]),vertex:makeVertexState(descriptor+16),primitive:makePrimitiveState(descriptor+48),depthStencil:makeDepthStencilState(HEAPU32[descriptor+72>>2]),multisample:makeMultisampleState(descriptor+76),fragment:makeFragmentState(HEAPU32[descriptor+92>>2])};return desc},fillLimitStruct:(limits,limitsOutPtr)=>{function setLimitValueU32(name,limitOffset){var limitValue=limits[name];HEAPU32[limitsOutPtr+limitOffset>>2]=limitValue}function setLimitValueU64(name,limitOffset){var limitValue=limits[name];writeI53ToI64(limitsOutPtr+limitOffset,limitValue)}setLimitValueU32(\"maxTextureDimension1D\",4);setLimitValueU32(\"maxTextureDimension2D\",8);setLimitValueU32(\"maxTextureDimension3D\",12);setLimitValueU32(\"maxTextureArrayLayers\",16);setLimitValueU32(\"maxBindGroups\",20);setLimitValueU32(\"maxBindGroupsPlusVertexBuffers\",24);setLimitValueU32(\"maxBindingsPerBindGroup\",28);setLimitValueU32(\"maxDynamicUniformBuffersPerPipelineLayout\",32);setLimitValueU32(\"maxDynamicStorageBuffersPerPipelineLayout\",36);setLimitValueU32(\"maxSampledTexturesPerShaderStage\",40);setLimitValueU32(\"maxSamplersPerShaderStage\",44);setLimitValueU32(\"maxStorageBuffersPerShaderStage\",48);setLimitValueU32(\"maxStorageTexturesPerShaderStage\",52);setLimitValueU32(\"maxUniformBuffersPerShaderStage\",56);setLimitValueU32(\"minUniformBufferOffsetAlignment\",80);setLimitValueU32(\"minStorageBufferOffsetAlignment\",84);setLimitValueU64(\"maxUniformBufferBindingSize\",64);setLimitValueU64(\"maxStorageBufferBindingSize\",72);setLimitValueU32(\"maxVertexBuffers\",88);setLimitValueU64(\"maxBufferSize\",96);setLimitValueU32(\"maxVertexAttributes\",104);setLimitValueU32(\"maxVertexBufferArrayStride\",108);setLimitValueU32(\"maxInterStageShaderVariables\",112);setLimitValueU32(\"maxColorAttachments\",116);setLimitValueU32(\"maxColorAttachmentBytesPerSample\",120);setLimitValueU32(\"maxComputeWorkgroupStorageSize\",124);setLimitValueU32(\"maxComputeInvocationsPerWorkgroup\",128);setLimitValueU32(\"maxComputeWorkgroupSizeX\",132);setLimitValueU32(\"maxComputeWorkgroupSizeY\",136);setLimitValueU32(\"maxComputeWorkgroupSizeZ\",140);setLimitValueU32(\"maxComputeWorkgroupsPerDimension\",144);if(limits.maxImmediateSize!==undefined){setLimitValueU32(\"maxImmediateSize\",148)}},fillAdapterInfoStruct:(info,infoStruct)=>{HEAPU32[infoStruct+52>>2]=info.subgroupMinSize;HEAPU32[infoStruct+56>>2]=info.subgroupMaxSize;var strs=info.vendor+info.architecture+info.device+info.description;var strPtr=stringToNewUTF8(strs);var vendorLen=lengthBytesUTF8(info.vendor);WebGPU.setStringView(infoStruct+4,strPtr,vendorLen);strPtr+=vendorLen;var architectureLen=lengthBytesUTF8(info.architecture);WebGPU.setStringView(infoStruct+12,strPtr,architectureLen);strPtr+=architectureLen;var deviceLen=lengthBytesUTF8(info.device);WebGPU.setStringView(infoStruct+20,strPtr,deviceLen);strPtr+=deviceLen;var descriptionLen=lengthBytesUTF8(info.description);WebGPU.setStringView(infoStruct+28,strPtr,descriptionLen);strPtr+=descriptionLen;HEAP32[infoStruct+36>>2]=2;var adapterType=info.isFallbackAdapter?3:4;HEAP32[infoStruct+40>>2]=adapterType;HEAPU32[infoStruct+44>>2]=0;HEAPU32[infoStruct+48>>2]=0},AddressMode:[,\"clamp-to-edge\",\"repeat\",\"mirror-repeat\"],BlendFactor:[,\"zero\",\"one\",\"src\",\"one-minus-src\",\"src-alpha\",\"one-minus-src-alpha\",\"dst\",\"one-minus-dst\",\"dst-alpha\",\"one-minus-dst-alpha\",\"src-alpha-saturated\",\"constant\",\"one-minus-constant\",\"src1\",\"one-minus-src1\",\"src1alpha\",\"one-minus-src1alpha\"],BlendOperation:[,\"add\",\"subtract\",\"reverse-subtract\",\"min\",\"max\"],BufferBindingType:[,,\"uniform\",\"storage\",\"read-only-storage\"],BufferMapState:[,\"unmapped\",\"pending\",\"mapped\"],CompareFunction:[,\"never\",\"less\",\"equal\",\"less-equal\",\"greater\",\"not-equal\",\"greater-equal\",\"always\"],CompilationInfoRequestStatus:[,\"success\",\"callback-cancelled\"],ComponentSwizzle:[,\"0\",\"1\",\"r\",\"g\",\"b\",\"a\"],CompositeAlphaMode:[,\"opaque\",\"premultiplied\",\"unpremultiplied\",\"inherit\"],CullMode:[,\"none\",\"front\",\"back\"],ErrorFilter:[,\"validation\",\"out-of-memory\",\"internal\"],FeatureLevel:[,\"compatibility\",\"core\"],FeatureName:{1:\"core-features-and-limits\",2:\"depth-clip-control\",3:\"depth32float-stencil8\",4:\"texture-compression-bc\",5:\"texture-compression-bc-sliced-3d\",6:\"texture-compression-etc2\",7:\"texture-compression-astc\",8:\"texture-compression-astc-sliced-3d\",9:\"timestamp-query\",10:\"indirect-first-instance\",11:\"shader-f16\",12:\"rg11b10ufloat-renderable\",13:\"bgra8unorm-storage\",14:\"float32-filterable\",15:\"float32-blendable\",16:\"clip-distances\",17:\"dual-source-blending\",18:\"subgroups\",19:\"texture-formats-tier1\",20:\"texture-formats-tier2\",21:\"primitive-index\",22:\"texture-component-swizzle\",327692:\"chromium-experimental-unorm16-texture-formats\",327729:\"chromium-experimental-multi-draw-indirect\"},FilterMode:[,\"nearest\",\"linear\"],FrontFace:[,\"ccw\",\"cw\"],IndexFormat:[,\"uint16\",\"uint32\"],InstanceFeatureName:[,\"timed-wait-any\",\"shader-source-spirv\",\"multiple-devices-per-adapter\"],LoadOp:[,\"load\",\"clear\"],MipmapFilterMode:[,\"nearest\",\"linear\"],OptionalBool:[\"false\",\"true\"],PowerPreference:[,\"low-power\",\"high-performance\"],PredefinedColorSpace:[,\"srgb\",\"display-p3\"],PrimitiveTopology:[,\"point-list\",\"line-list\",\"line-strip\",\"triangle-list\",\"triangle-strip\"],QueryType:[,\"occlusion\",\"timestamp\"],SamplerBindingType:[,,\"filtering\",\"non-filtering\",\"comparison\"],Status:[,\"success\",\"error\"],StencilOperation:[,\"keep\",\"zero\",\"replace\",\"invert\",\"increment-clamp\",\"decrement-clamp\",\"increment-wrap\",\"decrement-wrap\"],StorageTextureAccess:[,,\"write-only\",\"read-only\",\"read-write\"],StoreOp:[,\"store\",\"discard\"],SurfaceGetCurrentTextureStatus:[,\"success-optimal\",\"success-suboptimal\",\"timeout\",\"outdated\",\"lost\",\"error\"],TextureAspect:[,\"all\",\"stencil-only\",\"depth-only\"],TextureDimension:[,\"1d\",\"2d\",\"3d\"],TextureFormat:[,\"r8unorm\",\"r8snorm\",\"r8uint\",\"r8sint\",\"r16unorm\",\"r16snorm\",\"r16uint\",\"r16sint\",\"r16float\",\"rg8unorm\",\"rg8snorm\",\"rg8uint\",\"rg8sint\",\"r32float\",\"r32uint\",\"r32sint\",\"rg16unorm\",\"rg16snorm\",\"rg16uint\",\"rg16sint\",\"rg16float\",\"rgba8unorm\",\"rgba8unorm-srgb\",\"rgba8snorm\",\"rgba8uint\",\"rgba8sint\",\"bgra8unorm\",\"bgra8unorm-srgb\",\"rgb10a2uint\",\"rgb10a2unorm\",\"rg11b10ufloat\",\"rgb9e5ufloat\",\"rg32float\",\"rg32uint\",\"rg32sint\",\"rgba16unorm\",\"rgba16snorm\",\"rgba16uint\",\"rgba16sint\",\"rgba16float\",\"rgba32float\",\"rgba32uint\",\"rgba32sint\",\"stencil8\",\"depth16unorm\",\"depth24plus\",\"depth24plus-stencil8\",\"depth32float\",\"depth32float-stencil8\",\"bc1-rgba-unorm\",\"bc1-rgba-unorm-srgb\",\"bc2-rgba-unorm\",\"bc2-rgba-unorm-srgb\",\"bc3-rgba-unorm\",\"bc3-rgba-unorm-srgb\",\"bc4-r-unorm\",\"bc4-r-snorm\",\"bc5-rg-unorm\",\"bc5-rg-snorm\",\"bc6h-rgb-ufloat\",\"bc6h-rgb-float\",\"bc7-rgba-unorm\",\"bc7-rgba-unorm-srgb\",\"etc2-rgb8unorm\",\"etc2-rgb8unorm-srgb\",\"etc2-rgb8a1unorm\",\"etc2-rgb8a1unorm-srgb\",\"etc2-rgba8unorm\",\"etc2-rgba8unorm-srgb\",\"eac-r11unorm\",\"eac-r11snorm\",\"eac-rg11unorm\",\"eac-rg11snorm\",\"astc-4x4-unorm\",\"astc-4x4-unorm-srgb\",\"astc-5x4-unorm\",\"astc-5x4-unorm-srgb\",\"astc-5x5-unorm\",\"astc-5x5-unorm-srgb\",\"astc-6x5-unorm\",\"astc-6x5-unorm-srgb\",\"astc-6x6-unorm\",\"astc-6x6-unorm-srgb\",\"astc-8x5-unorm\",\"astc-8x5-unorm-srgb\",\"astc-8x6-unorm\",\"astc-8x6-unorm-srgb\",\"astc-8x8-unorm\",\"astc-8x8-unorm-srgb\",\"astc-10x5-unorm\",\"astc-10x5-unorm-srgb\",\"astc-10x6-unorm\",\"astc-10x6-unorm-srgb\",\"astc-10x8-unorm\",\"astc-10x8-unorm-srgb\",\"astc-10x10-unorm\",\"astc-10x10-unorm-srgb\",\"astc-12x10-unorm\",\"astc-12x10-unorm-srgb\",\"astc-12x12-unorm\",\"astc-12x12-unorm-srgb\"],TextureSampleType:[,,\"float\",\"unfilterable-float\",\"depth\",\"sint\",\"uint\"],TextureViewDimension:[,\"1d\",\"2d\",\"2d-array\",\"cube\",\"cube-array\",\"3d\"],ToneMappingMode:[,\"standard\",\"extended\"],VertexFormat:[,\"uint8\",\"uint8x2\",\"uint8x4\",\"sint8\",\"sint8x2\",\"sint8x4\",\"unorm8\",\"unorm8x2\",\"unorm8x4\",\"snorm8\",\"snorm8x2\",\"snorm8x4\",\"uint16\",\"uint16x2\",\"uint16x4\",\"sint16\",\"sint16x2\",\"sint16x4\",\"unorm16\",\"unorm16x2\",\"unorm16x4\",\"snorm16\",\"snorm16x2\",\"snorm16x4\",\"float16\",\"float16x2\",\"float16x4\",\"float32\",\"float32x2\",\"float32x3\",\"float32x4\",\"uint32\",\"uint32x2\",\"uint32x3\",\"uint32x4\",\"sint32\",\"sint32x2\",\"sint32x3\",\"sint32x4\",\"unorm10-10-10-2\",\"unorm8x4-bgra\"],VertexStepMode:[,\"vertex\",\"instance\"],WGSLLanguageFeatureName:[,\"readonly_and_readwrite_storage_textures\",\"packed_4x8_integer_dot_product\",\"unrestricted_pointer_parameters\",\"pointer_composite_access\",\"uniform_buffer_standard_layout\",\"subgroup_id\"]};var _emscripten_webgpu_get_device=()=>{if(WebGPU.preinitializedDeviceId===undefined){WebGPU.preinitializedDeviceId=WebGPU.importJsDevice(Module[\"preinitializedWebGPUDevice\"]);_wgpuDeviceAddRef(WebGPU.preinitializedDeviceId)}_wgpuDeviceAddRef(WebGPU.preinitializedDeviceId);return WebGPU.preinitializedDeviceId};var _emwgpuBufferDestroy=bufferPtr=>{var buffer=WebGPU.getJsObject(bufferPtr);var onUnmap=WebGPU.Internals.bufferOnUnmaps[bufferPtr];if(onUnmap){for(var i=0;i<onUnmap.length;++i){onUnmap[i]()}delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]}buffer.destroy()};var _emwgpuBufferGetMappedRange=(bufferPtr,offset,size)=>{var buffer=WebGPU.getJsObject(bufferPtr);if(size==-1)size=undefined;var mapped;try{mapped=buffer.getMappedRange(offset,size)}catch(ex){return 0}var data=_memalign(16,mapped.byteLength);HEAPU8.fill(0,data,mapped.byteLength);WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(()=>{new Uint8Array(mapped).set(HEAPU8.subarray(data,data+mapped.byteLength));_free(data)});return data};var _emwgpuBufferUnmap=bufferPtr=>{var buffer=WebGPU.getJsObject(bufferPtr);var onUnmap=WebGPU.Internals.bufferOnUnmaps[bufferPtr];if(!onUnmap){return}for(var i=0;i<onUnmap.length;++i){onUnmap[i]()}delete WebGPU.Internals.bufferOnUnmaps[bufferPtr];buffer.unmap()};var _emwgpuDelete=ptr=>{delete WebGPU.Internals.jsObjects[ptr]};var _emwgpuDeviceCreateBuffer=(devicePtr,descriptor,bufferPtr)=>{var mappedAtCreation=!!HEAPU32[descriptor+32>>2];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),usage:HEAPU32[descriptor+16>>2],size:readI53FromI64(descriptor+24),mappedAtCreation};var device=WebGPU.getJsObject(devicePtr);var buffer;try{buffer=device.createBuffer(desc)}catch(ex){return false}WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(mappedAtCreation){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return true};var _emwgpuDeviceCreateComputePipelineAsync=function(devicePtr,futureId_low,futureId_high,descriptor,pipelinePtr){var futureId=convertI32PairToI53Checked(futureId_low,futureId_high);var desc=WebGPU.makeComputePipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.futureInsert(futureId,device.createComputePipelineAsync(desc).then(pipeline=>{callUserCallback(()=>{WebGPU.Internals.jsObjectInsert(pipelinePtr,pipeline);_emwgpuOnCreateComputePipelineCompleted(futureId,1,pipelinePtr,0)})},pipelineError=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(pipelineError.message);var status=pipelineError.reason===\"validation\"?3:pipelineError.reason===\"internal\"?4:0;_emwgpuOnCreateComputePipelineCompleted(futureId,status,pipelinePtr,messagePtr);stackRestore(sp)})}))};var _emwgpuDeviceCreateRenderPipelineAsync=function(devicePtr,futureId_low,futureId_high,descriptor,pipelinePtr){var futureId=convertI32PairToI53Checked(futureId_low,futureId_high);var desc=WebGPU.makeRenderPipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.futureInsert(futureId,device.createRenderPipelineAsync(desc).then(pipeline=>{callUserCallback(()=>{WebGPU.Internals.jsObjectInsert(pipelinePtr,pipeline);_emwgpuOnCreateRenderPipelineCompleted(futureId,1,pipelinePtr,0)})},pipelineError=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(pipelineError.message);var status=pipelineError.reason===\"validation\"?3:pipelineError.reason===\"internal\"?4:0;_emwgpuOnCreateRenderPipelineCompleted(futureId,status,pipelinePtr,messagePtr);stackRestore(sp)})}))};var _emwgpuDeviceCreateShaderModule=(devicePtr,descriptor,shaderModulePtr)=>{var nextInChainPtr=HEAPU32[descriptor>>2];var sType=HEAP32[nextInChainPtr+4>>2];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),code:\"\"};switch(sType){case 2:{desc[\"code\"]=WebGPU.makeStringFromStringView(nextInChainPtr+8);break}}var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.jsObjectInsert(shaderModulePtr,device.createShaderModule(desc))};var _emwgpuDeviceDestroy=devicePtr=>{const device=WebGPU.getJsObject(devicePtr);device.onuncapturederror=null;device.destroy()};var _emwgpuWaitAny=(futurePtr,futureCount,timeoutMSPtr)=>{abort(\"TODO: Implement asyncify-free WaitAny for timeout=0\")};var ENV={};var getExecutableName=()=>thisProgram||\"./this.program\";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator==\"object\"&&navigator.language||\"C\").replace(\"-\",\"_\")+\".UTF-8\";var env={USER:\"web_user\",LOGNAME:\"web_user\",PATH:\"/\",PWD:\"/\",HOME:\"/home/web_user\",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr<len)break;if(typeof offset!=\"undefined\"){offset+=curr}}return ret};function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr<len){break}if(typeof offset!=\"undefined\"){offset+=curr}}return ret};function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _glActiveTexture=_emscripten_glActiveTexture;var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _glAttachShader=_emscripten_glAttachShader;var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _glBindAttribLocation=_emscripten_glBindAttribLocation;var _emscripten_glBindBuffer=(target,buffer)=>{if(buffer&&!GL.buffers[buffer]){var b=GLctx.createBuffer();b.name=buffer;GL.buffers[buffer]=b}if(target==34962){GLctx.currentArrayBufferBinding=buffer}else if(target==34963){GLctx.currentElementArrayBufferBinding=buffer}if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _glBindBuffer=_emscripten_glBindBuffer;var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _glBindBufferBase=_emscripten_glBindBufferBase;var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _glBindFramebuffer=_emscripten_glBindFramebuffer;var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _glBindTexture=_emscripten_glBindTexture;var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao]);var ibo=GLctx.getParameter(34965);GLctx.currentElementArrayBufferBinding=ibo?ibo.name|0:0};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _glBlendEquation=_emscripten_glBlendEquation;var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _glBlendFunc=_emscripten_glBlendFunc;var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _glBufferData=_emscripten_glBufferData;var _emscripten_glClear=x0=>GLctx.clear(x0);var _glClear=_emscripten_glClear;var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _glClearColor=_emscripten_glClearColor;var convertI32PairToI53=(lo,hi)=>(lo>>>0)+hi*4294967296;var _emscripten_glClientWaitSync=(sync,flags,timeout_low,timeout_high)=>{var timeout=convertI32PairToI53(timeout_low,timeout_high);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _glClientWaitSync=_emscripten_glClientWaitSync;var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _glColorMask=_emscripten_glColorMask;var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _glCompileShader=_emscripten_glCompileShader;var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _glCreateProgram=_emscripten_glCreateProgram;var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _glCreateShader=_emscripten_glCreateShader;var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i<n;i++){var id=HEAP32[buffers+i*4>>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentArrayBufferBinding)GLctx.currentArrayBufferBinding=0;if(id==GLctx.currentElementArrayBufferBinding)GLctx.currentElementArrayBufferBinding=0;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _glDeleteBuffers=_emscripten_glDeleteBuffers;var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i<n;++i){var id=HEAP32[framebuffers+i*4>>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _glDeleteFramebuffers=_emscripten_glDeleteFramebuffers;var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _glDeleteProgram=_emscripten_glDeleteProgram;var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _glDeleteShader=_emscripten_glDeleteShader;var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _glDeleteSync=_emscripten_glDeleteSync;var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i<n;i++){var id=HEAP32[textures+i*4>>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _glDeleteTextures=_emscripten_glDeleteTextures;var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i<n;i++){var id=HEAP32[vaos+i*4>>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _glDetachShader=_emscripten_glDetachShader;var _emscripten_glDisable=x0=>GLctx.disable(x0);var _glDisable=_emscripten_glDisable;var _emscripten_glDisableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=false;GLctx.disableVertexAttribArray(index)};var _glDisableVertexAttribArray=_emscripten_glDisableVertexAttribArray;var _emscripten_glDrawArrays=(mode,first,count)=>{GL.preDrawHandleClientVertexAttribBindings(first+count);GLctx.drawArrays(mode,first,count);GL.postDrawHandleClientVertexAttribBindings()};var _glDrawArrays=_emscripten_glDrawArrays;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i<n;i++){bufArray[i]=HEAP32[bufs+i*4>>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glEnable=x0=>GLctx.enable(x0);var _glEnable=_emscripten_glEnable;var _emscripten_glEnableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=true;GLctx.enableVertexAttribArray(index)};var _glEnableVertexAttribArray=_emscripten_glEnableVertexAttribArray;var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _glFenceSync=_emscripten_glFenceSync;var _emscripten_glFinish=()=>GLctx.finish();var _glFinish=_emscripten_glFinish;var _emscripten_glFlush=()=>GLctx.flush();var _glFlush=_emscripten_glFlush;var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _glFramebufferTexture2D=_emscripten_glFramebufferTexture2D;var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _glFramebufferTextureLayer=_emscripten_glFramebufferTextureLayer;var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,\"createBuffer\",GL.buffers)};var _glGenBuffers=_emscripten_glGenBuffers;var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,\"createFramebuffer\",GL.framebuffers)};var _glGenFramebuffers=_emscripten_glGenFramebuffers;var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,\"createTexture\",GL.textures)};var _glGenTextures=_emscripten_glGenTextures;var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,\"createVertexArray\",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _glGetAttribLocation=_emscripten_glGetAttribLocation;var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _glGetError=_emscripten_glGetError;var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>\"GL_\"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case\"number\":ret=result;break;case\"boolean\":ret=result?1:0;break;case\"string\":GL.recordError(1280);return;case\"object\":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i<result.length;++i){switch(type){case 0:HEAP32[p+i*4>>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _glGetFloatv=_emscripten_glGetFloatv;var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _glGetIntegerv=_emscripten_glGetIntegerv;var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log=\"(unknown error)\";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i<numActiveUniforms;++i){program.maxUniformLength=Math.max(program.maxUniformLength,GLctx.getActiveUniform(program,i).name.length+1)}}HEAP32[p>>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i<numActiveAttributes;++i){program.maxAttributeLength=Math.max(program.maxAttributeLength,GLctx.getActiveAttrib(program,i).name.length+1)}}HEAP32[p>>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i<numActiveUniformBlocks;++i){program.maxUniformBlockNameLength=Math.max(program.maxUniformBlockNameLength,GLctx.getActiveUniformBlockName(program,i).length+1)}}HEAP32[p>>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _glGetProgramiv=_emscripten_glGetProgramiv;var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log=\"(unknown error)\";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetShaderInfoLog=_emscripten_glGetShaderInfoLog;var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log=\"(unknown error)\";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var _glGetShaderiv=_emscripten_glGetShaderiv;var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(\" \"));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+\"0\";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _glGetString=_emscripten_glGetString;var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _glGetUniformBlockIndex=_emscripten_glGetUniformBlockIndex;var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)==\"]\"&&name.lastIndexOf(\"[\");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i<numActiveUniforms;++i){var u=GLctx.getActiveUniform(program,i);var nm=u.name;var sz=u.size;var lb=webglGetLeftBracePos(nm);var arrayName=lb>0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j<sz;++j){uniformLocsById[id]=j;program.uniformArrayNamesById[id++]=arrayName}}}};var _emscripten_glGetUniformLocation=(program,name)=>{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex<sizeAndId[0]){arrayIndex+=sizeAndId[1];if(uniformLocsById[arrayIndex]=uniformLocsById[arrayIndex]||GLctx.getUniformLocation(program,name)){return arrayIndex}}}else{GL.recordError(1281)}return-1};var _glGetUniformLocation=_emscripten_glGetUniformLocation;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _glLineWidth=_emscripten_glLineWidth;var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _glLinkProgram=_emscripten_glLinkProgram;var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _glPixelStorei=_emscripten_glPixelStorei;var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _glReadPixels=_emscripten_glReadPixels;var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _glShaderSource=_emscripten_glShaderSource;var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _glTexImage2D=_emscripten_glTexImage2D;var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _glTexParameterf=_emscripten_glTexParameterf;var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _glTexParameterfv=_emscripten_glTexParameterfv;var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _glTexParameteri=_emscripten_glTexParameteri;var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _glTexStorage2D=_emscripten_glTexStorage2D;var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _glTexStorage3D=_emscripten_glTexStorage3D;var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _glTexSubImage2D=_emscripten_glTexSubImage2D;var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _glTexSubImage3D=_emscripten_glTexSubImage3D;var webglGetUniformLocation=location=>{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc==\"number\"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:\"\"))}return webglLoc}else{GL.recordError(1282)}};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _glUniform1f=_emscripten_glUniform1f;var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;++i){view[i]=HEAPF32[value+4*i>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _glUniform1fv=_emscripten_glUniform1fv;var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _glUniform1i=_emscripten_glUniform1i;var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _glUniform2f=_emscripten_glUniform2f;var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=2){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _glUniform2fv=_emscripten_glUniform2fv;var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _glUniform3f=_emscripten_glUniform3f;var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _glUniform4f=_emscripten_glUniform4f;var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i<count;i+=4){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _glUniform4fv=_emscripten_glUniform4fv;var miniTempWebGLIntBuffers=[];var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i<count;i+=4){view[i]=HEAP32[value+4*i>>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _glUniform4iv=_emscripten_glUniform4iv;var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _glUniformBlockBinding=_emscripten_glUniformBlockBinding;var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=4){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix2fv=_emscripten_glUniformMatrix2fv;var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=9){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix3fv=_emscripten_glUniformMatrix3fv;var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i<count;i+=16){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3];view[i+4]=heap[dst+4];view[i+5]=heap[dst+5];view[i+6]=heap[dst+6];view[i+7]=heap[dst+7];view[i+8]=heap[dst+8];view[i+9]=heap[dst+9];view[i+10]=heap[dst+10];view[i+11]=heap[dst+11];view[i+12]=heap[dst+12];view[i+13]=heap[dst+13];view[i+14]=heap[dst+14];view[i+15]=heap[dst+15]}}else{var view=HEAPF32.subarray(value>>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix4fv=_emscripten_glUniformMatrix4fv;var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _glUseProgram=_emscripten_glUseProgram;var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{var cb=GL.currentContext.clientBuffers[index];if(!GLctx.currentArrayBufferBinding){cb.size=size;cb.type=type;cb.normalized=normalized;cb.stride=stride;cb.ptr=ptr;cb.clientside=true;cb.vertexAttribPointerAdaptor=function(index,size,type,normalized,stride,ptr){this.vertexAttribPointer(index,size,type,normalized,stride,ptr)};return}cb.clientside=false;GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _glVertexAttribPointer=_emscripten_glVertexAttribPointer;var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _glViewport=_emscripten_glViewport;function _mediapipe_find_canvas_event_target(canvasSelector){let target=findCanvasEventTarget(canvasSelector);if(Module&&!target){target=Module.canvasWebGpu}return Emval.toHandle(target)}function _mediapipe_webgl_tex_image_drawable(drawableHandle){const drawable=Emval.toValue(drawableHandle);GLctx.texImage2D(GLctx.TEXTURE_2D,0,GLctx.RGBA,GLctx.RGBA,GLctx.UNSIGNED_BYTE,drawable)}function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var _wgpuCommandEncoderBeginComputePass=(encoderPtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),timestampWrites:WebGPU.makePassTimestampWrites(HEAPU32[descriptor+12>>2])}}var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateComputePassEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.beginComputePass(desc));return ptr};var _wgpuCommandEncoderBeginRenderPass=(encoderPtr,descriptor)=>{function makeColorAttachment(caPtr){var viewPtr=HEAPU32[caPtr+4>>2];if(viewPtr===0){return undefined}var depthSlice=HEAPU32[caPtr+8>>2];if(depthSlice==4294967295)depthSlice=undefined;return{view:WebGPU.getJsObject(viewPtr),depthSlice,resolveTarget:WebGPU.getJsObject(HEAPU32[caPtr+12>>2]),clearValue:WebGPU.makeColor(caPtr+24),loadOp:WebGPU.LoadOp[HEAP32[caPtr+16>>2]],storeOp:WebGPU.StoreOp[HEAP32[caPtr+20>>2]]}}function makeColorAttachments(count,caPtr){var attachments=[];for(var i=0;i<count;++i){attachments.push(makeColorAttachment(caPtr+56*i))}return attachments}function makeDepthStencilAttachment(dsaPtr){if(dsaPtr===0)return undefined;return{view:WebGPU.getJsObject(HEAPU32[dsaPtr+4>>2]),depthClearValue:HEAPF32[dsaPtr+16>>2],depthLoadOp:WebGPU.LoadOp[HEAP32[dsaPtr+8>>2]],depthStoreOp:WebGPU.StoreOp[HEAP32[dsaPtr+12>>2]],depthReadOnly:!!HEAPU32[dsaPtr+20>>2],stencilClearValue:HEAPU32[dsaPtr+32>>2],stencilLoadOp:WebGPU.LoadOp[HEAP32[dsaPtr+24>>2]],stencilStoreOp:WebGPU.StoreOp[HEAP32[dsaPtr+28>>2]],stencilReadOnly:!!HEAPU32[dsaPtr+36>>2]}}function makeRenderPassDescriptor(descriptor){var nextInChainPtr=HEAPU32[descriptor>>2];var maxDrawCount=undefined;if(nextInChainPtr!==0){var sType=HEAP32[nextInChainPtr+4>>2];var renderPassMaxDrawCount=nextInChainPtr;maxDrawCount=readI53FromI64(renderPassMaxDrawCount+8)}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),colorAttachments:makeColorAttachments(HEAPU32[descriptor+12>>2],HEAPU32[descriptor+16>>2]),depthStencilAttachment:makeDepthStencilAttachment(HEAPU32[descriptor+20>>2]),occlusionQuerySet:WebGPU.getJsObject(HEAPU32[descriptor+24>>2]),timestampWrites:WebGPU.makePassTimestampWrites(HEAPU32[descriptor+28>>2]),maxDrawCount};return desc}var desc=makeRenderPassDescriptor(descriptor);var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateRenderPassEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.beginRenderPass(desc));return ptr};var _wgpuCommandEncoderCopyBufferToTexture=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyBufferToTexture(WebGPU.makeTexelCopyBufferInfo(srcPtr),WebGPU.makeTexelCopyTextureInfo(dstPtr),copySize)};var _wgpuCommandEncoderCopyTextureToBuffer=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyTextureToBuffer(WebGPU.makeTexelCopyTextureInfo(srcPtr),WebGPU.makeTexelCopyBufferInfo(dstPtr),copySize)};var _wgpuCommandEncoderCopyTextureToTexture=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyTextureToTexture(WebGPU.makeTexelCopyTextureInfo(srcPtr),WebGPU.makeTexelCopyTextureInfo(dstPtr),copySize)};var _wgpuCommandEncoderFinish=(encoderPtr,descriptor)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateCommandBuffer(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.finish());return ptr};var _wgpuComputePassEncoderDispatchWorkgroups=(passPtr,x,y,z)=>{var pass=WebGPU.getJsObject(passPtr);pass.dispatchWorkgroups(x,y,z)};var _wgpuComputePassEncoderEnd=passPtr=>{var pass=WebGPU.getJsObject(passPtr);pass.end()};var _wgpuComputePassEncoderSetBindGroup=(passPtr,groupIndex,groupPtr,dynamicOffsetCount,dynamicOffsetsPtr)=>{var pass=WebGPU.getJsObject(passPtr);var group=WebGPU.getJsObject(groupPtr);if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,HEAPU32,dynamicOffsetsPtr>>2,dynamicOffsetCount)}};var _wgpuComputePassEncoderSetPipeline=(passPtr,pipelinePtr)=>{var pass=WebGPU.getJsObject(passPtr);var pipeline=WebGPU.getJsObject(pipelinePtr);pass.setPipeline(pipeline)};var _wgpuComputePipelineGetBindGroupLayout=(pipelinePtr,groupIndex)=>{var pipeline=WebGPU.getJsObject(pipelinePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,pipeline.getBindGroupLayout(groupIndex));return ptr};var _wgpuDeviceCreateBindGroup=(devicePtr,descriptor)=>{function makeEntry(entryPtr){var bufferPtr=HEAPU32[entryPtr+8>>2];var samplerPtr=HEAPU32[entryPtr+32>>2];var textureViewPtr=HEAPU32[entryPtr+36>>2];var externalTexturePtr=0;WebGPU.iterateExtensions(entryPtr,{327681:ptr=>{externalTexturePtr=HEAPU32[ptr+8>>2]}});var resource;if(bufferPtr){var size=readI53FromI64(entryPtr+24);if(size==-1)size=undefined;resource={buffer:WebGPU.getJsObject(bufferPtr),offset:readI53FromI64(entryPtr+16),size}}else{resource=WebGPU.getJsObject(samplerPtr||textureViewPtr||externalTexturePtr)}return{binding:HEAPU32[entryPtr+4>>2],resource}}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i<count;++i){entries.push(makeEntry(entriesPtrs+40*i))}return entries}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.getJsObject(HEAPU32[descriptor+12>>2]),entries:makeEntries(HEAPU32[descriptor+16>>2],HEAPU32[descriptor+20>>2])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroup(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroup(desc));return ptr};var _wgpuDeviceCreateBindGroupLayout=(devicePtr,descriptor)=>{function makeBufferEntry(substructPtr){var typeInt=HEAPU32[substructPtr+4>>2];if(!typeInt)return undefined;return{type:WebGPU.BufferBindingType[typeInt],hasDynamicOffset:!!HEAPU32[substructPtr+8>>2],minBindingSize:readI53FromI64(substructPtr+16)}}function makeSamplerEntry(substructPtr){var typeInt=HEAPU32[substructPtr+4>>2];if(!typeInt)return undefined;return{type:WebGPU.SamplerBindingType[typeInt]}}function makeTextureEntry(substructPtr){var sampleTypeInt=HEAPU32[substructPtr+4>>2];if(!sampleTypeInt)return undefined;return{sampleType:WebGPU.TextureSampleType[sampleTypeInt],viewDimension:WebGPU.TextureViewDimension[HEAP32[substructPtr+8>>2]],multisampled:!!HEAPU32[substructPtr+12>>2]}}function makeStorageTextureEntry(substructPtr){var accessInt=HEAPU32[substructPtr+4>>2];if(!accessInt)return undefined;return{access:WebGPU.StorageTextureAccess[accessInt],format:WebGPU.TextureFormat[HEAP32[substructPtr+8>>2]],viewDimension:WebGPU.TextureViewDimension[HEAP32[substructPtr+12>>2]]}}function makeEntry(entryPtr){var entry={binding:HEAPU32[entryPtr+4>>2],visibility:HEAPU32[entryPtr+8>>2],buffer:makeBufferEntry(entryPtr+24),sampler:makeSamplerEntry(entryPtr+48),texture:makeTextureEntry(entryPtr+56),storageTexture:makeStorageTextureEntry(entryPtr+72)};WebGPU.iterateExtensions(entryPtr,{327682:ptr=>{entry[\"externalTexture\"]={}}});return entry}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i<count;++i){entries.push(makeEntry(entriesPtrs+88*i))}return entries}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),entries:makeEntries(HEAPU32[descriptor+12>>2],HEAPU32[descriptor+16>>2])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroupLayout(desc));return ptr};var _wgpuDeviceCreateCommandEncoder=(devicePtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4)}}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateCommandEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,device.createCommandEncoder(desc));return ptr};var _wgpuDeviceCreateComputePipeline=(devicePtr,descriptor)=>{var desc=WebGPU.makeComputePipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateComputePipeline(0);WebGPU.Internals.jsObjectInsert(ptr,device.createComputePipeline(desc));return ptr};var _wgpuDeviceCreatePipelineLayout=(devicePtr,descriptor)=>{var bglCount=HEAPU32[descriptor+12>>2];var bglPtr=HEAPU32[descriptor+16>>2];var bgls=[];for(var i=0;i<bglCount;++i){bgls.push(WebGPU.getJsObject(HEAPU32[bglPtr+4*i>>2]))}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),bindGroupLayouts:bgls};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreatePipelineLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createPipelineLayout(desc));return ptr};var _wgpuDeviceCreateRenderPipeline=(devicePtr,descriptor)=>{var desc=WebGPU.makeRenderPipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateRenderPipeline(0);WebGPU.Internals.jsObjectInsert(ptr,device.createRenderPipeline(desc));return ptr};var _wgpuDeviceCreateSampler=(devicePtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),addressModeU:WebGPU.AddressMode[HEAP32[descriptor+12>>2]],addressModeV:WebGPU.AddressMode[HEAP32[descriptor+16>>2]],addressModeW:WebGPU.AddressMode[HEAP32[descriptor+20>>2]],magFilter:WebGPU.FilterMode[HEAP32[descriptor+24>>2]],minFilter:WebGPU.FilterMode[HEAP32[descriptor+28>>2]],mipmapFilter:WebGPU.MipmapFilterMode[HEAP32[descriptor+32>>2]],lodMinClamp:HEAPF32[descriptor+36>>2],lodMaxClamp:HEAPF32[descriptor+40>>2],compare:WebGPU.CompareFunction[HEAP32[descriptor+44>>2]],maxAnisotropy:HEAPU16[descriptor+48>>1]}}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateSampler(0);WebGPU.Internals.jsObjectInsert(ptr,device.createSampler(desc));return ptr};var _wgpuDeviceCreateTexture=(devicePtr,descriptor)=>{var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),size:WebGPU.makeExtent3D(descriptor+28),mipLevelCount:HEAPU32[descriptor+44>>2],sampleCount:HEAPU32[descriptor+48>>2],dimension:WebGPU.TextureDimension[HEAP32[descriptor+24>>2]],format:WebGPU.TextureFormat[HEAP32[descriptor+40>>2]],usage:HEAPU32[descriptor+16>>2]};var viewFormatCount=HEAPU32[descriptor+52>>2];if(viewFormatCount){var viewFormatsPtr=HEAPU32[descriptor+56>>2];desc[\"viewFormats\"]=Array.from(HEAP32.subarray(viewFormatsPtr>>2,viewFormatsPtr+viewFormatCount*4>>2),format=>WebGPU.TextureFormat[format])}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateTexture(0);WebGPU.Internals.jsObjectInsert(ptr,device.createTexture(desc));return ptr};var _wgpuQueueSubmit=(queuePtr,commandCount,commands)=>{var queue=WebGPU.getJsObject(queuePtr);var cmds=Array.from(HEAP32.subarray(commands>>2,commands+commandCount*4>>2),id=>WebGPU.getJsObject(id));queue.submit(cmds)};function _wgpuQueueWriteBuffer(queuePtr,bufferPtr,bufferOffset_low,bufferOffset_high,data,size){var bufferOffset=convertI32PairToI53Checked(bufferOffset_low,bufferOffset_high);var queue=WebGPU.getJsObject(queuePtr);var buffer=WebGPU.getJsObject(bufferPtr);var subarray=HEAPU8.subarray(data,data+size);queue.writeBuffer(buffer,bufferOffset,subarray,0,size)}var _wgpuQueueWriteTexture=(queuePtr,destinationPtr,data,dataSize,dataLayoutPtr,writeSizePtr)=>{var queue=WebGPU.getJsObject(queuePtr);var destination=WebGPU.makeTexelCopyTextureInfo(destinationPtr);var dataLayout=WebGPU.makeTexelCopyBufferLayout(dataLayoutPtr);var writeSize=WebGPU.makeExtent3D(writeSizePtr);var subarray=HEAPU8.subarray(data,data+dataSize);queue.writeTexture(destination,subarray,dataLayout,writeSize)};var _wgpuRenderPassEncoderDraw=(passPtr,vertexCount,instanceCount,firstVertex,firstInstance)=>{firstVertex>>>=0;firstInstance>>>=0;var pass=WebGPU.getJsObject(passPtr);pass.draw(vertexCount,instanceCount,firstVertex,firstInstance)};var _wgpuRenderPassEncoderEnd=encoderPtr=>{var encoder=WebGPU.getJsObject(encoderPtr);encoder.end()};var _wgpuRenderPassEncoderSetBindGroup=(passPtr,groupIndex,groupPtr,dynamicOffsetCount,dynamicOffsetsPtr)=>{var pass=WebGPU.getJsObject(passPtr);var group=WebGPU.getJsObject(groupPtr);if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,HEAPU32,dynamicOffsetsPtr>>2,dynamicOffsetCount)}};var _wgpuRenderPassEncoderSetPipeline=(passPtr,pipelinePtr)=>{var pass=WebGPU.getJsObject(passPtr);var pipeline=WebGPU.getJsObject(pipelinePtr);pass.setPipeline(pipeline)};var _wgpuRenderPipelineGetBindGroupLayout=(pipelinePtr,groupIndex)=>{var pipeline=WebGPU.getJsObject(pipelinePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,pipeline.getBindGroupLayout(groupIndex));return ptr};var _wgpuTextureCreateView=(texturePtr,descriptor)=>{var desc;if(descriptor){var swizzle;var nextInChainPtr=HEAPU32[descriptor>>2];if(nextInChainPtr!==0){var sType=HEAP32[nextInChainPtr+4>>2];var swizzleDescriptor=nextInChainPtr;var swizzlePtr=swizzleDescriptor+8;var r=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr>>2]]||\"r\";var g=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+4>>2]]||\"g\";var b=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+8>>2]]||\"b\";var a=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+12>>2]]||\"a\";swizzle=`${r}${g}${b}${a}`}var mipLevelCount=HEAPU32[descriptor+24>>2];var arrayLayerCount=HEAPU32[descriptor+32>>2];desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),format:WebGPU.TextureFormat[HEAP32[descriptor+12>>2]],dimension:WebGPU.TextureViewDimension[HEAP32[descriptor+16>>2]],baseMipLevel:HEAPU32[descriptor+20>>2],mipLevelCount:mipLevelCount===4294967295?undefined:mipLevelCount,baseArrayLayer:HEAPU32[descriptor+28>>2],arrayLayerCount:arrayLayerCount===4294967295?undefined:arrayLayerCount,aspect:WebGPU.TextureAspect[HEAP32[descriptor+36>>2]],swizzle}}var texture=WebGPU.getJsObject(texturePtr);var ptr=_emwgpuCreateTextureView(0);WebGPU.Internals.jsObjectInsert(ptr,texture.createView(desc));return ptr};var _wgpuTextureDestroy=texturePtr=>{WebGPU.getJsObject(texturePtr).destroy()};var _wgpuTextureGetFormat=texturePtr=>{var texture=WebGPU.getJsObject(texturePtr);return WebGPU.TextureFormat.indexOf(texture.format)};var getCFunc=ident=>{var func=Module[\"_\"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType===\"string\"){return UTF8ToString(ret)}if(returnType===\"boolean\")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var FS_createPath=(...args)=>FS.createPath(...args);var FS_unlink=(...args)=>FS.unlink(...args);var FS_createLazyFile=(...args)=>FS.createLazyFile(...args);var FS_createDevice=(...args)=>FS.createDevice(...args);FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();registerPreMainLoop(()=>GL.newRenderingFrameStarted());for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module[\"preloadPlugins\"])preloadPlugins=Module[\"preloadPlugins\"];if(Module[\"noExitRuntime\"])noExitRuntime=Module[\"noExitRuntime\"];if(Module[\"print\"])out=Module[\"print\"];if(Module[\"printErr\"])err=Module[\"printErr\"];if(Module[\"wasmBinary\"])wasmBinary=Module[\"wasmBinary\"];if(Module[\"arguments\"])arguments_=Module[\"arguments\"];if(Module[\"thisProgram\"])thisProgram=Module[\"thisProgram\"];if(Module[\"preInit\"]){if(typeof Module[\"preInit\"]==\"function\")Module[\"preInit\"]=[Module[\"preInit\"]];while(Module[\"preInit\"].length>0){Module[\"preInit\"].shift()()}}}Module[\"addRunDependency\"]=addRunDependency;Module[\"removeRunDependency\"]=removeRunDependency;Module[\"ccall\"]=ccall;Module[\"stringToNewUTF8\"]=stringToNewUTF8;Module[\"FS_preloadFile\"]=FS_preloadFile;Module[\"FS_unlink\"]=FS_unlink;Module[\"FS_createPath\"]=FS_createPath;Module[\"FS_createDevice\"]=FS_createDevice;Module[\"FS_createDataFile\"]=FS_createDataFile;Module[\"FS_createLazyFile\"]=FS_createLazyFile;var ASM_CONSTS={1486195:$0=>{const canvas=Emval.toValue($0);const context=canvas.getContext(\"webgpu\");return WebGPU.importJsTexture(context.getCurrentTexture())},1486338:($0,$1,$2,$3,$4)=>{const drawable=Emval.toValue($0);const device=WebGPU.getJsObject($1);const texture=WebGPU.getJsObject($2);const width=$3;const height=$4;device.queue.copyExternalImageToTexture({source:drawable},{texture},[width,height])},1486597:($0,$1,$2,$3)=>{const sourceExtTex=Emval.toValue($0);const device=WebGPU.getJsObject($1);const sampler=WebGPU.getJsObject($2);const bgLayout=WebGPU.getJsObject($3);const bindGroup=device.createBindGroup({layout:bgLayout,entries:[{binding:0,resource:sampler},{binding:1,resource:sourceExtTex}]});return WebGPU.importJsBindGroup(bindGroup)},1486967:($0,$1)=>{const input=Emval.toValue($0);const output=Emval.toValue($1);const ctx=output.getContext(\"2d\");ctx.drawImage(input,0,0,output.width,output.height)},1487132:($0,$1)=>{const inputArray=Emval.toValue($0);const output=Emval.toValue($1);const ctx=output.getContext(\"2d\");const image_data=new ImageData(inputArray,output.width,output.height);ctx.putImageData(image_data,0,0)},1487356:($0,$1)=>{const input=Emval.toValue($0);const outputArray=Emval.toValue($1);const ctx=input.getContext(\"2d\");const data=ctx.getImageData(0,0,input.width,input.height);outputArray.set(data.data)},1487560:()=>typeof HTMLCanvasElement!==\"undefined\",1487615:()=>!!Module[\"preinitializedWebGPUDevice\"],1487666:()=>{specialHTMLTargets[\"#canvas\"]=Module.canvas}};function BeginGlQueryTiming(calc_name,num_repetitions){const gl=Module.canvas.getContext(\"webgl2\");const query=gl.createQuery();Module.WEBGL_SHADER_CALC_METRICS=Module.WEBGL_SHADER_CALC_METRICS||{};Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)]={query,repetitions:num_repetitions};Module.WEBGL_QUERY_TIMER_EXT=Module.WEBGL_QUERY_TIMER_EXT||gl.getExtension(\"EXT_disjoint_timer_query_webgl2\");gl.beginQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT,query)}function EndGlQueryTiming(calc_name){const gl=Module.canvas.getContext(\"webgl2\");gl.endQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT,Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)].query)}function JsWrapImageConverter(){if(!Module._imageConverter){Module._imageConverter=(binaryPtr,binarySize,width,height,numChannels,makeDeepCopy,outputType)=>{const imageData=new outputType(makeDeepCopy?Module.HEAPU8.slice(binaryPtr,binaryPtr+binarySize).buffer:Module.HEAPU8.buffer,binaryPtr,width*height*numChannels);return{data:imageData,width,height}}}}function JsOnUint8ArrayImageListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Uint8Array);Module._wrapSimpleListenerOutput(output_stream_name,image,timestamp_ms)}function JsOnFloat32ArrayImageListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Float32Array);Module._wrapSimpleListenerOutput(output_stream_name,image,timestamp_ms)}function JsOnWebGLTextureListener(output_stream_name,name,width,height,timestamp_ms){Module._wrapSimpleListenerOutput(output_stream_name,{data:GL.textures[name],width,height},timestamp_ms)}function JsOnUint8ArrayImageVectorListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Uint8Array);Module._wrapSimpleListenerOutput(output_stream_name,image,false,timestamp_ms)}function JsOnFloat32ArrayImageVectorListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Float32Array);Module._wrapSimpleListenerOutput(output_stream_name,image,false,timestamp_ms)}function JsOnWebGLTextureVectorListener(output_stream_name,name,width,height,timestamp_ms){Module._wrapSimpleListenerOutput(output_stream_name,{data:GL.textures[name],width,height},false,timestamp_ms)}function JsOnEmptyPacketListener(output_stream_name,timestamp){Module._wrapEmptyPacketListenerOutput(output_stream_name,timestamp)}function JsOnVectorFinishedListener(output_stream_name,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,undefined,true,timestamp)}function JsOnSimpleListenerBool(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerBool(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerInt(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerInt(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerUint(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerUint(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerDouble(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerDouble(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerFloat(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerFloat(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerString(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,UTF8ToString(out_data),timestamp)}function JsOnVectorListenerString(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,UTF8ToString(out_data),false,timestamp)}function JsOnVectorListenerProto(output_stream_name,proto_ptr,proto_size,make_deep_copy,timestamp){const newProtoArray=make_deep_copy?Module.HEAPU8.slice(proto_ptr,proto_ptr+proto_size):new Uint8Array(Module.HEAPU8.buffer,proto_ptr,proto_size);Module._wrapSimpleListenerOutput(output_stream_name,newProtoArray,false,timestamp)}function JsWrapSimpleListeners(){if(!Module._wrapSimpleListenerOutput){Module._wrapSimpleListenerOutput=(outputStreamName,...args)=>{if(Module.simpleListeners){const streamName=UTF8ToString(outputStreamName);if(Module.simpleListeners[streamName]){Module.simpleListeners[streamName](...args)}}}}if(!Module._wrapEmptyPacketListenerOutput){Module._wrapEmptyPacketListenerOutput=(outputStreamName,timestamp)=>{if(Module.emptyPacketListeners){const streamName=UTF8ToString(outputStreamName);if(Module.emptyPacketListeners[streamName]){Module.emptyPacketListeners[streamName](timestamp)}}}}}function JsOnSimpleListenerBinaryArray(output_stream_name,binary_ptr,binary_size,make_deep_copy,timestamp){const newProtoArray=make_deep_copy?Module.HEAPU8.slice(binary_ptr,binary_ptr+binary_size):new Uint8Array(Module.HEAPU8.buffer,binary_ptr,binary_size);Module._wrapSimpleListenerOutput(output_stream_name,newProtoArray,timestamp)}function mediapipe_import_external_texture(device_handle,source_handle){const device=WebGPU.getJsObject(device_handle);const source=Emval.toValue(source_handle);const externalTexture=device.importExternalTexture({source});return Emval.toHandle(externalTexture)}function mediapipe_create_utility_canvas2d(){let canvas;if(typeof HTMLCanvasElement!==\"undefined\"){canvas=document.createElement(\"canvas\");canvas.style.display=\"none\"}else{canvas=new OffscreenCanvas(0,0)}return Emval.toHandle(canvas)}function GetAdapterArchitecture(){const device=Module[\"preinitializedWebGPUDevice\"];const architecture=device.adapterInfo?device.adapterInfo.architecture:\"Unknown\";return stringToNewUTF8(architecture)}function GetAdapterDescription(){const device=Module[\"preinitializedWebGPUDevice\"];const description=device.adapterInfo?device.adapterInfo.description:\"Unknown\";return stringToNewUTF8(description)}function GetAdapterDeviceName(){const device=Module[\"preinitializedWebGPUDevice\"];const deviceName=device.adapterInfo?device.adapterInfo.device:\"Unknown\";return stringToNewUTF8(deviceName)}function GetAdapterVendor(){const device=Module[\"preinitializedWebGPUDevice\"];const vendor=device.adapterInfo?device.adapterInfo.vendor:\"Unknown\";return stringToNewUTF8(vendor)}function __asyncjs__mediapipe_map_buffer_jspi(buffer_handle,data){return Asyncify.handleAsync(async()=>{const buffer=WebGPU.getJsObject(buffer_handle);if(\"mapSync\"in buffer){buffer.mapSync(GPUMapMode.READ)}else{await buffer.mapAsync(GPUMapMode.READ)}const mapped=buffer.getMappedRange();HEAPU8.set(new Uint8Array(mapped),data);buffer.unmap()})}function hardware_concurrency(){var concurrency=1;try{concurrency=self.navigator.hardwareConcurrency}catch(e){}return concurrency}function JsWrapErrorListener(code,message){if(Module.errorListener){const stringMessage=UTF8ToString(message);Module.errorListener(code,stringMessage)}}function UseBottomLeftGpuOrigin(){return Module&&Module.gpuOriginForWebTexturesIsBottomLeft}function custom_emscripten_dbgn(str,len){if(typeof dbg!==\"undefined\"){dbg(UTF8ToString(str,len))}else{if(typeof custom_dbg===\"undefined\"){function custom_dbg(text){console.warn.apply(console,arguments)}}custom_dbg(UTF8ToString(str,len))}}var _free,_malloc,_wgpuDeviceAddRef,_addBoundTextureAsImageToStream,_attachImageListener,_attachImageVectorListener,_registerModelResourcesGraphService,_bindTextureToStream,_addBoundTextureToStream,_addDoubleToInputStream,_addFloatToInputStream,_addBoolToInputStream,_addIntToInputStream,_addUintToInputStream,_addStringToInputStream,_addRawDataSpanToInputStream,_allocateBoolVector,_allocateFloatVector,_allocateDoubleVector,_allocateIntVector,_allocateUintVector,_allocateStringVector,_addBoolVectorEntry,_addFloatVectorEntry,_addDoubleVectorEntry,_addIntVectorEntry,_addUintVectorEntry,_addStringVectorEntry,_addBoolVectorToInputStream,_addFloatVectorToInputStream,_addDoubleVectorToInputStream,_addIntVectorToInputStream,_addUintVectorToInputStream,_addStringVectorToInputStream,_addFlatHashMapToInputStream,_addProtoToInputStream,_addEmptyPacketToInputStream,_addBoolToInputSidePacket,_addDoubleToInputSidePacket,_addFloatToInputSidePacket,_addIntToInputSidePacket,_addUintToInputSidePacket,_addStringToInputSidePacket,_addRawDataSpanToInputSidePacket,_addProtoToInputSidePacket,_addBoolVectorToInputSidePacket,_addDoubleVectorToInputSidePacket,_addFloatVectorToInputSidePacket,_addIntVectorToInputSidePacket,_addUintVectorToInputSidePacket,_addStringVectorToInputSidePacket,_attachBoolListener,_attachBoolVectorListener,_attachDoubleListener,_attachDoubleVectorListener,_attachFloatListener,_attachFloatVectorListener,_attachIntListener,_attachIntVectorListener,_attachUintListener,_attachUintVectorListener,_attachStringListener,_attachStringVectorListener,_attachProtoListener,_attachProtoVectorListener,_getGraphConfig,___getTypeName,_emwgpuCreateBindGroup,_emwgpuCreateBindGroupLayout,_emwgpuCreateCommandBuffer,_emwgpuCreateCommandEncoder,_emwgpuCreateComputePassEncoder,_emwgpuCreateComputePipeline,_emwgpuCreateExternalTexture,_emwgpuCreatePipelineLayout,_emwgpuCreateQuerySet,_emwgpuCreateRenderBundle,_emwgpuCreateRenderBundleEncoder,_emwgpuCreateRenderPassEncoder,_emwgpuCreateRenderPipeline,_emwgpuCreateSampler,_emwgpuCreateSurface,_emwgpuCreateTexture,_emwgpuCreateTextureView,_emwgpuCreateAdapter,_emwgpuCreateBuffer,_emwgpuCreateDevice,_emwgpuCreateQueue,_emwgpuCreateShaderModule,_emwgpuOnCreateComputePipelineCompleted,_emwgpuOnCreateRenderPipelineCompleted,_clearSubgraphs,_pushBinarySubgraph,_pushTextSubgraph,_changeBinaryGraph,_changeTextGraph,_processGl,_process,_bindTextureToCanvas,_requestShaderRefreshOnGraphChange,_waitUntilIdle,_closeGraph,_setAutoRenderToScreen,_emscripten_builtin_memalign,_memalign,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,dynCall_ji,dynCall_jii,dynCall_iiiijij,dynCall_viiji,dynCall_viji,dynCall_iiiji,dynCall_jjj,dynCall_iiiijj,dynCall_viijj,dynCall_viiijjj,dynCall_vij,dynCall_viijii,dynCall_viiiji,dynCall_vijjj,dynCall_vj,dynCall_viij,dynCall_jiji,dynCall_iiiiij,dynCall_iiiiijj,dynCall_iiiiiijj,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_free=Module[\"_free\"]=wasmExports[\"ld\"];_malloc=Module[\"_malloc\"]=wasmExports[\"md\"];_wgpuDeviceAddRef=wasmExports[\"nd\"];_addBoundTextureAsImageToStream=Module[\"_addBoundTextureAsImageToStream\"]=wasmExports[\"od\"];_attachImageListener=Module[\"_attachImageListener\"]=wasmExports[\"pd\"];_attachImageVectorListener=Module[\"_attachImageVectorListener\"]=wasmExports[\"qd\"];_registerModelResourcesGraphService=Module[\"_registerModelResourcesGraphService\"]=wasmExports[\"rd\"];_bindTextureToStream=Module[\"_bindTextureToStream\"]=wasmExports[\"sd\"];_addBoundTextureToStream=Module[\"_addBoundTextureToStream\"]=wasmExports[\"td\"];_addDoubleToInputStream=Module[\"_addDoubleToInputStream\"]=wasmExports[\"ud\"];_addFloatToInputStream=Module[\"_addFloatToInputStream\"]=wasmExports[\"vd\"];_addBoolToInputStream=Module[\"_addBoolToInputStream\"]=wasmExports[\"wd\"];_addIntToInputStream=Module[\"_addIntToInputStream\"]=wasmExports[\"xd\"];_addUintToInputStream=Module[\"_addUintToInputStream\"]=wasmExports[\"yd\"];_addStringToInputStream=Module[\"_addStringToInputStream\"]=wasmExports[\"zd\"];_addRawDataSpanToInputStream=Module[\"_addRawDataSpanToInputStream\"]=wasmExports[\"Ad\"];_allocateBoolVector=Module[\"_allocateBoolVector\"]=wasmExports[\"Bd\"];_allocateFloatVector=Module[\"_allocateFloatVector\"]=wasmExports[\"Cd\"];_allocateDoubleVector=Module[\"_allocateDoubleVector\"]=wasmExports[\"Dd\"];_allocateIntVector=Module[\"_allocateIntVector\"]=wasmExports[\"Ed\"];_allocateUintVector=Module[\"_allocateUintVector\"]=wasmExports[\"Fd\"];_allocateStringVector=Module[\"_allocateStringVector\"]=wasmExports[\"Gd\"];_addBoolVectorEntry=Module[\"_addBoolVectorEntry\"]=wasmExports[\"Hd\"];_addFloatVectorEntry=Module[\"_addFloatVectorEntry\"]=wasmExports[\"Id\"];_addDoubleVectorEntry=Module[\"_addDoubleVectorEntry\"]=wasmExports[\"Jd\"];_addIntVectorEntry=Module[\"_addIntVectorEntry\"]=wasmExports[\"Kd\"];_addUintVectorEntry=Module[\"_addUintVectorEntry\"]=wasmExports[\"Ld\"];_addStringVectorEntry=Module[\"_addStringVectorEntry\"]=wasmExports[\"Md\"];_addBoolVectorToInputStream=Module[\"_addBoolVectorToInputStream\"]=wasmExports[\"Nd\"];_addFloatVectorToInputStream=Module[\"_addFloatVectorToInputStream\"]=wasmExports[\"Od\"];_addDoubleVectorToInputStream=Module[\"_addDoubleVectorToInputStream\"]=wasmExports[\"Pd\"];_addIntVectorToInputStream=Module[\"_addIntVectorToInputStream\"]=wasmExports[\"Qd\"];_addUintVectorToInputStream=Module[\"_addUintVectorToInputStream\"]=wasmExports[\"Rd\"];_addStringVectorToInputStream=Module[\"_addStringVectorToInputStream\"]=wasmExports[\"Sd\"];_addFlatHashMapToInputStream=Module[\"_addFlatHashMapToInputStream\"]=wasmExports[\"Td\"];_addProtoToInputStream=Module[\"_addProtoToInputStream\"]=wasmExports[\"Ud\"];_addEmptyPacketToInputStream=Module[\"_addEmptyPacketToInputStream\"]=wasmExports[\"Vd\"];_addBoolToInputSidePacket=Module[\"_addBoolToInputSidePacket\"]=wasmExports[\"Wd\"];_addDoubleToInputSidePacket=Module[\"_addDoubleToInputSidePacket\"]=wasmExports[\"Xd\"];_addFloatToInputSidePacket=Module[\"_addFloatToInputSidePacket\"]=wasmExports[\"Yd\"];_addIntToInputSidePacket=Module[\"_addIntToInputSidePacket\"]=wasmExports[\"Zd\"];_addUintToInputSidePacket=Module[\"_addUintToInputSidePacket\"]=wasmExports[\"_d\"];_addStringToInputSidePacket=Module[\"_addStringToInputSidePacket\"]=wasmExports[\"$d\"];_addRawDataSpanToInputSidePacket=Module[\"_addRawDataSpanToInputSidePacket\"]=wasmExports[\"ae\"];_addProtoToInputSidePacket=Module[\"_addProtoToInputSidePacket\"]=wasmExports[\"be\"];_addBoolVectorToInputSidePacket=Module[\"_addBoolVectorToInputSidePacket\"]=wasmExports[\"ce\"];_addDoubleVectorToInputSidePacket=Module[\"_addDoubleVectorToInputSidePacket\"]=wasmExports[\"de\"];_addFloatVectorToInputSidePacket=Module[\"_addFloatVectorToInputSidePacket\"]=wasmExports[\"ee\"];_addIntVectorToInputSidePacket=Module[\"_addIntVectorToInputSidePacket\"]=wasmExports[\"fe\"];_addUintVectorToInputSidePacket=Module[\"_addUintVectorToInputSidePacket\"]=wasmExports[\"ge\"];_addStringVectorToInputSidePacket=Module[\"_addStringVectorToInputSidePacket\"]=wasmExports[\"he\"];_attachBoolListener=Module[\"_attachBoolListener\"]=wasmExports[\"ie\"];_attachBoolVectorListener=Module[\"_attachBoolVectorListener\"]=wasmExports[\"je\"];_attachDoubleListener=Module[\"_attachDoubleListener\"]=wasmExports[\"ke\"];_attachDoubleVectorListener=Module[\"_attachDoubleVectorListener\"]=wasmExports[\"le\"];_attachFloatListener=Module[\"_attachFloatListener\"]=wasmExports[\"me\"];_attachFloatVectorListener=Module[\"_attachFloatVectorListener\"]=wasmExports[\"ne\"];_attachIntListener=Module[\"_attachIntListener\"]=wasmExports[\"oe\"];_attachIntVectorListener=Module[\"_attachIntVectorListener\"]=wasmExports[\"pe\"];_attachUintListener=Module[\"_attachUintListener\"]=wasmExports[\"qe\"];_attachUintVectorListener=Module[\"_attachUintVectorListener\"]=wasmExports[\"re\"];_attachStringListener=Module[\"_attachStringListener\"]=wasmExports[\"se\"];_attachStringVectorListener=Module[\"_attachStringVectorListener\"]=wasmExports[\"te\"];_attachProtoListener=Module[\"_attachProtoListener\"]=wasmExports[\"ue\"];_attachProtoVectorListener=Module[\"_attachProtoVectorListener\"]=wasmExports[\"ve\"];_getGraphConfig=Module[\"_getGraphConfig\"]=wasmExports[\"we\"];___getTypeName=wasmExports[\"xe\"];_emwgpuCreateBindGroup=wasmExports[\"ye\"];_emwgpuCreateBindGroupLayout=wasmExports[\"ze\"];_emwgpuCreateCommandBuffer=wasmExports[\"Ae\"];_emwgpuCreateCommandEncoder=wasmExports[\"Be\"];_emwgpuCreateComputePassEncoder=wasmExports[\"Ce\"];_emwgpuCreateComputePipeline=wasmExports[\"De\"];_emwgpuCreateExternalTexture=wasmExports[\"Ee\"];_emwgpuCreatePipelineLayout=wasmExports[\"Fe\"];_emwgpuCreateQuerySet=wasmExports[\"Ge\"];_emwgpuCreateRenderBundle=wasmExports[\"He\"];_emwgpuCreateRenderBundleEncoder=wasmExports[\"Ie\"];_emwgpuCreateRenderPassEncoder=wasmExports[\"Je\"];_emwgpuCreateRenderPipeline=wasmExports[\"Ke\"];_emwgpuCreateSampler=wasmExports[\"Le\"];_emwgpuCreateSurface=wasmExports[\"Me\"];_emwgpuCreateTexture=wasmExports[\"Ne\"];_emwgpuCreateTextureView=wasmExports[\"Oe\"];_emwgpuCreateAdapter=wasmExports[\"Pe\"];_emwgpuCreateBuffer=wasmExports[\"Qe\"];_emwgpuCreateDevice=wasmExports[\"Re\"];_emwgpuCreateQueue=wasmExports[\"Se\"];_emwgpuCreateShaderModule=wasmExports[\"Te\"];_emwgpuOnCreateComputePipelineCompleted=wasmExports[\"Ue\"];_emwgpuOnCreateRenderPipelineCompleted=wasmExports[\"Ve\"];_clearSubgraphs=Module[\"_clearSubgraphs\"]=wasmExports[\"We\"];_pushBinarySubgraph=Module[\"_pushBinarySubgraph\"]=wasmExports[\"Xe\"];_pushTextSubgraph=Module[\"_pushTextSubgraph\"]=wasmExports[\"Ye\"];_changeBinaryGraph=Module[\"_changeBinaryGraph\"]=wasmExports[\"Ze\"];_changeTextGraph=Module[\"_changeTextGraph\"]=wasmExports[\"_e\"];_processGl=Module[\"_processGl\"]=wasmExports[\"$e\"];_process=Module[\"_process\"]=wasmExports[\"af\"];_bindTextureToCanvas=Module[\"_bindTextureToCanvas\"]=wasmExports[\"bf\"];_requestShaderRefreshOnGraphChange=Module[\"_requestShaderRefreshOnGraphChange\"]=wasmExports[\"cf\"];_waitUntilIdle=Module[\"_waitUntilIdle\"]=wasmExports[\"df\"];_closeGraph=Module[\"_closeGraph\"]=wasmExports[\"ef\"];_setAutoRenderToScreen=Module[\"_setAutoRenderToScreen\"]=wasmExports[\"ff\"];_emscripten_builtin_memalign=wasmExports[\"gf\"];_memalign=wasmExports[\"hf\"];__emscripten_tempret_set=wasmExports[\"jf\"];__emscripten_stack_restore=wasmExports[\"kf\"];__emscripten_stack_alloc=wasmExports[\"lf\"];_emscripten_stack_get_current=wasmExports[\"mf\"];dynCall_ji=wasmExports[\"dynCall_ji\"];dynCall_jii=wasmExports[\"dynCall_jii\"];dynCall_iiiijij=wasmExports[\"dynCall_iiiijij\"];dynCall_viiji=wasmExports[\"dynCall_viiji\"];dynCall_viji=wasmExports[\"dynCall_viji\"];dynCall_iiiji=wasmExports[\"dynCall_iiiji\"];dynCall_jjj=wasmExports[\"dynCall_jjj\"];dynCall_iiiijj=wasmExports[\"dynCall_iiiijj\"];dynCall_viijj=wasmExports[\"dynCall_viijj\"];dynCall_viiijjj=wasmExports[\"dynCall_viiijjj\"];dynCall_vij=wasmExports[\"dynCall_vij\"];dynCall_viijii=wasmExports[\"dynCall_viijii\"];dynCall_viiiji=wasmExports[\"dynCall_viiiji\"];dynCall_vijjj=wasmExports[\"dynCall_vijjj\"];dynCall_vj=wasmExports[\"dynCall_vj\"];dynCall_viij=wasmExports[\"dynCall_viij\"];dynCall_jiji=wasmExports[\"dynCall_jiji\"];dynCall_iiiiij=wasmExports[\"dynCall_iiiiij\"];dynCall_iiiiijj=wasmExports[\"dynCall_iiiiijj\"];dynCall_iiiiiijj=wasmExports[\"dynCall_iiiiiijj\"];memory=wasmMemory=wasmExports[\"id\"];__indirect_function_table=wasmTable=wasmExports[\"kd\"]}var _kVersionStampBuildChangelistStr=Module[\"_kVersionStampBuildChangelistStr\"]=1024;var _kVersionStampCitcSnapshotStr=Module[\"_kVersionStampCitcSnapshotStr\"]=1056;var _kVersionStampCitcWorkspaceIdStr=Module[\"_kVersionStampCitcWorkspaceIdStr\"]=1088;var _kVersionStampSourceUriStr=Module[\"_kVersionStampSourceUriStr\"]=1600;var _kVersionStampBuildClientStr=Module[\"_kVersionStampBuildClientStr\"]=2112;var _kVersionStampBuildClientMintStatusStr=Module[\"_kVersionStampBuildClientMintStatusStr\"]=2624;var _kVersionStampBuildCompilerStr=Module[\"_kVersionStampBuildCompilerStr\"]=2656;var _kVersionStampBuildDateTimePstStr=Module[\"_kVersionStampBuildDateTimePstStr\"]=3168;var _kVersionStampBuildDepotPathStr=Module[\"_kVersionStampBuildDepotPathStr\"]=3200;var _kVersionStampBuildIdStr=Module[\"_kVersionStampBuildIdStr\"]=3712;var _kVersionStampBuildInfoStr=Module[\"_kVersionStampBuildInfoStr\"]=4224;var _kVersionStampBuildLabelStr=Module[\"_kVersionStampBuildLabelStr\"]=4736;var _kVersionStampBuildTargetStr=Module[\"_kVersionStampBuildTargetStr\"]=5248;var _kVersionStampBuildTimestampStr=Module[\"_kVersionStampBuildTimestampStr\"]=5760;var _kVersionStampBuildToolStr=Module[\"_kVersionStampBuildToolStr\"]=5792;var _kVersionStampG3BuildTargetStr=Module[\"_kVersionStampG3BuildTargetStr\"]=6304;var _kVersionStampVerifiableStr=Module[\"_kVersionStampVerifiableStr\"]=6816;var _kVersionStampBuildFdoTypeStr=Module[\"_kVersionStampBuildFdoTypeStr\"]=6848;var _kVersionStampBuildBaselineChangelistStr=Module[\"_kVersionStampBuildBaselineChangelistStr\"]=6880;var _kVersionStampBuildLtoTypeStr=Module[\"_kVersionStampBuildLtoTypeStr\"]=6912;var _kVersionStampBuildPropellerTypeStr=Module[\"_kVersionStampBuildPropellerTypeStr\"]=6944;var _kVersionStampBuildPghoTypeStr=Module[\"_kVersionStampBuildPghoTypeStr\"]=6976;var _kVersionStampBuildUsernameStr=Module[\"_kVersionStampBuildUsernameStr\"]=7008;var _kVersionStampBuildHostnameStr=Module[\"_kVersionStampBuildHostnameStr\"]=7520;var _kVersionStampBuildDirectoryStr=Module[\"_kVersionStampBuildDirectoryStr\"]=8032;var _kVersionStampBuildChangelistInt=Module[\"_kVersionStampBuildChangelistInt\"]=8544;var _kVersionStampCitcSnapshotInt=Module[\"_kVersionStampCitcSnapshotInt\"]=8552;var _kVersionStampBuildClientMintStatusInt=Module[\"_kVersionStampBuildClientMintStatusInt\"]=8556;var _kVersionStampBuildTimestampInt=Module[\"_kVersionStampBuildTimestampInt\"]=8560;var _kVersionStampVerifiableInt=Module[\"_kVersionStampVerifiableInt\"]=8568;var _kVersionStampBuildCoverageEnabledInt=Module[\"_kVersionStampBuildCoverageEnabledInt\"]=8572;var _kVersionStampBuildBaselineChangelistInt=Module[\"_kVersionStampBuildBaselineChangelistInt\"]=8576;var _kVersionStampPrecookedTimestampStr=Module[\"_kVersionStampPrecookedTimestampStr\"]=8592;var _kVersionStampPrecookedClientInfoStr=Module[\"_kVersionStampPrecookedClientInfoStr\"]=9104;var wasmImports={hd:BeginGlQueryTiming,gd:EndGlQueryTiming,fd:GetAdapterArchitecture,ed:GetAdapterDescription,dd:GetAdapterDeviceName,cd:GetAdapterVendor,bd:JsOnEmptyPacketListener,ad:JsOnFloat32ArrayImageListener,$c:JsOnFloat32ArrayImageVectorListener,pb:JsOnSimpleListenerBinaryArray,_c:JsOnSimpleListenerBool,Zc:JsOnSimpleListenerDouble,Yc:JsOnSimpleListenerFloat,Xc:JsOnSimpleListenerInt,Wc:JsOnSimpleListenerString,Vc:JsOnSimpleListenerUint,Uc:JsOnUint8ArrayImageListener,Tc:JsOnUint8ArrayImageVectorListener,O:JsOnVectorFinishedListener,Sc:JsOnVectorListenerBool,Rc:JsOnVectorListenerDouble,Qc:JsOnVectorListenerFloat,Pc:JsOnVectorListenerInt,Oc:JsOnVectorListenerProto,Nc:JsOnVectorListenerString,Mc:JsOnVectorListenerUint,Lc:JsOnWebGLTextureListener,Kc:JsOnWebGLTextureVectorListener,Pa:JsWrapErrorListener,ob:JsWrapImageConverter,u:JsWrapSimpleListeners,nb:UseBottomLeftGpuOrigin,yb:__asyncjs__mediapipe_map_buffer_jspi,o:___cxa_throw,Jc:___syscall_dup,Ic:___syscall_faccessat,mb:___syscall_fcntl64,Hc:___syscall_fstat64,Mb:___syscall_ftruncate64,Gc:___syscall_ioctl,Fc:___syscall_lstat64,Ec:___syscall_newfstatat,lb:___syscall_openat,Dc:___syscall_stat64,yc:__abort_js,Jb:__embind_register_bigint,xc:__embind_register_bool,wc:__embind_register_emval,jb:__embind_register_float,I:__embind_register_integer,r:__embind_register_memory_view,vc:__embind_register_std_string,Ma:__embind_register_std_wstring,uc:__embind_register_void,$:__emval_create_invoker,q:__emval_decref,La:__emval_get_global,ib:__emval_get_property,ja:__emval_incref,Ka:__emval_instanceof,_:__emval_invoke,ua:__emval_new_cstring,Z:__emval_run_destructors,hb:__emval_set_property,tc:__emval_typeof,Ib:__gmtime_js,Hb:__localtime_js,Gb:__mktime_js,Fb:__mmap_js,Eb:__munmap_js,sc:__tzset_js,Lb:_clock_time_get,rc:custom_emscripten_dbgn,Y:_emscripten_asm_const_int,gb:_emscripten_asm_const_ptr,Ja:_emscripten_errn,qc:_emscripten_get_heap_max,y:_emscripten_get_now,ia:_emscripten_has_asyncify,pc:_emscripten_outn,oc:_emscripten_pc_get_function,nc:_emscripten_resize_heap,fb:_emscripten_stack_snapshot,mc:_emscripten_stack_unwind_buffer,lc:_emscripten_webgl_create_context,kc:_emscripten_webgl_destroy_context,jc:_emscripten_webgl_get_context_attributes,ha:_emscripten_webgl_get_current_context,ic:_emscripten_webgl_make_context_current,Q:_emscripten_webgpu_get_device,hc:_emwgpuBufferDestroy,gc:_emwgpuBufferGetMappedRange,fc:_emwgpuBufferUnmap,x:_emwgpuDelete,ec:_emwgpuDeviceCreateBuffer,Db:_emwgpuDeviceCreateComputePipelineAsync,Cb:_emwgpuDeviceCreateRenderPipelineAsync,dc:_emwgpuDeviceCreateShaderModule,cc:_emwgpuDeviceDestroy,bc:_emwgpuWaitAny,Cc:_environ_get,Bc:_environ_sizes_get,eb:_exit,Oa:_fd_close,kb:_fd_read,Kb:_fd_seek,Na:_fd_write,b:_glActiveTexture,ta:_glAttachShader,ac:_glBindAttribLocation,c:_glBindBuffer,db:_glBindBufferBase,t:_glBindFramebuffer,a:_glBindTexture,m:_glBindVertexArray,cb:_glBlendEquation,$b:_glBlendFunc,j:_glBufferData,H:_glClear,X:_glClearColor,da:_glClientWaitSync,ga:_glColorMask,bb:_glCompileShader,ab:_glCreateProgram,$a:_glCreateShader,p:_glDeleteBuffers,P:_glDeleteFramebuffers,h:_glDeleteProgram,sa:_glDeleteShader,ra:_glDeleteSync,D:_glDeleteTextures,A:_glDeleteVertexArrays,_a:_glDetachShader,G:_glDisable,n:_glDisableVertexAttribArray,i:_glDrawArrays,fa:_glDrawBuffers,_b:_glEnable,l:_glEnableVertexAttribArray,Za:_glFenceSync,qa:_glFinish,v:_glFlush,C:_glFramebufferTexture2D,Ya:_glFramebufferTextureLayer,s:_glGenBuffers,W:_glGenFramebuffers,F:_glGenTextures,B:_glGenVertexArrays,Xa:_glGetAttribLocation,ea:_glGetError,Zb:_glGetFloatv,w:_glGetIntegerv,Yb:_glGetProgramiv,Xb:_glGetShaderInfoLog,Wb:_glGetShaderiv,N:_glGetString,Vb:_glGetUniformBlockIndex,d:_glGetUniformLocation,Ub:_glLineWidth,Wa:_glLinkProgram,pa:_glPixelStorei,oa:_glReadPixels,Va:_glShaderSource,E:_glTexImage2D,na:_glTexParameterf,Ua:_glTexParameterfv,f:_glTexParameteri,ma:_glTexStorage2D,Tb:_glTexStorage3D,V:_glTexSubImage2D,Sb:_glTexSubImage3D,M:_glUniform1f,la:_glUniform1fv,e:_glUniform1i,U:_glUniform2f,Rb:_glUniform2fv,Ia:_glUniform3f,Ta:_glUniform4f,T:_glUniform4fv,Qb:_glUniform4iv,Pb:_glUniformBlockBinding,Ob:_glUniformMatrix2fv,Nb:_glUniformMatrix3fv,Ha:_glUniformMatrix4fv,g:_glUseProgram,k:_glVertexAttribPointer,S:_glViewport,Ga:hardware_concurrency,Bb:mediapipe_create_utility_canvas2d,Ab:_mediapipe_find_canvas_event_target,zb:mediapipe_import_external_texture,xb:_mediapipe_webgl_tex_image_drawable,Ac:_proc_exit,zc:_random_get,Fa:_wgpuCommandEncoderBeginComputePass,Ea:_wgpuCommandEncoderBeginRenderPass,wb:_wgpuCommandEncoderCopyBufferToTexture,vb:_wgpuCommandEncoderCopyTextureToBuffer,ub:_wgpuCommandEncoderCopyTextureToTexture,L:_wgpuCommandEncoderFinish,Da:_wgpuComputePassEncoderDispatchWorkgroups,Ca:_wgpuComputePassEncoderEnd,Ba:_wgpuComputePassEncoderSetBindGroup,Aa:_wgpuComputePassEncoderSetPipeline,za:_wgpuComputePipelineGetBindGroupLayout,ca:_wgpuDeviceCreateBindGroup,tb:_wgpuDeviceCreateBindGroupLayout,K:_wgpuDeviceCreateCommandEncoder,sb:_wgpuDeviceCreateComputePipeline,rb:_wgpuDeviceCreatePipelineLayout,Sa:_wgpuDeviceCreateRenderPipeline,R:_wgpuDeviceCreateSampler,ba:_wgpuDeviceCreateTexture,J:_wgpuQueueSubmit,ka:_wgpuQueueWriteBuffer,qb:_wgpuQueueWriteTexture,ya:_wgpuRenderPassEncoderDraw,xa:_wgpuRenderPassEncoderEnd,wa:_wgpuRenderPassEncoderSetBindGroup,va:_wgpuRenderPassEncoderSetPipeline,Ra:_wgpuRenderPipelineGetBindGroupLayout,z:_wgpuTextureCreateView,Qa:_wgpuTextureDestroy,aa:_wgpuTextureGetFormat};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module[\"calledRun\"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module[\"onRuntimeInitialized\"]?.();postRun()}if(Module[\"setStatus\"]){Module[\"setStatus\"](\"Running...\");setTimeout(()=>{setTimeout(()=>Module[\"setStatus\"](\"\"),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}\n;return moduleRtn}})();if(typeof exports===\"object\"&&typeof module===\"object\"){module.exports=ModuleFactory;module.exports.default=ModuleFactory}else if(typeof define===\"function\"&&define[\"amd\"])define([],()=>ModuleFactory);\n"
  },
  {
    "path": "src/assets/mediapipeVision/vision_wasm_nosimd_internal.js",
    "content": "var ModuleFactory=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!=\"renderer\";var arguments_=[];var thisProgram=\"./this.program\";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!=\"undefined\"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory=\"\";function locateFile(path){if(Module[\"locateFile\"]){return Module[\"locateFile\"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require(\"fs\");scriptDirectory=__dirname+\"/\";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:\"utf8\");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\\\/g,\"/\")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(\".\",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,false);xhr.responseType=\"arraybuffer\";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{if(isFileURI(url)){return new Promise((resolve,reject)=>{var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,true);xhr.responseType=\"arraybuffer\";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){resolve(xhr.response);return}reject(xhr.status)};xhr.onerror=reject;xhr.send(null)})}var response=await fetch(url,{credentials:\"same-origin\"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+\" : \"+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var isFileURI=filename=>filename.startsWith(\"file://\");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module[\"HEAPU8\"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);Module[\"HEAPU32\"]=HEAPU32=new Uint32Array(b);Module[\"HEAPF32\"]=HEAPF32=new Float32Array(b);Module[\"HEAPF64\"]=HEAPF64=new Float64Array(b)}function preRun(){if(Module[\"preRun\"]){if(typeof Module[\"preRun\"]==\"function\")Module[\"preRun\"]=[Module[\"preRun\"]];while(Module[\"preRun\"].length){addOnPreRun(Module[\"preRun\"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module[\"noFSInit\"]&&!FS.initialized)FS.init();TTY.init();wasmExports[\"id\"]();FS.ignorePermissions=false}function postRun(){if(Module[\"postRun\"]){if(typeof Module[\"postRun\"]==\"function\")Module[\"postRun\"]=[Module[\"postRun\"]];while(Module[\"postRun\"].length){addOnPostRun(Module[\"postRun\"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module[\"onAbort\"]?.(what);what=\"Aborted(\"+what+\")\";err(what);ABORT=true;what+=\". Build with -sASSERTIONS for more info.\";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile(\"vision_wasm_nosimd_internal.wasm\")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw\"both async and sync fetching of the wasm failed\"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:\"same-origin\"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err(\"falling back to ArrayBuffer instantiation\")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result[\"instance\"])}var info=getWasmImports();if(Module[\"instantiateWasm\"]){return new Promise((resolve,reject)=>{Module[\"instantiateWasm\"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}var tempDouble;var tempI64;var handleException=e=>{if(e instanceof ExitStatus||e==\"unwind\"){return EXITSTATUS}quit_(1,e)};class ExitStatus{name=\"ExitStatus\";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module[\"onExit\"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{func();maybeExit()}catch(e){handleException(e)}};function getFullscreenElement(){return document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement||document.msFullscreenElement}var safeSetTimeout=(func,timeout)=>setTimeout(()=>{callUserCallback(func)},timeout);var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text=\"warning: \"+text;err(text)}};var preloadPlugins=[];var Browser={useWebGL:false,isFullscreen:false,pointerLock:false,moduleContextCreatedCallbacks:[],workers:[],preloadedImages:{},preloadedAudios:{},getCanvas:()=>Module[\"canvas\"],init(){if(Browser.initted)return;Browser.initted=true;var imagePlugin={};imagePlugin[\"canHandle\"]=function imagePlugin_canHandle(name){return!Module[\"noImageDecoding\"]&&/\\.(jpg|jpeg|png|bmp|webp)$/i.test(name)};imagePlugin[\"handle\"]=async function imagePlugin_handle(byteArray,name){var b=new Blob([byteArray],{type:Browser.getMimetype(name)});if(b.size!==byteArray.length){b=new Blob([new Uint8Array(byteArray).buffer],{type:Browser.getMimetype(name)})}var url=URL.createObjectURL(b);return new Promise((resolve,reject)=>{var img=new Image;img.onload=()=>{var canvas=document.createElement(\"canvas\");canvas.width=img.width;canvas.height=img.height;var ctx=canvas.getContext(\"2d\");ctx.drawImage(img,0,0);Browser.preloadedImages[name]=canvas;URL.revokeObjectURL(url);resolve(byteArray)};img.onerror=event=>{err(`Image ${url} could not be decoded`);reject()};img.src=url})};preloadPlugins.push(imagePlugin);var audioPlugin={};audioPlugin[\"canHandle\"]=function audioPlugin_canHandle(name){return!Module[\"noAudioDecoding\"]&&name.slice(-4)in{\".ogg\":1,\".wav\":1,\".mp3\":1}};audioPlugin[\"handle\"]=async function audioPlugin_handle(byteArray,name){return new Promise((resolve,reject)=>{var done=false;function finish(audio){if(done)return;done=true;Browser.preloadedAudios[name]=audio;resolve(byteArray)}var b=new Blob([byteArray],{type:Browser.getMimetype(name)});var url=URL.createObjectURL(b);var audio=new Audio;audio.addEventListener(\"canplaythrough\",()=>finish(audio),false);audio.onerror=function audio_onerror(event){if(done)return;err(`warning: browser could not fully decode audio ${name}, trying slower base64 approach`);function encode64(data){var BASE=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";var PAD=\"=\";var ret=\"\";var leftchar=0;var leftbits=0;for(var i=0;i<data.length;i++){leftchar=leftchar<<8|data[i];leftbits+=8;while(leftbits>=6){var curr=leftchar>>leftbits-6&63;leftbits-=6;ret+=BASE[curr]}}if(leftbits==2){ret+=BASE[(leftchar&3)<<4];ret+=PAD+PAD}else if(leftbits==4){ret+=BASE[(leftchar&15)<<2];ret+=PAD}return ret}audio.src=\"data:audio/x-\"+name.slice(-3)+\";base64,\"+encode64(byteArray);finish(audio)};audio.src=url;safeSetTimeout(()=>{finish(audio)},1e4)})};preloadPlugins.push(audioPlugin);function pointerLockChange(){var canvas=Browser.getCanvas();Browser.pointerLock=document.pointerLockElement===canvas}var canvas=Browser.getCanvas();if(canvas){document.addEventListener(\"pointerlockchange\",pointerLockChange,false);if(Module[\"elementPointerLock\"]){canvas.addEventListener(\"click\",ev=>{if(!Browser.pointerLock&&Browser.getCanvas().requestPointerLock){Browser.getCanvas().requestPointerLock();ev.preventDefault()}},false)}}},createContext(canvas,useWebGL,setInModule,webGLContextAttributes){if(useWebGL&&Module[\"ctx\"]&&canvas==Browser.getCanvas())return Module[\"ctx\"];var ctx;var contextHandle;if(useWebGL){var contextAttributes={antialias:false,alpha:false,majorVersion:typeof WebGL2RenderingContext!=\"undefined\"?2:1};if(webGLContextAttributes){for(var attribute in webGLContextAttributes){contextAttributes[attribute]=webGLContextAttributes[attribute]}}if(typeof GL!=\"undefined\"){contextHandle=GL.createContext(canvas,contextAttributes);if(contextHandle){ctx=GL.getContext(contextHandle).GLctx}}}else{ctx=canvas.getContext(\"2d\")}if(!ctx)return null;if(setInModule){Module[\"ctx\"]=ctx;if(useWebGL)GL.makeContextCurrent(contextHandle);Browser.useWebGL=useWebGL;Browser.moduleContextCreatedCallbacks.forEach(callback=>callback());Browser.init()}return ctx},fullscreenHandlersInstalled:false,lockPointer:undefined,resizeCanvas:undefined,requestFullscreen(lockPointer,resizeCanvas){Browser.lockPointer=lockPointer;Browser.resizeCanvas=resizeCanvas;if(typeof Browser.lockPointer==\"undefined\")Browser.lockPointer=true;if(typeof Browser.resizeCanvas==\"undefined\")Browser.resizeCanvas=false;var canvas=Browser.getCanvas();function fullscreenChange(){Browser.isFullscreen=false;var canvasContainer=canvas.parentNode;if(getFullscreenElement()===canvasContainer){canvas.exitFullscreen=Browser.exitFullscreen;if(Browser.lockPointer)canvas.requestPointerLock();Browser.isFullscreen=true;if(Browser.resizeCanvas){Browser.setFullscreenCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}else{canvasContainer.parentNode.insertBefore(canvas,canvasContainer);canvasContainer.parentNode.removeChild(canvasContainer);if(Browser.resizeCanvas){Browser.setWindowedCanvasSize()}else{Browser.updateCanvasDimensions(canvas)}}Module[\"onFullScreen\"]?.(Browser.isFullscreen);Module[\"onFullscreen\"]?.(Browser.isFullscreen)}if(!Browser.fullscreenHandlersInstalled){Browser.fullscreenHandlersInstalled=true;document.addEventListener(\"fullscreenchange\",fullscreenChange,false);document.addEventListener(\"mozfullscreenchange\",fullscreenChange,false);document.addEventListener(\"webkitfullscreenchange\",fullscreenChange,false);document.addEventListener(\"MSFullscreenChange\",fullscreenChange,false)}var canvasContainer=document.createElement(\"div\");canvas.parentNode.insertBefore(canvasContainer,canvas);canvasContainer.appendChild(canvas);canvasContainer.requestFullscreen=canvasContainer[\"requestFullscreen\"]||canvasContainer[\"mozRequestFullScreen\"]||canvasContainer[\"msRequestFullscreen\"]||(canvasContainer[\"webkitRequestFullscreen\"]?()=>canvasContainer[\"webkitRequestFullscreen\"](Element[\"ALLOW_KEYBOARD_INPUT\"]):null)||(canvasContainer[\"webkitRequestFullScreen\"]?()=>canvasContainer[\"webkitRequestFullScreen\"](Element[\"ALLOW_KEYBOARD_INPUT\"]):null);canvasContainer.requestFullscreen()},exitFullscreen(){if(!Browser.isFullscreen){return false}var CFS=document[\"exitFullscreen\"]||document[\"cancelFullScreen\"]||document[\"mozCancelFullScreen\"]||document[\"msExitFullscreen\"]||document[\"webkitCancelFullScreen\"]||(()=>{});CFS.apply(document,[]);return true},safeSetTimeout(func,timeout){return safeSetTimeout(func,timeout)},getMimetype(name){return{jpg:\"image/jpeg\",jpeg:\"image/jpeg\",png:\"image/png\",bmp:\"image/bmp\",ogg:\"audio/ogg\",wav:\"audio/wav\",mp3:\"audio/mpeg\"}[name.slice(name.lastIndexOf(\".\")+1)]},getUserMedia(func){window.getUserMedia||=navigator[\"getUserMedia\"]||navigator[\"mozGetUserMedia\"];window.getUserMedia(func)},getMovementX(event){return event[\"movementX\"]||event[\"mozMovementX\"]||event[\"webkitMovementX\"]||0},getMovementY(event){return event[\"movementY\"]||event[\"mozMovementY\"]||event[\"webkitMovementY\"]||0},getMouseWheelDelta(event){var delta=0;switch(event.type){case\"DOMMouseScroll\":delta=event.detail/3;break;case\"mousewheel\":delta=event.wheelDelta/120;break;case\"wheel\":delta=event.deltaY;switch(event.deltaMode){case 0:delta/=100;break;case 1:delta/=3;break;case 2:delta*=80;break;default:abort(\"unrecognized mouse wheel delta mode: \"+event.deltaMode)}break;default:abort(\"unrecognized mouse wheel event: \"+event.type)}return delta},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseCoords(pageX,pageY){var canvas=Browser.getCanvas();var rect=canvas.getBoundingClientRect();var scrollX=typeof window.scrollX!=\"undefined\"?window.scrollX:window.pageXOffset;var scrollY=typeof window.scrollY!=\"undefined\"?window.scrollY:window.pageYOffset;var adjustedX=pageX-(scrollX+rect.left);var adjustedY=pageY-(scrollY+rect.top);adjustedX=adjustedX*(canvas.width/rect.width);adjustedY=adjustedY*(canvas.height/rect.height);return{x:adjustedX,y:adjustedY}},setMouseCoords(pageX,pageY){const{x,y}=Browser.calculateMouseCoords(pageX,pageY);Browser.mouseMovementX=x-Browser.mouseX;Browser.mouseMovementY=y-Browser.mouseY;Browser.mouseX=x;Browser.mouseY=y},calculateMouseEvent(event){if(Browser.pointerLock){if(event.type!=\"mousemove\"&&\"mozMovementX\"in event){Browser.mouseMovementX=Browser.mouseMovementY=0}else{Browser.mouseMovementX=Browser.getMovementX(event);Browser.mouseMovementY=Browser.getMovementY(event)}Browser.mouseX+=Browser.mouseMovementX;Browser.mouseY+=Browser.mouseMovementY}else{if(event.type===\"touchstart\"||event.type===\"touchend\"||event.type===\"touchmove\"){var touch=event.touch;if(touch===undefined){return}var coords=Browser.calculateMouseCoords(touch.pageX,touch.pageY);if(event.type===\"touchstart\"){Browser.lastTouches[touch.identifier]=coords;Browser.touches[touch.identifier]=coords}else if(event.type===\"touchend\"||event.type===\"touchmove\"){var last=Browser.touches[touch.identifier];last||=coords;Browser.lastTouches[touch.identifier]=last;Browser.touches[touch.identifier]=coords}return}Browser.setMouseCoords(event.pageX,event.pageY)}},resizeListeners:[],updateResizeListeners(){var canvas=Browser.getCanvas();Browser.resizeListeners.forEach(listener=>listener(canvas.width,canvas.height))},setCanvasSize(width,height,noUpdates){var canvas=Browser.getCanvas();Browser.updateCanvasDimensions(canvas,width,height);if(!noUpdates)Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize(){if(typeof SDL!=\"undefined\"){var flags=HEAPU32[SDL.screen>>2];flags=flags|8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Browser.getCanvas());Browser.updateResizeListeners()},setWindowedCanvasSize(){if(typeof SDL!=\"undefined\"){var flags=HEAPU32[SDL.screen>>2];flags=flags&~8388608;HEAP32[SDL.screen>>2]=flags}Browser.updateCanvasDimensions(Browser.getCanvas());Browser.updateResizeListeners()},updateCanvasDimensions(canvas,wNative,hNative){if(wNative&&hNative){canvas.widthNative=wNative;canvas.heightNative=hNative}else{wNative=canvas.widthNative;hNative=canvas.heightNative}var w=wNative;var h=hNative;if(Module[\"forcedAspectRatio\"]>0){if(w/h<Module[\"forcedAspectRatio\"]){w=Math.round(h*Module[\"forcedAspectRatio\"])}else{h=Math.round(w/Module[\"forcedAspectRatio\"])}}if(getFullscreenElement()===canvas.parentNode&&typeof screen!=\"undefined\"){var factor=Math.min(screen.width/w,screen.height/h);w=Math.round(w*factor);h=Math.round(h*factor)}if(Browser.resizeCanvas){if(canvas.width!=w)canvas.width=w;if(canvas.height!=h)canvas.height=h;if(typeof canvas.style!=\"undefined\"){canvas.style.removeProperty(\"width\");canvas.style.removeProperty(\"height\")}}else{if(canvas.width!=wNative)canvas.width=wNative;if(canvas.height!=hNative)canvas.height=hNative;if(typeof canvas.style!=\"undefined\"){if(w!=wNative||h!=hNative){canvas.style.setProperty(\"width\",w+\"px\",\"important\");canvas.style.setProperty(\"height\",h+\"px\",\"important\")}else{canvas.style.removeProperty(\"width\");canvas.style.removeProperty(\"height\")}}}}};var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var PATH={isAbs:path=>path.charAt(0)===\"/\",splitPath:filename=>{var splitPathRe=/^(\\/?|)([\\s\\S]*?)((?:\\.{1,2}|[^\\/]+?|)(\\.[^.\\/]*|))(?:[\\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last===\".\"){parts.splice(i,1)}else if(last===\"..\"){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift(\"..\")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)===\"/\";path=PATH.normalizeArray(path.split(\"/\").filter(p=>!!p),!isAbsolute).join(\"/\");if(!path&&!isAbsolute){path=\".\"}if(path&&trailingSlash){path+=\"/\"}return(isAbsolute?\"/\":\"\")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return\".\"}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\\/]+|\\/)\\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join(\"/\")),join2:(l,r)=>PATH.normalize(l+\"/\"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require(\"crypto\");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath=\"\",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!=\"string\"){throw new TypeError(\"Arguments to path.resolve must be strings\")}else if(!path){return\"\"}resolvedPath=path+\"/\"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split(\"/\").filter(p=>!!p),!resolvedAbsolute).join(\"/\");return(resolvedAbsolute?\"/\":\"\")+resolvedPath||\".\"},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[start]!==\"\")break}var end=arr.length-1;for(;end>=0;end--){if(arr[end]!==\"\")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split(\"/\"));var toParts=trim(to.split(\"/\"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i<length;i++){if(fromParts[i]!==toParts[i]){samePartsLength=i;break}}var outputParts=[];for(var i=samePartsLength;i<fromParts.length;i++){outputParts.push(\"..\")}outputParts=outputParts.concat(toParts.slice(samePartsLength));return outputParts.join(\"/\")}};var UTF8Decoder=new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(heapOrArray.buffer?heapOrArray.subarray(idx,endPtr):new Uint8Array(heapOrArray.slice(idx,endPtr)))};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes(\"EOF\"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString(\"utf-8\")}}else if(globalThis.window?.prompt){result=window.prompt(\"Input: \");if(result!==null){result+=\"\\n\"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i<length;i++){var result;try{result=stream.tty.ops.get_char(stream.tty)}catch(e){throw new FS.ErrnoError(29)}if(result===undefined&&bytesRead===0){throw new FS.ErrnoError(6)}if(result===null||result===undefined)break;bytesRead++;buffer[offset+i]=result}if(bytesRead){stream.node.atime=Date.now()}return bytesRead},write(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.put_char){throw new FS.ErrnoError(60)}try{for(var i=0;i<length;i++){stream.tty.ops.put_char(stream.tty,buffer[offset+i])}}catch(e){throw new FS.ErrnoError(29)}if(length){stream.node.mtime=stream.node.ctime=Date.now()}return i}},default_tty_ops:{get_char(tty){return FS_stdin_getChar()},put_char(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,\"/\",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity<CAPACITY_DOUBLING_MAX?2:1.125)>>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of[\"mode\",\"atime\",\"mtime\",\"ctime\"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=\"<generic error, no stack>\"}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[\".\",\"..\",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i<size;i++)buffer[offset+i]=contents[position+i]}return size},write(stream,buffer,offset,length,position,canOwn){if(buffer.buffer===HEAP8.buffer){canOwn=false}if(!length)return 0;var node=stream.node;node.mtime=node.ctime=Date.now();if(buffer.subarray&&(!node.contents||node.contents.subarray)){if(canOwn){node.contents=buffer.subarray(offset,offset+length);node.usedBytes=length;return length}else if(node.usedBytes===0&&position===0){node.contents=buffer.slice(offset,offset+length);node.usedBytes=length;return length}else if(position+length<=node.usedBytes){node.contents.set(buffer.subarray(offset,offset+length),position);return length}}MEMFS.expandFileStorage(node,position+length);if(node.contents.subarray&&buffer.subarray){node.contents.set(buffer.subarray(offset,offset+length),position)}else{for(var i=0;i<length;i++){node.contents[position+i]=buffer[offset+i]}}node.usedBytes=Math.max(node.usedBytes,position+length);return length},llseek(stream,offset,whence){var position=offset;if(whence===1){position+=stream.position}else if(whence===2){if(FS.isFile(stream.node.mode)){position+=stream.node.usedBytes}}if(position<0){throw new FS.ErrnoError(28)}return position},mmap(stream,length,position,prot,flags){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}var ptr;var allocated;var contents=stream.node.contents;if(!(flags&2)&&contents&&contents.buffer===HEAP8.buffer){allocated=false;ptr=contents.byteOffset}else{allocated=true;ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}if(contents){if(position>0||position+length<contents.length){if(contents.subarray){contents=contents.subarray(position,position+length)}else{contents=Array.prototype.slice.call(contents,position,position+length)}}HEAP8.set(contents,ptr)}}return{ptr,allocated}},msync(stream,buffer,offset,length,mmapFlags){MEMFS.stream_ops.write(stream,buffer,0,length,offset,false);return 0}}};var FS_modeStringToFlags=str=>{var flagModes={r:0,\"r+\":2,w:512|64|1,\"w+\":512|64|2,a:1024|64|1,\"a+\":1024|64|2};var flags=flagModes[str];if(typeof flags==\"undefined\"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module[\"monitorRunDependencies\"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module[\"monitorRunDependencies\"]?.(runDependencies)};var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!=\"undefined\")Browser.init();for(var plugin of preloadPlugins){if(plugin[\"canHandle\"](fullname)){return plugin[\"handle\"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url==\"string\"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:\"/\",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name=\"ErrnoError\";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+\"/\"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split(\"/\").filter(p=>!!p);var current=FS.root;var current_path=\"/\";for(var i=0;i<parts.length;i++){var islast=i===parts.length-1;if(islast&&opts.parent){break}if(parts[i]===\".\"){continue}if(parts[i]===\"..\"){current_path=PATH.dirname(current_path);if(FS.isRoot(current)){path=current_path+\"/\"+parts.slice(i+1).join(\"/\");nlinks--;continue linkloop}else{current=current.parent}continue}current_path=PATH.join2(current_path,parts[i]);try{current=FS.lookupNode(current,parts[i])}catch(e){if(e?.errno===44&&islast&&opts.noent_okay){return{path:current_path}}throw e}if(FS.isMountpoint(current)&&(!islast||opts.follow_mount)){current=current.mounted.root}if(FS.isLink(current.mode)&&(!islast||opts.follow)){if(!current.node_ops.readlink){throw new FS.ErrnoError(52)}var link=current.node_ops.readlink(current);if(!PATH.isAbs(link)){link=PATH.dirname(current_path)+\"/\"+link}path=link+\"/\"+parts.slice(i+1).join(\"/\");continue linkloop}}return{path:current_path,node:current}}throw new FS.ErrnoError(32)},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!==\"/\"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i<name.length;i++){hash=(hash<<5)-hash+name.charCodeAt(i)|0}return(parentid+hash>>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=[\"r\",\"w\",\"rw\"][flag&3];if(flag&512){perms+=\"w\"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes(\"r\")&&!(node.mode&292)){return 2}else if(perms.includes(\"w\")&&!(node.mode&146)){return 2}else if(perms.includes(\"x\")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,\"x\");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,\"wx\")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,\"wx\");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!==\"r\"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate==\"function\"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint===\"/\";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name===\".\"||name===\"..\"){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split(\"/\");var d=\"\";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+=\"/\";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev==\"undefined\"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!==\".\"){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!==\".\"){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,\"w\");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,\"w\");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path==\"string\"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===\"\"){throw new FS.ErrnoError(44)}flags=typeof flags==\"string\"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path==\"object\"){node=path}else{isDirPath=path.endsWith(\"/\");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module[\"logReadFiles\"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!=\"undefined\";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!=\"undefined\";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||\"binary\";if(opts.encoding!==\"utf8\"&&opts.encoding!==\"binary\"){abort(`Invalid encoding type \"${opts.encoding}\"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding===\"utf8\"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data==\"string\"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort(\"Unsupported data type\")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,\"x\");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir(\"/tmp\");FS.mkdir(\"/home\");FS.mkdir(\"/home/web_user\")},createDefaultDevices(){FS.mkdir(\"/dev\");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev(\"/dev/null\",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev(\"/dev/tty\",FS.makedev(5,0));FS.mkdev(\"/dev/tty1\",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice(\"/dev\",\"random\",randomByte);FS.createDevice(\"/dev\",\"urandom\",randomByte);FS.mkdir(\"/dev/shm\");FS.mkdir(\"/dev/shm/tmp\")},createSpecialDirectories(){FS.mkdir(\"/proc\");var proc_self=FS.mkdir(\"/proc/self\");FS.mkdir(\"/proc/self/fd\");FS.mount({mount(){var node=FS.createNode(proc_self,\"fd\",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:\"fake\"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},\"/proc/self/fd\")},createStandardStreams(input,output,error){if(input){FS.createDevice(\"/dev\",\"stdin\",input)}else{FS.symlink(\"/dev/tty\",\"/dev/stdin\")}if(output){FS.createDevice(\"/dev\",\"stdout\",null,output)}else{FS.symlink(\"/dev/tty\",\"/dev/stdout\")}if(error){FS.createDevice(\"/dev\",\"stderr\",null,error)}else{FS.symlink(\"/dev/tty1\",\"/dev/stderr\")}var stdin=FS.open(\"/dev/stdin\",0);var stdout=FS.open(\"/dev/stdout\",1);var stderr=FS.open(\"/dev/stderr\",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},\"/\");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module[\"stdin\"];output??=Module[\"stdout\"];error??=Module[\"stderr\"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path===\"/\"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent==\"string\"?parent:FS.getPath(parent);var parts=path.split(\"/\").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent==\"string\"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent==\"string\"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data==\"string\"){var arr=new Array(data.length);for(var i=0,len=data.length;i<len;++i)arr[i]=data.charCodeAt(i);data=arr}FS.chmod(node,mode|146);var stream=FS.open(node,577);FS.write(stream,data,0,data.length,0,canOwn);FS.close(stream);FS.chmod(node,mode)}},createDevice(parent,name,input,output){var path=PATH.join2(typeof parent==\"string\"?parent:FS.getPath(parent),name);var mode=FS_getMode(!!input,!!output);FS.createDevice.major??=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open(stream){stream.seekable=false},close(stream){if(output?.buffer?.length){output(10)}},read(stream,buffer,offset,length,pos){var bytesRead=0;for(var i=0;i<length;i++){var result;try{result=input()}catch(e){throw new FS.ErrnoError(29)}if(result===undefined&&bytesRead===0){throw new FS.ErrnoError(6)}if(result===null||result===undefined)break;bytesRead++;buffer[offset+i]=result}if(bytesRead){stream.node.atime=Date.now()}return bytesRead},write(stream,buffer,offset,length,pos){for(var i=0;i<length;i++){try{output(buffer[offset+i])}catch(e){throw new FS.ErrnoError(29)}}if(length){stream.node.mtime=stream.node.ctime=Date.now()}return i}});return FS.mkdev(path,mode,dev)},forceLoadFile(obj){if(obj.isDevice||obj.isFolder||obj.link||obj.contents)return true;if(globalThis.XMLHttpRequest){abort(\"Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.\")}else{try{obj.contents=readBinary(obj.url)}catch(e){throw new FS.ErrnoError(29)}}},createLazyFile(parent,name,url,canRead,canWrite){class LazyUint8Array{lengthKnown=false;chunks=[];get(idx){if(idx>this.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open(\"HEAD\",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort(\"Couldn't load \"+url+\". Status: \"+xhr.status);var datalength=Number(xhr.getResponseHeader(\"Content-length\"));var header;var hasByteServing=(header=xhr.getResponseHeader(\"Accept-Ranges\"))&&header===\"bytes\";var usesGzip=(header=xhr.getResponseHeader(\"Content-Encoding\"))&&header===\"gzip\";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort(\"invalid range (\"+from+\", \"+to+\") or no bytes requested!\");if(to>datalength-1)abort(\"only \"+datalength+\" bytes available! programmer error!\");var xhr=new XMLHttpRequest;xhr.open(\"GET\",url,false);if(datalength!==chunkSize)xhr.setRequestHeader(\"Range\",\"bytes=\"+from+\"-\"+to);xhr.responseType=\"arraybuffer\";if(xhr.overrideMimeType){xhr.overrideMimeType(\"text/plain; charset=x-user-defined\")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort(\"Couldn't load \"+url+\". Status: \"+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||\"\",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]==\"undefined\"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]==\"undefined\")abort(\"doXHR failed!\");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out(\"LazyFiles on gzip forces download of the whole file when length is accessed\")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort(\"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc\");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i<size;i++){buffer[offset+i]=contents[position+i]}}else{for(var i=0;i<size;i++){buffer[offset+i]=contents.get(position+i)}}return size}stream_ops.read=(stream,buffer,offset,length,position)=>{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>{if(!ptr)return\"\";var end=findStringEnd(HEAPU8,ptr,maxBytesToRead,ignoreNul);return UTF8Decoder.decode(HEAPU8.subarray(ptr,end))};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+\"/\"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;tempI64=[stats.blocks>>>0,(tempDouble=stats.blocks,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+8>>2]=tempI64[0],HEAP32[buf+12>>2]=tempI64[1];tempI64=[stats.bfree>>>0,(tempDouble=stats.bfree,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+16>>2]=tempI64[0],HEAP32[buf+20>>2]=tempI64[1];tempI64=[stats.bavail>>>0,(tempDouble=stats.bavail,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];tempI64=[stats.files>>>0,(tempDouble=stats.files,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+32>>2]=tempI64[0],HEAP32[buf+36>>2]=tempI64[1];tempI64=[stats.ffree>>>0,(tempDouble=stats.ffree,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_dup(fd){try{var old=SYSCALLS.getStreamFromFD(fd);return FS.dupStream(old).fd}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms=\"\";if(amode&4)perms+=\"r\";if(amode&2)perms+=\"w\";if(amode&1)perms+=\"x\";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;function ___syscall_ftruncate64(fd,length_low,length_high){var length=convertI32PairToI53Checked(length_low,length_high);try{if(isNaN(length))return-61;FS.ftruncate(fd,length);return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var __abort_js=()=>abort(\"\");var __embind_register_bigint=(primitiveType,name,size,minRange,maxRange)=>{};var AsciiToString=ptr=>{var str=\"\";while(1){var ch=HEAPU8[ptr++];if(!ch)return str;str+=String.fromCharCode(ch)}};var awaitingDependencies={};var registeredTypes={};var typeDependencies={};var BindingError=class BindingError extends Error{constructor(message){super(message);this.name=\"BindingError\"}};var throwBindingError=message=>{throw new BindingError(message)};function sharedRegisterType(rawType,registeredInstance,options={}){var name=registeredInstance.name;if(!rawType){throwBindingError(`type \"${name}\" must have a positive integer typeid pointer`)}if(registeredTypes.hasOwnProperty(rawType)){if(options.ignoreDuplicateRegistrations){return}else{throwBindingError(`Cannot register type '${name}' twice`)}}registeredTypes[rawType]=registeredInstance;delete typeDependencies[rawType];if(awaitingDependencies.hasOwnProperty(rawType)){var callbacks=awaitingDependencies[rawType];delete awaitingDependencies[rawType];callbacks.forEach(cb=>cb())}}function registerType(rawType,registeredInstance,options={}){return sharedRegisterType(rawType,registeredInstance,options)}var __embind_register_bool=(rawType,name,trueValue,falseValue)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:function(wt){return!!wt},toWireType:function(destructors,o){return o?trueValue:falseValue},readValueFromPointer:function(pointer){return this.fromWireType(HEAPU8[pointer])},destructorFunction:null})};var emval_freelist=[];var emval_handles=[0,1,,1,null,1,true,1,false,1];var __emval_decref=handle=>{if(handle>9&&0===--emval_handles[handle+1]){emval_handles[handle]=undefined;emval_freelist.push(handle)}};var Emval={toValue:handle=>{if(!handle){throwBindingError(`Cannot use deleted val. handle = ${handle}`)}return emval_handles[handle]},toHandle:value=>{switch(value){case undefined:return 2;case null:return 4;case true:return 6;case false:return 8;default:{const handle=emval_freelist.pop()||emval_handles.length;emval_handles[handle]=value;emval_handles[handle+1]=1;return handle}}}};function readPointer(pointer){return this.fromWireType(HEAPU32[pointer>>2])}var EmValType={name:\"emscripten::val\",fromWireType:handle=>{var rv=Emval.toValue(handle);__emval_decref(handle);return rv},toWireType:(destructors,value)=>Emval.toHandle(value),readValueFromPointer:readPointer,destructorFunction:null};var __embind_register_emval=rawType=>registerType(rawType,EmValType);var floatReadValueFromPointer=(name,width)=>{switch(width){case 4:return function(pointer){return this.fromWireType(HEAPF32[pointer>>2])};case 8:return function(pointer){return this.fromWireType(HEAPF64[pointer>>3])};default:throw new TypeError(`invalid float width (${width}): ${name}`)}};var __embind_register_float=(rawType,name,size)=>{name=AsciiToString(name);registerType(rawType,{name,fromWireType:value=>value,toWireType:(destructors,value)=>value,readValueFromPointer:floatReadValueFromPointer(name,size),destructorFunction:null})};var integerReadValueFromPointer=(name,width,signed)=>{switch(width){case 1:return signed?pointer=>HEAP8[pointer]:pointer=>HEAPU8[pointer];case 2:return signed?pointer=>HEAP16[pointer>>1]:pointer=>HEAPU16[pointer>>1];case 4:return signed?pointer=>HEAP32[pointer>>2]:pointer=>HEAPU32[pointer>>2];default:throw new TypeError(`invalid integer width (${width}): ${name}`)}};var __embind_register_integer=(primitiveType,name,size,minRange,maxRange)=>{name=AsciiToString(name);const isUnsignedType=minRange===0;let fromWireType=value=>value;if(isUnsignedType){var bitshift=32-8*size;fromWireType=value=>value<<bitshift>>>bitshift;maxRange=fromWireType(maxRange)}registerType(primitiveType,{name,fromWireType,toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(name,size,minRange!==0),destructorFunction:null})};var __embind_register_memory_view=(rawType,dataTypeIndex,name)=>{var typeMapping=[Int8Array,Uint8Array,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array];var TA=typeMapping[dataTypeIndex];function decodeMemoryView(handle){var size=HEAPU32[handle>>2];var data=HEAPU32[handle+4>>2];return new TA(HEAP8.buffer,data,size)}name=AsciiToString(name);registerType(rawType,{name,fromWireType:decodeMemoryView,readValueFromPointer:decodeMemoryView},{ignoreDuplicateRegistrations:true})};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var __embind_register_std_string=(rawType,name)=>{name=AsciiToString(name);var stdStringIsUTF8=true;registerType(rawType,{name,fromWireType(value){var length=HEAPU32[value>>2];var payload=value+4;var str;if(stdStringIsUTF8){str=UTF8ToString(payload,length,true)}else{str=\"\";for(var i=0;i<length;++i){str+=String.fromCharCode(HEAPU8[payload+i])}}_free(value);return str},toWireType(destructors,value){if(value instanceof ArrayBuffer){value=new Uint8Array(value)}var length;var valueIsOfTypeString=typeof value==\"string\";if(!(valueIsOfTypeString||ArrayBuffer.isView(value)&&value.BYTES_PER_ELEMENT==1)){throwBindingError(\"Cannot pass non-string to std::string\")}if(stdStringIsUTF8&&valueIsOfTypeString){length=lengthBytesUTF8(value)}else{length=value.length}var base=_malloc(4+length+1);var ptr=base+4;HEAPU32[base>>2]=length;if(valueIsOfTypeString){if(stdStringIsUTF8){stringToUTF8(value,ptr,length+1)}else{for(var i=0;i<length;++i){var charCode=value.charCodeAt(i);if(charCode>255){_free(base);throwBindingError(\"String has UTF-16 code units that do not fit in 8 bits\")}HEAPU8[ptr+i]=charCode}}}else{HEAPU8.set(value,ptr)}if(destructors!==null){destructors.push(_free,base)}return base},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var UTF16Decoder=new TextDecoder(\"utf-16le\");var UTF16ToString=(ptr,maxBytesToRead,ignoreNul)=>{var idx=ptr>>1;var endIdx=findStringEnd(HEAPU16,idx,maxBytesToRead/2,ignoreNul);return UTF16Decoder.decode(HEAPU16.subarray(idx,endIdx))};var stringToUTF16=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<2)return 0;maxBytesToWrite-=2;var startPtr=outPtr;var numCharsToWrite=maxBytesToWrite<str.length*2?maxBytesToWrite/2:str.length;for(var i=0;i<numCharsToWrite;++i){var codeUnit=str.charCodeAt(i);HEAP16[outPtr>>1]=codeUnit;outPtr+=2}HEAP16[outPtr>>1]=0;return outPtr-startPtr};var lengthBytesUTF16=str=>str.length*2;var UTF32ToString=(ptr,maxBytesToRead,ignoreNul)=>{var str=\"\";var startIdx=ptr>>2;for(var i=0;!(i>=maxBytesToRead/4);i++){var utf32=HEAPU32[startIdx+i];if(!utf32&&!ignoreNul)break;str+=String.fromCodePoint(utf32)}return str};var stringToUTF32=(str,outPtr,maxBytesToWrite)=>{maxBytesToWrite??=2147483647;if(maxBytesToWrite<4)return 0;var startPtr=outPtr;var endPtr=startPtr+maxBytesToWrite-4;for(var i=0;i<str.length;++i){var codePoint=str.codePointAt(i);if(codePoint>65535){i++}HEAP32[outPtr>>2]=codePoint;outPtr+=4;if(outPtr+4>endPtr)break}HEAP32[outPtr>>2]=0;return outPtr-startPtr};var lengthBytesUTF32=str=>{var len=0;for(var i=0;i<str.length;++i){var codePoint=str.codePointAt(i);if(codePoint>65535){i++}len+=4}return len};var __embind_register_std_wstring=(rawType,charSize,name)=>{name=AsciiToString(name);var decodeString,encodeString,lengthBytesUTF;if(charSize===2){decodeString=UTF16ToString;encodeString=stringToUTF16;lengthBytesUTF=lengthBytesUTF16}else{decodeString=UTF32ToString;encodeString=stringToUTF32;lengthBytesUTF=lengthBytesUTF32}registerType(rawType,{name,fromWireType:value=>{var length=HEAPU32[value>>2];var str=decodeString(value+4,length*charSize,true);_free(value);return str},toWireType:(destructors,value)=>{if(!(typeof value==\"string\")){throwBindingError(`Cannot pass non-string to C++ string type ${name}`)}var length=lengthBytesUTF(value);var ptr=_malloc(4+length+charSize);HEAPU32[ptr>>2]=length/charSize;encodeString(value,ptr+4,length+charSize);if(destructors!==null){destructors.push(_free,ptr)}return ptr},readValueFromPointer:readPointer,destructorFunction(ptr){_free(ptr)}})};var __embind_register_void=(rawType,name)=>{name=AsciiToString(name);registerType(rawType,{isVoid:true,name,fromWireType:()=>undefined,toWireType:(destructors,o)=>undefined})};var emval_methodCallers=[];var emval_addMethodCaller=caller=>{var id=emval_methodCallers.length;emval_methodCallers.push(caller);return id};var getTypeName=type=>{var ptr=___getTypeName(type);var rv=AsciiToString(ptr);_free(ptr);return rv};var requireRegisteredType=(rawType,humanName)=>{var impl=registeredTypes[rawType];if(undefined===impl){throwBindingError(`${humanName} has unknown type ${getTypeName(rawType)}`)}return impl};var emval_lookupTypes=(argCount,argTypes)=>{var a=new Array(argCount);for(var i=0;i<argCount;++i){a[i]=requireRegisteredType(HEAPU32[argTypes+i*4>>2],`parameter ${i}`)}return a};var createNamedFunction=(name,func)=>Object.defineProperty(func,\"name\",{value:name});var emval_returnValue=(toReturnWire,destructorsRef,handle)=>{var destructors=[];var result=toReturnWire(destructors,handle);if(destructors.length){HEAPU32[destructorsRef>>2]=Emval.toHandle(destructors)}return result};var emval_symbols={};var getStringOrSymbol=address=>{var symbol=emval_symbols[address];if(symbol===undefined){return AsciiToString(address)}return symbol};var __emval_create_invoker=(argCount,argTypesPtr,kind)=>{var GenericWireTypeSize=8;var[retType,...argTypes]=emval_lookupTypes(argCount,argTypesPtr);var toReturnWire=retType.toWireType.bind(retType);var argFromPtr=argTypes.map(type=>type.readValueFromPointer.bind(type));argCount--;var argN=new Array(argCount);var invokerFunction=(handle,methodName,destructorsRef,args)=>{var offset=0;for(var i=0;i<argCount;++i){argN[i]=argFromPtr[i](args+offset);offset+=GenericWireTypeSize}var rv;switch(kind){case 0:rv=Emval.toValue(handle).apply(null,argN);break;case 2:rv=Reflect.construct(Emval.toValue(handle),argN);break;case 3:rv=argN[0];break;case 1:rv=Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN);break}return emval_returnValue(toReturnWire,destructorsRef,rv)};var functionName=`methodCaller<(${argTypes.map(t=>t.name)}) => ${retType.name}>`;return emval_addMethodCaller(createNamedFunction(functionName,invokerFunction))};var __emval_get_global=name=>{if(!name){return Emval.toHandle(globalThis)}name=getStringOrSymbol(name);return Emval.toHandle(globalThis[name])};var __emval_get_property=(handle,key)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);return Emval.toHandle(handle[key])};var __emval_incref=handle=>{if(handle>9){emval_handles[handle+1]+=1}};var __emval_instanceof=(object,constructor)=>{object=Emval.toValue(object);constructor=Emval.toValue(constructor);return object instanceof constructor};var __emval_invoke=(caller,handle,methodName,destructorsRef,args)=>emval_methodCallers[caller](handle,methodName,destructorsRef,args);var __emval_new_cstring=v=>Emval.toHandle(getStringOrSymbol(v));var runDestructors=destructors=>{while(destructors.length){var ptr=destructors.pop();var del=destructors.pop();del(ptr)}};var __emval_run_destructors=handle=>{var destructors=Emval.toValue(handle);runDestructors(destructors);__emval_decref(handle)};var __emval_set_property=(handle,key,value)=>{handle=Emval.toValue(handle);key=Emval.toValue(key);value=Emval.toValue(value);handle[key]=value};var __emval_typeof=handle=>{handle=Emval.toValue(handle);return Emval.toHandle(typeof handle)};function __gmtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};function __localtime_js(time_low,time_high,tmPtr){var time=convertI32PairToI53Checked(time_low,time_high);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var setTempRet0=val=>__emscripten_tempret_set(val);var __mktime_js=function(tmPtr){var ret=(()=>{var date=new Date(HEAP32[tmPtr+20>>2]+1900,HEAP32[tmPtr+16>>2],HEAP32[tmPtr+12>>2],HEAP32[tmPtr+8>>2],HEAP32[tmPtr+4>>2],HEAP32[tmPtr>>2],0);var dst=HEAP32[tmPtr+32>>2];var guessedOffset=date.getTimezoneOffset();var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dstOffset=Math.min(winterOffset,summerOffset);if(dst<0){HEAP32[tmPtr+32>>2]=Number(summerOffset!=winterOffset&&dstOffset==guessedOffset)}else if(dst>0!=(dstOffset==guessedOffset)){var nonDstOffset=Math.max(winterOffset,summerOffset);var trueOffset=dst>0?dstOffset:nonDstOffset;date.setTime(date.getTime()+(trueOffset-guessedOffset)*6e4)}HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getYear();var timeMs=date.getTime();if(isNaN(timeMs)){return-1}return timeMs/1e3})();return setTempRet0((tempDouble=ret,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)),ret>>>0};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?\"-\":\"+\";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,\"0\");var minutes=String(absOffset%60).padStart(2,\"0\");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffset<winterOffset){stringToUTF8(winterName,std_name,17);stringToUTF8(summerName,dst_name,17)}else{stringToUTF8(winterName,dst_name,17);stringToUTF8(summerName,std_name,17)}};var _emscripten_get_now=()=>performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision_low,ignored_precision_high,ptime){var ignored_precision=convertI32PairToI53Checked(ignored_precision_low,ignored_precision_high);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);tempI64=[nsec>>>0,(tempDouble=nsec,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[ptime>>2]=tempI64[0],HEAP32[ptime+4>>2]=tempI64[1];return 0}var readEmAsmArgsArray=[];var readEmAsmArgs=(sigPtr,buf)=>{readEmAsmArgsArray.length=0;var ch;while(ch=HEAPU8[sigPtr++]){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?HEAPU32[buf>>2]:ch==105?HEAP32[buf>>2]:HEAPF64[buf>>3]);buf+=wide?8:4}return readEmAsmArgsArray};var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};var _emscripten_asm_const_int=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var _emscripten_asm_const_ptr=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);var _emscripten_errn=(str,len)=>err(UTF8ToString(str,len));var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_has_asyncify=()=>0;var _emscripten_outn=(str,len)=>out(UTF8ToString(str,len));var UNWIND_CACHE={};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var convertFrameToPC=frame=>{var match;if(match=/\\bwasm-function\\[\\d+\\]:(0x[0-9a-f]+)/.exec(frame)){return+match[1]}else if(match=/:(\\d+):\\d+(?:\\)|$)/.exec(frame)){return 2147483648|+match[1]}return 0};var saveInUnwindCache=callstack=>{for(var line of callstack){var pc=convertFrameToPC(line);if(pc){UNWIND_CACHE[pc]=line}}};var jsStackTrace=()=>(new Error).stack.toString();var _emscripten_stack_snapshot=()=>{var callstack=jsStackTrace().split(\"\\n\");if(callstack[0]==\"Error\"){callstack.shift()}saveInUnwindCache(callstack);UNWIND_CACHE.last_addr=convertFrameToPC(callstack[3]);UNWIND_CACHE.last_stack=callstack;return UNWIND_CACHE.last_addr};var _emscripten_pc_get_function=pc=>{var frame=UNWIND_CACHE[pc];if(!frame)return 0;var name;var match;if(match=/^\\s+at .*\\.wasm\\.(.*) \\(.*\\)$/.exec(frame)){name=match[1]}else if(match=/^\\s+at (.*) \\(.*\\)$/.exec(frame)){name=match[1]}else if(match=/^(.+?)@/.exec(frame)){name=match[1]}else{return 0}_free(_emscripten_pc_get_function.ret??0);_emscripten_pc_get_function.ret=stringToNewUTF8(name);return _emscripten_pc_get_function.ret};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var _emscripten_stack_unwind_buffer=(addr,buffer,count)=>{var stack;if(UNWIND_CACHE.last_addr==addr){stack=UNWIND_CACHE.last_stack}else{stack=jsStackTrace().split(\"\\n\");if(stack[0]==\"Error\"){stack.shift()}saveInUnwindCache(stack)}var offset=3;while(stack[offset]&&convertFrameToPC(stack[offset])!=addr){++offset}for(var i=0;i<count&&stack[i+offset];++i){HEAP32[buffer+i*4>>2]=convertFrameToPC(stack[i+offset])}return i};var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension(\"ANGLE_instanced_arrays\");if(ext){ctx[\"vertexAttribDivisor\"]=(index,divisor)=>ext[\"vertexAttribDivisorANGLE\"](index,divisor);ctx[\"drawArraysInstanced\"]=(mode,first,count,primcount)=>ext[\"drawArraysInstancedANGLE\"](mode,first,count,primcount);ctx[\"drawElementsInstanced\"]=(mode,count,type,indices,primcount)=>ext[\"drawElementsInstancedANGLE\"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension(\"OES_vertex_array_object\");if(ext){ctx[\"createVertexArray\"]=()=>ext[\"createVertexArrayOES\"]();ctx[\"deleteVertexArray\"]=vao=>ext[\"deleteVertexArrayOES\"](vao);ctx[\"bindVertexArray\"]=vao=>ext[\"bindVertexArrayOES\"](vao);ctx[\"isVertexArray\"]=vao=>ext[\"isVertexArrayOES\"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension(\"WEBGL_draw_buffers\");if(ext){ctx[\"drawBuffers\"]=(n,bufs)=>ext[\"drawBuffersWEBGL\"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension(\"WEBGL_draw_instanced_base_vertex_base_instance\"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension(\"WEBGL_multi_draw_instanced_base_vertex_base_instance\"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension(\"EXT_polygon_offset_clamp\"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension(\"EXT_clip_control\"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension(\"WEBGL_polygon_mode\"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension(\"WEBGL_multi_draw\"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=[\"ANGLE_instanced_arrays\",\"EXT_blend_minmax\",\"EXT_disjoint_timer_query\",\"EXT_frag_depth\",\"EXT_shader_texture_lod\",\"EXT_sRGB\",\"OES_element_index_uint\",\"OES_fbo_render_mipmap\",\"OES_standard_derivatives\",\"OES_texture_float\",\"OES_texture_half_float\",\"OES_texture_half_float_linear\",\"OES_vertex_array_object\",\"WEBGL_color_buffer_float\",\"WEBGL_depth_texture\",\"WEBGL_draw_buffers\",\"EXT_color_buffer_float\",\"EXT_conservative_depth\",\"EXT_disjoint_timer_query_webgl2\",\"EXT_texture_norm16\",\"NV_shader_noperspective_interpolation\",\"WEBGL_clip_cull_distance\",\"EXT_clip_control\",\"EXT_color_buffer_half_float\",\"EXT_depth_clamp\",\"EXT_float_blend\",\"EXT_polygon_offset_clamp\",\"EXT_texture_compression_bptc\",\"EXT_texture_compression_rgtc\",\"EXT_texture_filter_anisotropic\",\"KHR_parallel_shader_compile\",\"OES_texture_float_linear\",\"WEBGL_blend_func_extended\",\"WEBGL_compressed_texture_astc\",\"WEBGL_compressed_texture_etc\",\"WEBGL_compressed_texture_etc1\",\"WEBGL_compressed_texture_s3tc\",\"WEBGL_compressed_texture_s3tc_srgb\",\"WEBGL_debug_renderer_info\",\"WEBGL_debug_shaders\",\"WEBGL_lose_context\",\"WEBGL_multi_draw\",\"WEBGL_polygon_mode\"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var registerPreMainLoop=f=>{typeof MainLoop!=\"undefined\"&&MainLoop.preMainLoop.push(f)};var GL={counter:1,buffers:[],mappedBuffers:{},programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],byteSizeByTypeRoot:5120,byteSizeByType:[1,1,2,2,4,4,4,2,3,4,8],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i<ret;i++){table[i]=null}while(table[ret]){ret=GL.counter++}return ret},genObject:(n,buffers,createFunction,objectTable)=>{for(var i=0;i<n;i++){var buffer=GLctx[createFunction]();var id=buffer&&GL.getNewId(objectTable);if(buffer){buffer.name=id;objectTable[id]=buffer}else{GL.recordError(1282)}HEAP32[buffers+i*4>>2]=id}},MAX_TEMP_BUFFER_SIZE:2097152,numTempVertexBuffersPerSize:64,log2ceilLookup:i=>32-Math.clz32(i===0?0:i-1),generateTempBuffers:(quads,context)=>{var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);context.tempVertexBufferCounters1=[];context.tempVertexBufferCounters2=[];context.tempVertexBufferCounters1.length=context.tempVertexBufferCounters2.length=largestIndex+1;context.tempVertexBuffers1=[];context.tempVertexBuffers2=[];context.tempVertexBuffers1.length=context.tempVertexBuffers2.length=largestIndex+1;context.tempIndexBuffers=[];context.tempIndexBuffers.length=largestIndex+1;for(var i=0;i<=largestIndex;++i){context.tempIndexBuffers[i]=null;context.tempVertexBufferCounters1[i]=context.tempVertexBufferCounters2[i]=0;var ringbufferLength=GL.numTempVertexBuffersPerSize;context.tempVertexBuffers1[i]=[];context.tempVertexBuffers2[i]=[];var ringbuffer1=context.tempVertexBuffers1[i];var ringbuffer2=context.tempVertexBuffers2[i];ringbuffer1.length=ringbuffer2.length=ringbufferLength;for(var j=0;j<ringbufferLength;++j){ringbuffer1[j]=ringbuffer2[j]=null}}if(quads){context.tempQuadIndexBuffer=GLctx.createBuffer();context.GLctx.bindBuffer(34963,context.tempQuadIndexBuffer);var numIndexes=GL.MAX_TEMP_BUFFER_SIZE>>1;var quadIndexes=new Uint16Array(numIndexes);var i=0,v=0;while(1){quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+1;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v;if(i>=numIndexes)break;quadIndexes[i++]=v+2;if(i>=numIndexes)break;quadIndexes[i++]=v+3;if(i>=numIndexes)break;v+=4}context.GLctx.bufferData(34963,quadIndexes,35044);context.GLctx.bindBuffer(34963,null)}},getTempVertexBuffer:sizeBytes=>{var idx=GL.log2ceilLookup(sizeBytes);var ringbuffer=GL.currentContext.tempVertexBuffers1[idx];var nextFreeBufferIndex=GL.currentContext.tempVertexBufferCounters1[idx];GL.currentContext.tempVertexBufferCounters1[idx]=GL.currentContext.tempVertexBufferCounters1[idx]+1&GL.numTempVertexBuffersPerSize-1;var vbo=ringbuffer[nextFreeBufferIndex];if(vbo){return vbo}var prevVBO=GLctx.getParameter(34964);ringbuffer[nextFreeBufferIndex]=GLctx.createBuffer();GLctx.bindBuffer(34962,ringbuffer[nextFreeBufferIndex]);GLctx.bufferData(34962,1<<idx,35048);GLctx.bindBuffer(34962,prevVBO);return ringbuffer[nextFreeBufferIndex]},getTempIndexBuffer:sizeBytes=>{var idx=GL.log2ceilLookup(sizeBytes);var ibo=GL.currentContext.tempIndexBuffers[idx];if(ibo){return ibo}var prevIBO=GLctx.getParameter(34965);GL.currentContext.tempIndexBuffers[idx]=GLctx.createBuffer();GLctx.bindBuffer(34963,GL.currentContext.tempIndexBuffers[idx]);GLctx.bufferData(34963,1<<idx,35048);GLctx.bindBuffer(34963,prevIBO);return GL.currentContext.tempIndexBuffers[idx]},newRenderingFrameStarted:()=>{if(!GL.currentContext){return}var vb=GL.currentContext.tempVertexBuffers1;GL.currentContext.tempVertexBuffers1=GL.currentContext.tempVertexBuffers2;GL.currentContext.tempVertexBuffers2=vb;vb=GL.currentContext.tempVertexBufferCounters1;GL.currentContext.tempVertexBufferCounters1=GL.currentContext.tempVertexBufferCounters2;GL.currentContext.tempVertexBufferCounters2=vb;var largestIndex=GL.log2ceilLookup(GL.MAX_TEMP_BUFFER_SIZE);for(var i=0;i<=largestIndex;++i){GL.currentContext.tempVertexBufferCounters1[i]=0}},getSource:(shader,count,string,length)=>{var source=\"\";for(var i=0;i<count;++i){var len=length?HEAPU32[length+i*4>>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},calcBufLength:(size,type,stride,count)=>{if(stride>0){return count*stride}var typeSize=GL.byteSizeByType[type-GL.byteSizeByTypeRoot];return size*typeSize*count},usedTempBuffers:[],preDrawHandleClientVertexAttribBindings:count=>{GL.resetBufferBinding=false;for(var i=0;i<GL.currentContext.maxVertexAttribs;++i){var cb=GL.currentContext.clientBuffers[i];if(!cb.clientside||!cb.enabled)continue;GL.resetBufferBinding=true;var size=GL.calcBufLength(cb.size,cb.type,cb.stride,count);var buf=GL.getTempVertexBuffer(size);GLctx.bindBuffer(34962,buf);GLctx.bufferSubData(34962,0,HEAPU8.subarray(cb.ptr,cb.ptr+size));cb.vertexAttribPointerAdaptor.call(GLctx,i,cb.size,cb.type,cb.normalized,cb.stride,0)}},postDrawHandleClientVertexAttribBindings:()=>{if(GL.resetBufferBinding){GLctx.bindBuffer(34962,GL.buffers[GLctx.currentArrayBufferBinding])}},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver==\"webgl\"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext(\"webgl2\",webGLContextAttributes):canvas.getContext(\"webgl\",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault==\"undefined\"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}context.maxVertexAttribs=context.GLctx.getParameter(34921);context.clientBuffers=[];for(var i=0;i<context.maxVertexAttribs;i++){context.clientBuffers[i]={enabled:false,clientside:false,size:0,type:0,normalized:0,stride:0,ptr:0,vertexAttribPointerAdaptor:null}}GL.generateTempBuffers(false,context);return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module[\"ctx\"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents==\"object\"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension(\"EXT_disjoint_timer_query_webgl2\")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension(\"EXT_disjoint_timer_query\")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes(\"lose_context\")&&!ext.includes(\"debug\")){GLctx.getExtension(ext)}})}};var webglPowerPreferences=[\"default\",\"low-power\",\"high-performance\"];var specialHTMLTargets=[0,typeof document!=\"undefined\"?document:0,typeof window!=\"undefined\"?window:0];var findEventTarget=target=>{if(!target)return window;if(typeof target==\"number\")target=specialHTMLTargets[target]||UTF8ToString(target);if(target===\"#window\")return window;else if(target===\"#document\")return document;else if(target===\"#screen\")return screen;else if(target===\"#canvas\")return Module[\"canvas\"];else if(typeof target==\"string\")return typeof document!=\"undefined\"?document.getElementById(target):null;return target};var findCanvasEventTarget=target=>{if(typeof target==\"number\")target=UTF8ToString(target);if(!target||target===\"#canvas\"){if(typeof GL!=\"undefined\"&&GL.offscreenCanvases[\"canvas\"])return GL.offscreenCanvases[\"canvas\"];return Module[\"canvas\"]}if(typeof GL!=\"undefined\"&&GL.offscreenCanvases[target])return GL.offscreenCanvases[target];return findEventTarget(target)};var _emscripten_webgl_do_create_context=(target,attributes)=>{var attr32=attributes>>2;var powerPreference=HEAP32[attr32+(8>>2)];var contextAttributes={alpha:!!HEAP8[attributes+0],depth:!!HEAP8[attributes+1],stencil:!!HEAP8[attributes+2],antialias:!!HEAP8[attributes+3],premultipliedAlpha:!!HEAP8[attributes+4],preserveDrawingBuffer:!!HEAP8[attributes+5],powerPreference:webglPowerPreferences[powerPreference],failIfMajorPerformanceCaveat:!!HEAP8[attributes+12],majorVersion:HEAP32[attr32+(16>>2)],minorVersion:HEAP32[attr32+(20>>2)],enableExtensionsByDefault:HEAP8[attributes+24],explicitSwapControl:HEAP8[attributes+25],proxyContextToMainThread:HEAP32[attr32+(28>>2)],renderViaOffscreenBackBuffer:HEAP8[attributes+32]};var canvas=findCanvasEventTarget(target);if(!canvas){return 0}if(contextAttributes.explicitSwapControl){return 0}var contextHandle=GL.createContext(canvas,contextAttributes);return contextHandle};var _emscripten_webgl_create_context=_emscripten_webgl_do_create_context;var _emscripten_webgl_destroy_context=contextHandle=>{if(GL.currentContext==contextHandle)GL.currentContext=0;GL.deleteContext(contextHandle)};var _emscripten_webgl_get_context_attributes=(c,a)=>{if(!a)return-5;c=GL.contexts[c];if(!c)return-3;var t=c.GLctx?.getContextAttributes();if(!t)return-3;HEAP8[a]=t.alpha;HEAP8[a+1]=t.depth;HEAP8[a+2]=t.stencil;HEAP8[a+3]=t.antialias;HEAP8[a+4]=t.premultipliedAlpha;HEAP8[a+5]=t.preserveDrawingBuffer;var power=t[\"powerPreference\"]&&webglPowerPreferences.indexOf(t[\"powerPreference\"]);HEAP32[a+8>>2]=power;HEAP8[a+12]=t.failIfMajorPerformanceCaveat;HEAP32[a+16>>2]=c.version;HEAP32[a+20>>2]=0;HEAP8[a+24]=c.attributes.enableExtensionsByDefault;return 0};var _emscripten_webgl_do_get_current_context=()=>GL.currentContext?GL.currentContext.handle:0;var _emscripten_webgl_get_current_context=_emscripten_webgl_do_get_current_context;var _emscripten_webgl_make_context_current=contextHandle=>{var success=GL.makeContextCurrent(contextHandle);return success?0:-5};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var readI53FromI64=ptr=>HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*4294967296;var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var WebGPU={Internals:{jsObjects:[],jsObjectInsert:(ptr,jsObject)=>{ptr>>>=0;WebGPU.Internals.jsObjects[ptr]=jsObject},bufferOnUnmaps:[],futures:[],futureInsert:(futureId,promise)=>{}},getJsObject:ptr=>{if(!ptr)return undefined;ptr>>>=0;return WebGPU.Internals.jsObjects[ptr]},importJsAdapter:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateAdapter(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroup:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroup(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBindGroupLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateBindGroupLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsBuffer:(buffer,parentPtr=0)=>{assert(buffer.mapState!=\"pending\");var mapState=buffer.mapState==\"mapped\"?3:1;var bufferPtr=_emwgpuCreateBuffer(parentPtr,mapState);WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(buffer.mapState==\"mapped\"){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return bufferPtr},importJsCommandBuffer:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandBuffer(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsCommandEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateCommandEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsComputePipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateComputePipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsDevice:(device,parentPtr=0)=>{var queuePtr=_emwgpuCreateQueue(parentPtr);var devicePtr=_emwgpuCreateDevice(parentPtr,queuePtr);WebGPU.Internals.jsObjectInsert(queuePtr,device.queue);WebGPU.Internals.jsObjectInsert(devicePtr,device);return devicePtr},importJsExternalTexture:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateExternalTexture(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsPipelineLayout:(obj,parentPtr=0)=>{var ptr=_emwgpuCreatePipelineLayout(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQuerySet:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQuerySet(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsQueue:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateQueue(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundle:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundle(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderBundleEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderBundleEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPassEncoder:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPassEncoder(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsRenderPipeline:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateRenderPipeline(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSampler:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSampler(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsShaderModule:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateShaderModule(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsSurface:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateSurface(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTexture:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTexture(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},importJsTextureView:(obj,parentPtr=0)=>{var ptr=_emwgpuCreateTextureView(parentPtr);WebGPU.Internals.jsObjects[ptr]=obj;return ptr},errorCallback:(callback,type,message,userdata)=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(message);getWasmTableEntry(callback)(type,messagePtr,userdata);stackRestore(sp)},iterateExtensions:(root,handlers)=>{for(var ptr=HEAPU32[root>>2];ptr;ptr=HEAPU32[ptr>>2]){var sType=HEAP32[ptr+4>>2];var handler=handlers[sType](ptr)}},setStringView:(ptr,data,length)=>{HEAPU32[ptr>>2]=data;HEAPU32[ptr+4>>2]=length},makeStringFromStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>2];var length=HEAPU32[stringViewPtr+4>>2];return UTF8ToString(ptr,length)},makeStringFromOptionalStringView:stringViewPtr=>{var ptr=HEAPU32[stringViewPtr>>2];var length=HEAPU32[stringViewPtr+4>>2];if(!ptr){if(length===0){return\"\"}return undefined}return UTF8ToString(ptr,length)},makeColor:ptr=>({r:HEAPF64[ptr>>3],g:HEAPF64[ptr+8>>3],b:HEAPF64[ptr+16>>3],a:HEAPF64[ptr+24>>3]}),makeExtent3D:ptr=>({width:HEAPU32[ptr>>2],height:HEAPU32[ptr+4>>2],depthOrArrayLayers:HEAPU32[ptr+8>>2]}),makeOrigin3D:ptr=>({x:HEAPU32[ptr>>2],y:HEAPU32[ptr+4>>2],z:HEAPU32[ptr+8>>2]}),makeTexelCopyTextureInfo:ptr=>({texture:WebGPU.getJsObject(HEAPU32[ptr>>2]),mipLevel:HEAPU32[ptr+4>>2],origin:WebGPU.makeOrigin3D(ptr+8),aspect:WebGPU.TextureAspect[HEAP32[ptr+20>>2]]}),makeTexelCopyBufferLayout:ptr=>{var bytesPerRow=HEAPU32[ptr+8>>2];var rowsPerImage=HEAPU32[ptr+12>>2];return{offset:readI53FromI64(ptr),bytesPerRow:bytesPerRow===4294967295?undefined:bytesPerRow,rowsPerImage:rowsPerImage===4294967295?undefined:rowsPerImage}},makeTexelCopyBufferInfo:ptr=>{var layoutPtr=ptr+0;var bufferCopyView=WebGPU.makeTexelCopyBufferLayout(layoutPtr);bufferCopyView[\"buffer\"]=WebGPU.getJsObject(HEAPU32[ptr+16>>2]);return bufferCopyView},makePassTimestampWrites:ptr=>{if(ptr===0)return undefined;return{querySet:WebGPU.getJsObject(HEAPU32[ptr+4>>2]),beginningOfPassWriteIndex:HEAPU32[ptr+8>>2],endOfPassWriteIndex:HEAPU32[ptr+12>>2]}},makePipelineConstants:(constantCount,constantsPtr)=>{if(!constantCount)return;var constants={};for(var i=0;i<constantCount;++i){var entryPtr=constantsPtr+24*i;var key=WebGPU.makeStringFromStringView(entryPtr+4);constants[key]=HEAPF64[entryPtr+16>>3]}return constants},makePipelineLayout:layoutPtr=>{if(!layoutPtr)return\"auto\";return WebGPU.getJsObject(layoutPtr)},makeComputeState:ptr=>{if(!ptr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[ptr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[ptr+16>>2],HEAPU32[ptr+20>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(ptr+8)};return desc},makeComputePipelineDesc:descriptor=>{var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>2]),compute:WebGPU.makeComputeState(descriptor+16)};return desc},makeRenderPipelineDesc:descriptor=>{function makePrimitiveState(psPtr){if(!psPtr)return undefined;return{topology:WebGPU.PrimitiveTopology[HEAP32[psPtr+4>>2]],stripIndexFormat:WebGPU.IndexFormat[HEAP32[psPtr+8>>2]],frontFace:WebGPU.FrontFace[HEAP32[psPtr+12>>2]],cullMode:WebGPU.CullMode[HEAP32[psPtr+16>>2]],unclippedDepth:!!HEAPU32[psPtr+20>>2]}}function makeBlendComponent(bdPtr){if(!bdPtr)return undefined;return{operation:WebGPU.BlendOperation[HEAP32[bdPtr>>2]],srcFactor:WebGPU.BlendFactor[HEAP32[bdPtr+4>>2]],dstFactor:WebGPU.BlendFactor[HEAP32[bdPtr+8>>2]]}}function makeBlendState(bsPtr){if(!bsPtr)return undefined;return{alpha:makeBlendComponent(bsPtr+12),color:makeBlendComponent(bsPtr+0)}}function makeColorState(csPtr){var format=WebGPU.TextureFormat[HEAP32[csPtr+4>>2]];return format?{format,blend:makeBlendState(HEAPU32[csPtr+8>>2]),writeMask:HEAPU32[csPtr+16>>2]}:undefined}function makeColorStates(count,csArrayPtr){var states=[];for(var i=0;i<count;++i){states.push(makeColorState(csArrayPtr+24*i))}return states}function makeStencilStateFace(ssfPtr){return{compare:WebGPU.CompareFunction[HEAP32[ssfPtr>>2]],failOp:WebGPU.StencilOperation[HEAP32[ssfPtr+4>>2]],depthFailOp:WebGPU.StencilOperation[HEAP32[ssfPtr+8>>2]],passOp:WebGPU.StencilOperation[HEAP32[ssfPtr+12>>2]]}}function makeDepthStencilState(dssPtr){if(!dssPtr)return undefined;return{format:WebGPU.TextureFormat[HEAP32[dssPtr+4>>2]],depthWriteEnabled:!!HEAPU32[dssPtr+8>>2],depthCompare:WebGPU.CompareFunction[HEAP32[dssPtr+12>>2]],stencilFront:makeStencilStateFace(dssPtr+16),stencilBack:makeStencilStateFace(dssPtr+32),stencilReadMask:HEAPU32[dssPtr+48>>2],stencilWriteMask:HEAPU32[dssPtr+52>>2],depthBias:HEAP32[dssPtr+56>>2],depthBiasSlopeScale:HEAPF32[dssPtr+60>>2],depthBiasClamp:HEAPF32[dssPtr+64>>2]}}function makeVertexAttribute(vaPtr){return{format:WebGPU.VertexFormat[HEAP32[vaPtr+4>>2]],offset:readI53FromI64(vaPtr+8),shaderLocation:HEAPU32[vaPtr+16>>2]}}function makeVertexAttributes(count,vaArrayPtr){var vas=[];for(var i=0;i<count;++i){vas.push(makeVertexAttribute(vaArrayPtr+i*24))}return vas}function makeVertexBuffer(vbPtr){if(!vbPtr)return undefined;var stepMode=WebGPU.VertexStepMode[HEAP32[vbPtr+4>>2]];var attributeCount=HEAPU32[vbPtr+16>>2];if(!stepMode&&!attributeCount){return null}return{arrayStride:readI53FromI64(vbPtr+8),stepMode,attributes:makeVertexAttributes(attributeCount,HEAPU32[vbPtr+20>>2])}}function makeVertexBuffers(count,vbArrayPtr){if(!count)return undefined;var vbs=[];for(var i=0;i<count;++i){vbs.push(makeVertexBuffer(vbArrayPtr+i*24))}return vbs}function makeVertexState(viPtr){if(!viPtr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[viPtr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[viPtr+16>>2],HEAPU32[viPtr+20>>2]),buffers:makeVertexBuffers(HEAPU32[viPtr+24>>2],HEAPU32[viPtr+28>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(viPtr+8)};return desc}function makeMultisampleState(msPtr){if(!msPtr)return undefined;return{count:HEAPU32[msPtr+4>>2],mask:HEAPU32[msPtr+8>>2],alphaToCoverageEnabled:!!HEAPU32[msPtr+12>>2]}}function makeFragmentState(fsPtr){if(!fsPtr)return undefined;var desc={module:WebGPU.getJsObject(HEAPU32[fsPtr+4>>2]),constants:WebGPU.makePipelineConstants(HEAPU32[fsPtr+16>>2],HEAPU32[fsPtr+20>>2]),targets:makeColorStates(HEAPU32[fsPtr+24>>2],HEAPU32[fsPtr+28>>2]),entryPoint:WebGPU.makeStringFromOptionalStringView(fsPtr+8)};return desc}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.makePipelineLayout(HEAPU32[descriptor+12>>2]),vertex:makeVertexState(descriptor+16),primitive:makePrimitiveState(descriptor+48),depthStencil:makeDepthStencilState(HEAPU32[descriptor+72>>2]),multisample:makeMultisampleState(descriptor+76),fragment:makeFragmentState(HEAPU32[descriptor+92>>2])};return desc},fillLimitStruct:(limits,limitsOutPtr)=>{function setLimitValueU32(name,limitOffset){var limitValue=limits[name];HEAPU32[limitsOutPtr+limitOffset>>2]=limitValue}function setLimitValueU64(name,limitOffset){var limitValue=limits[name];writeI53ToI64(limitsOutPtr+limitOffset,limitValue)}setLimitValueU32(\"maxTextureDimension1D\",4);setLimitValueU32(\"maxTextureDimension2D\",8);setLimitValueU32(\"maxTextureDimension3D\",12);setLimitValueU32(\"maxTextureArrayLayers\",16);setLimitValueU32(\"maxBindGroups\",20);setLimitValueU32(\"maxBindGroupsPlusVertexBuffers\",24);setLimitValueU32(\"maxBindingsPerBindGroup\",28);setLimitValueU32(\"maxDynamicUniformBuffersPerPipelineLayout\",32);setLimitValueU32(\"maxDynamicStorageBuffersPerPipelineLayout\",36);setLimitValueU32(\"maxSampledTexturesPerShaderStage\",40);setLimitValueU32(\"maxSamplersPerShaderStage\",44);setLimitValueU32(\"maxStorageBuffersPerShaderStage\",48);setLimitValueU32(\"maxStorageTexturesPerShaderStage\",52);setLimitValueU32(\"maxUniformBuffersPerShaderStage\",56);setLimitValueU32(\"minUniformBufferOffsetAlignment\",80);setLimitValueU32(\"minStorageBufferOffsetAlignment\",84);setLimitValueU64(\"maxUniformBufferBindingSize\",64);setLimitValueU64(\"maxStorageBufferBindingSize\",72);setLimitValueU32(\"maxVertexBuffers\",88);setLimitValueU64(\"maxBufferSize\",96);setLimitValueU32(\"maxVertexAttributes\",104);setLimitValueU32(\"maxVertexBufferArrayStride\",108);setLimitValueU32(\"maxInterStageShaderVariables\",112);setLimitValueU32(\"maxColorAttachments\",116);setLimitValueU32(\"maxColorAttachmentBytesPerSample\",120);setLimitValueU32(\"maxComputeWorkgroupStorageSize\",124);setLimitValueU32(\"maxComputeInvocationsPerWorkgroup\",128);setLimitValueU32(\"maxComputeWorkgroupSizeX\",132);setLimitValueU32(\"maxComputeWorkgroupSizeY\",136);setLimitValueU32(\"maxComputeWorkgroupSizeZ\",140);setLimitValueU32(\"maxComputeWorkgroupsPerDimension\",144);if(limits.maxImmediateSize!==undefined){setLimitValueU32(\"maxImmediateSize\",148)}},fillAdapterInfoStruct:(info,infoStruct)=>{HEAPU32[infoStruct+52>>2]=info.subgroupMinSize;HEAPU32[infoStruct+56>>2]=info.subgroupMaxSize;var strs=info.vendor+info.architecture+info.device+info.description;var strPtr=stringToNewUTF8(strs);var vendorLen=lengthBytesUTF8(info.vendor);WebGPU.setStringView(infoStruct+4,strPtr,vendorLen);strPtr+=vendorLen;var architectureLen=lengthBytesUTF8(info.architecture);WebGPU.setStringView(infoStruct+12,strPtr,architectureLen);strPtr+=architectureLen;var deviceLen=lengthBytesUTF8(info.device);WebGPU.setStringView(infoStruct+20,strPtr,deviceLen);strPtr+=deviceLen;var descriptionLen=lengthBytesUTF8(info.description);WebGPU.setStringView(infoStruct+28,strPtr,descriptionLen);strPtr+=descriptionLen;HEAP32[infoStruct+36>>2]=2;var adapterType=info.isFallbackAdapter?3:4;HEAP32[infoStruct+40>>2]=adapterType;HEAPU32[infoStruct+44>>2]=0;HEAPU32[infoStruct+48>>2]=0},AddressMode:[,\"clamp-to-edge\",\"repeat\",\"mirror-repeat\"],BlendFactor:[,\"zero\",\"one\",\"src\",\"one-minus-src\",\"src-alpha\",\"one-minus-src-alpha\",\"dst\",\"one-minus-dst\",\"dst-alpha\",\"one-minus-dst-alpha\",\"src-alpha-saturated\",\"constant\",\"one-minus-constant\",\"src1\",\"one-minus-src1\",\"src1alpha\",\"one-minus-src1alpha\"],BlendOperation:[,\"add\",\"subtract\",\"reverse-subtract\",\"min\",\"max\"],BufferBindingType:[,,\"uniform\",\"storage\",\"read-only-storage\"],BufferMapState:[,\"unmapped\",\"pending\",\"mapped\"],CompareFunction:[,\"never\",\"less\",\"equal\",\"less-equal\",\"greater\",\"not-equal\",\"greater-equal\",\"always\"],CompilationInfoRequestStatus:[,\"success\",\"callback-cancelled\"],ComponentSwizzle:[,\"0\",\"1\",\"r\",\"g\",\"b\",\"a\"],CompositeAlphaMode:[,\"opaque\",\"premultiplied\",\"unpremultiplied\",\"inherit\"],CullMode:[,\"none\",\"front\",\"back\"],ErrorFilter:[,\"validation\",\"out-of-memory\",\"internal\"],FeatureLevel:[,\"compatibility\",\"core\"],FeatureName:{1:\"core-features-and-limits\",2:\"depth-clip-control\",3:\"depth32float-stencil8\",4:\"texture-compression-bc\",5:\"texture-compression-bc-sliced-3d\",6:\"texture-compression-etc2\",7:\"texture-compression-astc\",8:\"texture-compression-astc-sliced-3d\",9:\"timestamp-query\",10:\"indirect-first-instance\",11:\"shader-f16\",12:\"rg11b10ufloat-renderable\",13:\"bgra8unorm-storage\",14:\"float32-filterable\",15:\"float32-blendable\",16:\"clip-distances\",17:\"dual-source-blending\",18:\"subgroups\",19:\"texture-formats-tier1\",20:\"texture-formats-tier2\",21:\"primitive-index\",22:\"texture-component-swizzle\",327692:\"chromium-experimental-unorm16-texture-formats\",327729:\"chromium-experimental-multi-draw-indirect\"},FilterMode:[,\"nearest\",\"linear\"],FrontFace:[,\"ccw\",\"cw\"],IndexFormat:[,\"uint16\",\"uint32\"],InstanceFeatureName:[,\"timed-wait-any\",\"shader-source-spirv\",\"multiple-devices-per-adapter\"],LoadOp:[,\"load\",\"clear\"],MipmapFilterMode:[,\"nearest\",\"linear\"],OptionalBool:[\"false\",\"true\"],PowerPreference:[,\"low-power\",\"high-performance\"],PredefinedColorSpace:[,\"srgb\",\"display-p3\"],PrimitiveTopology:[,\"point-list\",\"line-list\",\"line-strip\",\"triangle-list\",\"triangle-strip\"],QueryType:[,\"occlusion\",\"timestamp\"],SamplerBindingType:[,,\"filtering\",\"non-filtering\",\"comparison\"],Status:[,\"success\",\"error\"],StencilOperation:[,\"keep\",\"zero\",\"replace\",\"invert\",\"increment-clamp\",\"decrement-clamp\",\"increment-wrap\",\"decrement-wrap\"],StorageTextureAccess:[,,\"write-only\",\"read-only\",\"read-write\"],StoreOp:[,\"store\",\"discard\"],SurfaceGetCurrentTextureStatus:[,\"success-optimal\",\"success-suboptimal\",\"timeout\",\"outdated\",\"lost\",\"error\"],TextureAspect:[,\"all\",\"stencil-only\",\"depth-only\"],TextureDimension:[,\"1d\",\"2d\",\"3d\"],TextureFormat:[,\"r8unorm\",\"r8snorm\",\"r8uint\",\"r8sint\",\"r16unorm\",\"r16snorm\",\"r16uint\",\"r16sint\",\"r16float\",\"rg8unorm\",\"rg8snorm\",\"rg8uint\",\"rg8sint\",\"r32float\",\"r32uint\",\"r32sint\",\"rg16unorm\",\"rg16snorm\",\"rg16uint\",\"rg16sint\",\"rg16float\",\"rgba8unorm\",\"rgba8unorm-srgb\",\"rgba8snorm\",\"rgba8uint\",\"rgba8sint\",\"bgra8unorm\",\"bgra8unorm-srgb\",\"rgb10a2uint\",\"rgb10a2unorm\",\"rg11b10ufloat\",\"rgb9e5ufloat\",\"rg32float\",\"rg32uint\",\"rg32sint\",\"rgba16unorm\",\"rgba16snorm\",\"rgba16uint\",\"rgba16sint\",\"rgba16float\",\"rgba32float\",\"rgba32uint\",\"rgba32sint\",\"stencil8\",\"depth16unorm\",\"depth24plus\",\"depth24plus-stencil8\",\"depth32float\",\"depth32float-stencil8\",\"bc1-rgba-unorm\",\"bc1-rgba-unorm-srgb\",\"bc2-rgba-unorm\",\"bc2-rgba-unorm-srgb\",\"bc3-rgba-unorm\",\"bc3-rgba-unorm-srgb\",\"bc4-r-unorm\",\"bc4-r-snorm\",\"bc5-rg-unorm\",\"bc5-rg-snorm\",\"bc6h-rgb-ufloat\",\"bc6h-rgb-float\",\"bc7-rgba-unorm\",\"bc7-rgba-unorm-srgb\",\"etc2-rgb8unorm\",\"etc2-rgb8unorm-srgb\",\"etc2-rgb8a1unorm\",\"etc2-rgb8a1unorm-srgb\",\"etc2-rgba8unorm\",\"etc2-rgba8unorm-srgb\",\"eac-r11unorm\",\"eac-r11snorm\",\"eac-rg11unorm\",\"eac-rg11snorm\",\"astc-4x4-unorm\",\"astc-4x4-unorm-srgb\",\"astc-5x4-unorm\",\"astc-5x4-unorm-srgb\",\"astc-5x5-unorm\",\"astc-5x5-unorm-srgb\",\"astc-6x5-unorm\",\"astc-6x5-unorm-srgb\",\"astc-6x6-unorm\",\"astc-6x6-unorm-srgb\",\"astc-8x5-unorm\",\"astc-8x5-unorm-srgb\",\"astc-8x6-unorm\",\"astc-8x6-unorm-srgb\",\"astc-8x8-unorm\",\"astc-8x8-unorm-srgb\",\"astc-10x5-unorm\",\"astc-10x5-unorm-srgb\",\"astc-10x6-unorm\",\"astc-10x6-unorm-srgb\",\"astc-10x8-unorm\",\"astc-10x8-unorm-srgb\",\"astc-10x10-unorm\",\"astc-10x10-unorm-srgb\",\"astc-12x10-unorm\",\"astc-12x10-unorm-srgb\",\"astc-12x12-unorm\",\"astc-12x12-unorm-srgb\"],TextureSampleType:[,,\"float\",\"unfilterable-float\",\"depth\",\"sint\",\"uint\"],TextureViewDimension:[,\"1d\",\"2d\",\"2d-array\",\"cube\",\"cube-array\",\"3d\"],ToneMappingMode:[,\"standard\",\"extended\"],VertexFormat:[,\"uint8\",\"uint8x2\",\"uint8x4\",\"sint8\",\"sint8x2\",\"sint8x4\",\"unorm8\",\"unorm8x2\",\"unorm8x4\",\"snorm8\",\"snorm8x2\",\"snorm8x4\",\"uint16\",\"uint16x2\",\"uint16x4\",\"sint16\",\"sint16x2\",\"sint16x4\",\"unorm16\",\"unorm16x2\",\"unorm16x4\",\"snorm16\",\"snorm16x2\",\"snorm16x4\",\"float16\",\"float16x2\",\"float16x4\",\"float32\",\"float32x2\",\"float32x3\",\"float32x4\",\"uint32\",\"uint32x2\",\"uint32x3\",\"uint32x4\",\"sint32\",\"sint32x2\",\"sint32x3\",\"sint32x4\",\"unorm10-10-10-2\",\"unorm8x4-bgra\"],VertexStepMode:[,\"vertex\",\"instance\"],WGSLLanguageFeatureName:[,\"readonly_and_readwrite_storage_textures\",\"packed_4x8_integer_dot_product\",\"unrestricted_pointer_parameters\",\"pointer_composite_access\",\"uniform_buffer_standard_layout\",\"subgroup_id\"]};var _emscripten_webgpu_get_device=()=>{if(WebGPU.preinitializedDeviceId===undefined){WebGPU.preinitializedDeviceId=WebGPU.importJsDevice(Module[\"preinitializedWebGPUDevice\"]);_wgpuDeviceAddRef(WebGPU.preinitializedDeviceId)}_wgpuDeviceAddRef(WebGPU.preinitializedDeviceId);return WebGPU.preinitializedDeviceId};var _emwgpuBufferDestroy=bufferPtr=>{var buffer=WebGPU.getJsObject(bufferPtr);var onUnmap=WebGPU.Internals.bufferOnUnmaps[bufferPtr];if(onUnmap){for(var i=0;i<onUnmap.length;++i){onUnmap[i]()}delete WebGPU.Internals.bufferOnUnmaps[bufferPtr]}buffer.destroy()};var _emwgpuBufferGetMappedRange=(bufferPtr,offset,size)=>{var buffer=WebGPU.getJsObject(bufferPtr);if(size==-1)size=undefined;var mapped;try{mapped=buffer.getMappedRange(offset,size)}catch(ex){return 0}var data=_memalign(16,mapped.byteLength);HEAPU8.fill(0,data,mapped.byteLength);WebGPU.Internals.bufferOnUnmaps[bufferPtr].push(()=>{new Uint8Array(mapped).set(HEAPU8.subarray(data,data+mapped.byteLength));_free(data)});return data};var _emwgpuBufferUnmap=bufferPtr=>{var buffer=WebGPU.getJsObject(bufferPtr);var onUnmap=WebGPU.Internals.bufferOnUnmaps[bufferPtr];if(!onUnmap){return}for(var i=0;i<onUnmap.length;++i){onUnmap[i]()}delete WebGPU.Internals.bufferOnUnmaps[bufferPtr];buffer.unmap()};var _emwgpuDelete=ptr=>{delete WebGPU.Internals.jsObjects[ptr]};var _emwgpuDeviceCreateBuffer=(devicePtr,descriptor,bufferPtr)=>{var mappedAtCreation=!!HEAPU32[descriptor+32>>2];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),usage:HEAPU32[descriptor+16>>2],size:readI53FromI64(descriptor+24),mappedAtCreation};var device=WebGPU.getJsObject(devicePtr);var buffer;try{buffer=device.createBuffer(desc)}catch(ex){return false}WebGPU.Internals.jsObjectInsert(bufferPtr,buffer);if(mappedAtCreation){WebGPU.Internals.bufferOnUnmaps[bufferPtr]=[]}return true};var _emwgpuDeviceCreateComputePipelineAsync=function(devicePtr,futureId_low,futureId_high,descriptor,pipelinePtr){var futureId=convertI32PairToI53Checked(futureId_low,futureId_high);var desc=WebGPU.makeComputePipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.futureInsert(futureId,device.createComputePipelineAsync(desc).then(pipeline=>{callUserCallback(()=>{WebGPU.Internals.jsObjectInsert(pipelinePtr,pipeline);_emwgpuOnCreateComputePipelineCompleted(futureId,1,pipelinePtr,0)})},pipelineError=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(pipelineError.message);var status=pipelineError.reason===\"validation\"?3:pipelineError.reason===\"internal\"?4:0;_emwgpuOnCreateComputePipelineCompleted(futureId,status,pipelinePtr,messagePtr);stackRestore(sp)})}))};var _emwgpuDeviceCreateRenderPipelineAsync=function(devicePtr,futureId_low,futureId_high,descriptor,pipelinePtr){var futureId=convertI32PairToI53Checked(futureId_low,futureId_high);var desc=WebGPU.makeRenderPipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.futureInsert(futureId,device.createRenderPipelineAsync(desc).then(pipeline=>{callUserCallback(()=>{WebGPU.Internals.jsObjectInsert(pipelinePtr,pipeline);_emwgpuOnCreateRenderPipelineCompleted(futureId,1,pipelinePtr,0)})},pipelineError=>{callUserCallback(()=>{var sp=stackSave();var messagePtr=stringToUTF8OnStack(pipelineError.message);var status=pipelineError.reason===\"validation\"?3:pipelineError.reason===\"internal\"?4:0;_emwgpuOnCreateRenderPipelineCompleted(futureId,status,pipelinePtr,messagePtr);stackRestore(sp)})}))};var _emwgpuDeviceCreateShaderModule=(devicePtr,descriptor,shaderModulePtr)=>{var nextInChainPtr=HEAPU32[descriptor>>2];var sType=HEAP32[nextInChainPtr+4>>2];var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),code:\"\"};switch(sType){case 2:{desc[\"code\"]=WebGPU.makeStringFromStringView(nextInChainPtr+8);break}}var device=WebGPU.getJsObject(devicePtr);WebGPU.Internals.jsObjectInsert(shaderModulePtr,device.createShaderModule(desc))};var _emwgpuDeviceDestroy=devicePtr=>{const device=WebGPU.getJsObject(devicePtr);device.onuncapturederror=null;device.destroy()};var _emwgpuWaitAny=(futurePtr,futureCount,timeoutMSPtr)=>{abort(\"TODO: Implement asyncify-free WaitAny for timeout=0\")};var ENV={};var getExecutableName=()=>thisProgram||\"./this.program\";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator==\"object\"&&navigator.language||\"C\").replace(\"-\",\"_\")+\".UTF-8\";var env={USER:\"web_user\",LOGNAME:\"web_user\",PATH:\"/\",PWD:\"/\",HOME:\"/home/web_user\",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr<len)break;if(typeof offset!=\"undefined\"){offset+=curr}}return ret};function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr<len){break}if(typeof offset!=\"undefined\"){offset+=curr}}return ret};function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _glActiveTexture=_emscripten_glActiveTexture;var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _glAttachShader=_emscripten_glAttachShader;var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _glBindAttribLocation=_emscripten_glBindAttribLocation;var _emscripten_glBindBuffer=(target,buffer)=>{if(buffer&&!GL.buffers[buffer]){var b=GLctx.createBuffer();b.name=buffer;GL.buffers[buffer]=b}if(target==34962){GLctx.currentArrayBufferBinding=buffer}else if(target==34963){GLctx.currentElementArrayBufferBinding=buffer}if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _glBindBuffer=_emscripten_glBindBuffer;var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _glBindBufferBase=_emscripten_glBindBufferBase;var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _glBindFramebuffer=_emscripten_glBindFramebuffer;var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _glBindTexture=_emscripten_glBindTexture;var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao]);var ibo=GLctx.getParameter(34965);GLctx.currentElementArrayBufferBinding=ibo?ibo.name|0:0};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _glBlendEquation=_emscripten_glBlendEquation;var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _glBlendFunc=_emscripten_glBlendFunc;var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _glBufferData=_emscripten_glBufferData;var _emscripten_glClear=x0=>GLctx.clear(x0);var _glClear=_emscripten_glClear;var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _glClearColor=_emscripten_glClearColor;var convertI32PairToI53=(lo,hi)=>(lo>>>0)+hi*4294967296;var _emscripten_glClientWaitSync=(sync,flags,timeout_low,timeout_high)=>{var timeout=convertI32PairToI53(timeout_low,timeout_high);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _glClientWaitSync=_emscripten_glClientWaitSync;var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _glColorMask=_emscripten_glColorMask;var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _glCompileShader=_emscripten_glCompileShader;var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _glCreateProgram=_emscripten_glCreateProgram;var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _glCreateShader=_emscripten_glCreateShader;var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i<n;i++){var id=HEAP32[buffers+i*4>>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentArrayBufferBinding)GLctx.currentArrayBufferBinding=0;if(id==GLctx.currentElementArrayBufferBinding)GLctx.currentElementArrayBufferBinding=0;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _glDeleteBuffers=_emscripten_glDeleteBuffers;var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i<n;++i){var id=HEAP32[framebuffers+i*4>>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _glDeleteFramebuffers=_emscripten_glDeleteFramebuffers;var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _glDeleteProgram=_emscripten_glDeleteProgram;var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _glDeleteShader=_emscripten_glDeleteShader;var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _glDeleteSync=_emscripten_glDeleteSync;var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i<n;i++){var id=HEAP32[textures+i*4>>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _glDeleteTextures=_emscripten_glDeleteTextures;var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i<n;i++){var id=HEAP32[vaos+i*4>>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _glDetachShader=_emscripten_glDetachShader;var _emscripten_glDisable=x0=>GLctx.disable(x0);var _glDisable=_emscripten_glDisable;var _emscripten_glDisableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=false;GLctx.disableVertexAttribArray(index)};var _glDisableVertexAttribArray=_emscripten_glDisableVertexAttribArray;var _emscripten_glDrawArrays=(mode,first,count)=>{GL.preDrawHandleClientVertexAttribBindings(first+count);GLctx.drawArrays(mode,first,count);GL.postDrawHandleClientVertexAttribBindings()};var _glDrawArrays=_emscripten_glDrawArrays;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i<n;i++){bufArray[i]=HEAP32[bufs+i*4>>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glEnable=x0=>GLctx.enable(x0);var _glEnable=_emscripten_glEnable;var _emscripten_glEnableVertexAttribArray=index=>{var cb=GL.currentContext.clientBuffers[index];cb.enabled=true;GLctx.enableVertexAttribArray(index)};var _glEnableVertexAttribArray=_emscripten_glEnableVertexAttribArray;var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _glFenceSync=_emscripten_glFenceSync;var _emscripten_glFinish=()=>GLctx.finish();var _glFinish=_emscripten_glFinish;var _emscripten_glFlush=()=>GLctx.flush();var _glFlush=_emscripten_glFlush;var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _glFramebufferTexture2D=_emscripten_glFramebufferTexture2D;var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _glFramebufferTextureLayer=_emscripten_glFramebufferTextureLayer;var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,\"createBuffer\",GL.buffers)};var _glGenBuffers=_emscripten_glGenBuffers;var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,\"createFramebuffer\",GL.framebuffers)};var _glGenFramebuffers=_emscripten_glGenFramebuffers;var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,\"createTexture\",GL.textures)};var _glGenTextures=_emscripten_glGenTextures;var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,\"createVertexArray\",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _glGetAttribLocation=_emscripten_glGetAttribLocation;var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _glGetError=_emscripten_glGetError;var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>\"GL_\"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case\"number\":ret=result;break;case\"boolean\":ret=result?1:0;break;case\"string\":GL.recordError(1280);return;case\"object\":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i<result.length;++i){switch(type){case 0:HEAP32[p+i*4>>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _glGetFloatv=_emscripten_glGetFloatv;var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _glGetIntegerv=_emscripten_glGetIntegerv;var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log=\"(unknown error)\";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i<numActiveUniforms;++i){program.maxUniformLength=Math.max(program.maxUniformLength,GLctx.getActiveUniform(program,i).name.length+1)}}HEAP32[p>>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i<numActiveAttributes;++i){program.maxAttributeLength=Math.max(program.maxAttributeLength,GLctx.getActiveAttrib(program,i).name.length+1)}}HEAP32[p>>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i<numActiveUniformBlocks;++i){program.maxUniformBlockNameLength=Math.max(program.maxUniformBlockNameLength,GLctx.getActiveUniformBlockName(program,i).length+1)}}HEAP32[p>>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _glGetProgramiv=_emscripten_glGetProgramiv;var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log=\"(unknown error)\";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetShaderInfoLog=_emscripten_glGetShaderInfoLog;var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log=\"(unknown error)\";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var _glGetShaderiv=_emscripten_glGetShaderiv;var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(\" \"));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+\"0\";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _glGetString=_emscripten_glGetString;var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _glGetUniformBlockIndex=_emscripten_glGetUniformBlockIndex;var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)==\"]\"&&name.lastIndexOf(\"[\");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i<numActiveUniforms;++i){var u=GLctx.getActiveUniform(program,i);var nm=u.name;var sz=u.size;var lb=webglGetLeftBracePos(nm);var arrayName=lb>0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j<sz;++j){uniformLocsById[id]=j;program.uniformArrayNamesById[id++]=arrayName}}}};var _emscripten_glGetUniformLocation=(program,name)=>{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex<sizeAndId[0]){arrayIndex+=sizeAndId[1];if(uniformLocsById[arrayIndex]=uniformLocsById[arrayIndex]||GLctx.getUniformLocation(program,name)){return arrayIndex}}}else{GL.recordError(1281)}return-1};var _glGetUniformLocation=_emscripten_glGetUniformLocation;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _glLineWidth=_emscripten_glLineWidth;var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _glLinkProgram=_emscripten_glLinkProgram;var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _glPixelStorei=_emscripten_glPixelStorei;var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _glReadPixels=_emscripten_glReadPixels;var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _glShaderSource=_emscripten_glShaderSource;var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _glTexImage2D=_emscripten_glTexImage2D;var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _glTexParameterf=_emscripten_glTexParameterf;var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _glTexParameterfv=_emscripten_glTexParameterfv;var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _glTexParameteri=_emscripten_glTexParameteri;var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _glTexStorage2D=_emscripten_glTexStorage2D;var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _glTexStorage3D=_emscripten_glTexStorage3D;var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _glTexSubImage2D=_emscripten_glTexSubImage2D;var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _glTexSubImage3D=_emscripten_glTexSubImage3D;var webglGetUniformLocation=location=>{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc==\"number\"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:\"\"))}return webglLoc}else{GL.recordError(1282)}};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _glUniform1f=_emscripten_glUniform1f;var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;++i){view[i]=HEAPF32[value+4*i>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _glUniform1fv=_emscripten_glUniform1fv;var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _glUniform1i=_emscripten_glUniform1i;var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _glUniform2f=_emscripten_glUniform2f;var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=2){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _glUniform2fv=_emscripten_glUniform2fv;var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _glUniform3f=_emscripten_glUniform3f;var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _glUniform4f=_emscripten_glUniform4f;var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i<count;i+=4){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _glUniform4fv=_emscripten_glUniform4fv;var miniTempWebGLIntBuffers=[];var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i<count;i+=4){view[i]=HEAP32[value+4*i>>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _glUniform4iv=_emscripten_glUniform4iv;var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _glUniformBlockBinding=_emscripten_glUniformBlockBinding;var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=4){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix2fv=_emscripten_glUniformMatrix2fv;var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i<count;i+=9){view[i]=HEAPF32[value+4*i>>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix3fv=_emscripten_glUniformMatrix3fv;var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i<count;i+=16){var dst=value+i;view[i]=heap[dst];view[i+1]=heap[dst+1];view[i+2]=heap[dst+2];view[i+3]=heap[dst+3];view[i+4]=heap[dst+4];view[i+5]=heap[dst+5];view[i+6]=heap[dst+6];view[i+7]=heap[dst+7];view[i+8]=heap[dst+8];view[i+9]=heap[dst+9];view[i+10]=heap[dst+10];view[i+11]=heap[dst+11];view[i+12]=heap[dst+12];view[i+13]=heap[dst+13];view[i+14]=heap[dst+14];view[i+15]=heap[dst+15]}}else{var view=HEAPF32.subarray(value>>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix4fv=_emscripten_glUniformMatrix4fv;var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _glUseProgram=_emscripten_glUseProgram;var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{var cb=GL.currentContext.clientBuffers[index];if(!GLctx.currentArrayBufferBinding){cb.size=size;cb.type=type;cb.normalized=normalized;cb.stride=stride;cb.ptr=ptr;cb.clientside=true;cb.vertexAttribPointerAdaptor=function(index,size,type,normalized,stride,ptr){this.vertexAttribPointer(index,size,type,normalized,stride,ptr)};return}cb.clientside=false;GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _glVertexAttribPointer=_emscripten_glVertexAttribPointer;var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _glViewport=_emscripten_glViewport;function _mediapipe_find_canvas_event_target(canvasSelector){let target=findCanvasEventTarget(canvasSelector);if(Module&&!target){target=Module.canvasWebGpu}return Emval.toHandle(target)}function _mediapipe_webgl_tex_image_drawable(drawableHandle){const drawable=Emval.toValue(drawableHandle);GLctx.texImage2D(GLctx.TEXTURE_2D,0,GLctx.RGBA,GLctx.RGBA,GLctx.UNSIGNED_BYTE,drawable)}function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS==\"undefined\"||!(e.name===\"ErrnoError\"))throw e;return e.errno}}var _wgpuCommandEncoderBeginComputePass=(encoderPtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),timestampWrites:WebGPU.makePassTimestampWrites(HEAPU32[descriptor+12>>2])}}var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateComputePassEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.beginComputePass(desc));return ptr};var _wgpuCommandEncoderBeginRenderPass=(encoderPtr,descriptor)=>{function makeColorAttachment(caPtr){var viewPtr=HEAPU32[caPtr+4>>2];if(viewPtr===0){return undefined}var depthSlice=HEAPU32[caPtr+8>>2];if(depthSlice==4294967295)depthSlice=undefined;return{view:WebGPU.getJsObject(viewPtr),depthSlice,resolveTarget:WebGPU.getJsObject(HEAPU32[caPtr+12>>2]),clearValue:WebGPU.makeColor(caPtr+24),loadOp:WebGPU.LoadOp[HEAP32[caPtr+16>>2]],storeOp:WebGPU.StoreOp[HEAP32[caPtr+20>>2]]}}function makeColorAttachments(count,caPtr){var attachments=[];for(var i=0;i<count;++i){attachments.push(makeColorAttachment(caPtr+56*i))}return attachments}function makeDepthStencilAttachment(dsaPtr){if(dsaPtr===0)return undefined;return{view:WebGPU.getJsObject(HEAPU32[dsaPtr+4>>2]),depthClearValue:HEAPF32[dsaPtr+16>>2],depthLoadOp:WebGPU.LoadOp[HEAP32[dsaPtr+8>>2]],depthStoreOp:WebGPU.StoreOp[HEAP32[dsaPtr+12>>2]],depthReadOnly:!!HEAPU32[dsaPtr+20>>2],stencilClearValue:HEAPU32[dsaPtr+32>>2],stencilLoadOp:WebGPU.LoadOp[HEAP32[dsaPtr+24>>2]],stencilStoreOp:WebGPU.StoreOp[HEAP32[dsaPtr+28>>2]],stencilReadOnly:!!HEAPU32[dsaPtr+36>>2]}}function makeRenderPassDescriptor(descriptor){var nextInChainPtr=HEAPU32[descriptor>>2];var maxDrawCount=undefined;if(nextInChainPtr!==0){var sType=HEAP32[nextInChainPtr+4>>2];var renderPassMaxDrawCount=nextInChainPtr;maxDrawCount=readI53FromI64(renderPassMaxDrawCount+8)}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),colorAttachments:makeColorAttachments(HEAPU32[descriptor+12>>2],HEAPU32[descriptor+16>>2]),depthStencilAttachment:makeDepthStencilAttachment(HEAPU32[descriptor+20>>2]),occlusionQuerySet:WebGPU.getJsObject(HEAPU32[descriptor+24>>2]),timestampWrites:WebGPU.makePassTimestampWrites(HEAPU32[descriptor+28>>2]),maxDrawCount};return desc}var desc=makeRenderPassDescriptor(descriptor);var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateRenderPassEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.beginRenderPass(desc));return ptr};var _wgpuCommandEncoderCopyBufferToTexture=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyBufferToTexture(WebGPU.makeTexelCopyBufferInfo(srcPtr),WebGPU.makeTexelCopyTextureInfo(dstPtr),copySize)};var _wgpuCommandEncoderCopyTextureToBuffer=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyTextureToBuffer(WebGPU.makeTexelCopyTextureInfo(srcPtr),WebGPU.makeTexelCopyBufferInfo(dstPtr),copySize)};var _wgpuCommandEncoderCopyTextureToTexture=(encoderPtr,srcPtr,dstPtr,copySizePtr)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var copySize=WebGPU.makeExtent3D(copySizePtr);commandEncoder.copyTextureToTexture(WebGPU.makeTexelCopyTextureInfo(srcPtr),WebGPU.makeTexelCopyTextureInfo(dstPtr),copySize)};var _wgpuCommandEncoderFinish=(encoderPtr,descriptor)=>{var commandEncoder=WebGPU.getJsObject(encoderPtr);var ptr=_emwgpuCreateCommandBuffer(0);WebGPU.Internals.jsObjectInsert(ptr,commandEncoder.finish());return ptr};var _wgpuComputePassEncoderDispatchWorkgroups=(passPtr,x,y,z)=>{var pass=WebGPU.getJsObject(passPtr);pass.dispatchWorkgroups(x,y,z)};var _wgpuComputePassEncoderEnd=passPtr=>{var pass=WebGPU.getJsObject(passPtr);pass.end()};var _wgpuComputePassEncoderSetBindGroup=(passPtr,groupIndex,groupPtr,dynamicOffsetCount,dynamicOffsetsPtr)=>{var pass=WebGPU.getJsObject(passPtr);var group=WebGPU.getJsObject(groupPtr);if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,HEAPU32,dynamicOffsetsPtr>>2,dynamicOffsetCount)}};var _wgpuComputePassEncoderSetPipeline=(passPtr,pipelinePtr)=>{var pass=WebGPU.getJsObject(passPtr);var pipeline=WebGPU.getJsObject(pipelinePtr);pass.setPipeline(pipeline)};var _wgpuComputePipelineGetBindGroupLayout=(pipelinePtr,groupIndex)=>{var pipeline=WebGPU.getJsObject(pipelinePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,pipeline.getBindGroupLayout(groupIndex));return ptr};var _wgpuDeviceCreateBindGroup=(devicePtr,descriptor)=>{function makeEntry(entryPtr){var bufferPtr=HEAPU32[entryPtr+8>>2];var samplerPtr=HEAPU32[entryPtr+32>>2];var textureViewPtr=HEAPU32[entryPtr+36>>2];var externalTexturePtr=0;WebGPU.iterateExtensions(entryPtr,{327681:ptr=>{externalTexturePtr=HEAPU32[ptr+8>>2]}});var resource;if(bufferPtr){var size=readI53FromI64(entryPtr+24);if(size==-1)size=undefined;resource={buffer:WebGPU.getJsObject(bufferPtr),offset:readI53FromI64(entryPtr+16),size}}else{resource=WebGPU.getJsObject(samplerPtr||textureViewPtr||externalTexturePtr)}return{binding:HEAPU32[entryPtr+4>>2],resource}}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i<count;++i){entries.push(makeEntry(entriesPtrs+40*i))}return entries}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),layout:WebGPU.getJsObject(HEAPU32[descriptor+12>>2]),entries:makeEntries(HEAPU32[descriptor+16>>2],HEAPU32[descriptor+20>>2])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroup(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroup(desc));return ptr};var _wgpuDeviceCreateBindGroupLayout=(devicePtr,descriptor)=>{function makeBufferEntry(substructPtr){var typeInt=HEAPU32[substructPtr+4>>2];if(!typeInt)return undefined;return{type:WebGPU.BufferBindingType[typeInt],hasDynamicOffset:!!HEAPU32[substructPtr+8>>2],minBindingSize:readI53FromI64(substructPtr+16)}}function makeSamplerEntry(substructPtr){var typeInt=HEAPU32[substructPtr+4>>2];if(!typeInt)return undefined;return{type:WebGPU.SamplerBindingType[typeInt]}}function makeTextureEntry(substructPtr){var sampleTypeInt=HEAPU32[substructPtr+4>>2];if(!sampleTypeInt)return undefined;return{sampleType:WebGPU.TextureSampleType[sampleTypeInt],viewDimension:WebGPU.TextureViewDimension[HEAP32[substructPtr+8>>2]],multisampled:!!HEAPU32[substructPtr+12>>2]}}function makeStorageTextureEntry(substructPtr){var accessInt=HEAPU32[substructPtr+4>>2];if(!accessInt)return undefined;return{access:WebGPU.StorageTextureAccess[accessInt],format:WebGPU.TextureFormat[HEAP32[substructPtr+8>>2]],viewDimension:WebGPU.TextureViewDimension[HEAP32[substructPtr+12>>2]]}}function makeEntry(entryPtr){var entry={binding:HEAPU32[entryPtr+4>>2],visibility:HEAPU32[entryPtr+8>>2],buffer:makeBufferEntry(entryPtr+24),sampler:makeSamplerEntry(entryPtr+48),texture:makeTextureEntry(entryPtr+56),storageTexture:makeStorageTextureEntry(entryPtr+72)};WebGPU.iterateExtensions(entryPtr,{327682:ptr=>{entry[\"externalTexture\"]={}}});return entry}function makeEntries(count,entriesPtrs){var entries=[];for(var i=0;i<count;++i){entries.push(makeEntry(entriesPtrs+88*i))}return entries}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),entries:makeEntries(HEAPU32[descriptor+12>>2],HEAPU32[descriptor+16>>2])};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createBindGroupLayout(desc));return ptr};var _wgpuDeviceCreateCommandEncoder=(devicePtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4)}}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateCommandEncoder(0);WebGPU.Internals.jsObjectInsert(ptr,device.createCommandEncoder(desc));return ptr};var _wgpuDeviceCreateComputePipeline=(devicePtr,descriptor)=>{var desc=WebGPU.makeComputePipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateComputePipeline(0);WebGPU.Internals.jsObjectInsert(ptr,device.createComputePipeline(desc));return ptr};var _wgpuDeviceCreatePipelineLayout=(devicePtr,descriptor)=>{var bglCount=HEAPU32[descriptor+12>>2];var bglPtr=HEAPU32[descriptor+16>>2];var bgls=[];for(var i=0;i<bglCount;++i){bgls.push(WebGPU.getJsObject(HEAPU32[bglPtr+4*i>>2]))}var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),bindGroupLayouts:bgls};var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreatePipelineLayout(0);WebGPU.Internals.jsObjectInsert(ptr,device.createPipelineLayout(desc));return ptr};var _wgpuDeviceCreateRenderPipeline=(devicePtr,descriptor)=>{var desc=WebGPU.makeRenderPipelineDesc(descriptor);var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateRenderPipeline(0);WebGPU.Internals.jsObjectInsert(ptr,device.createRenderPipeline(desc));return ptr};var _wgpuDeviceCreateSampler=(devicePtr,descriptor)=>{var desc;if(descriptor){desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),addressModeU:WebGPU.AddressMode[HEAP32[descriptor+12>>2]],addressModeV:WebGPU.AddressMode[HEAP32[descriptor+16>>2]],addressModeW:WebGPU.AddressMode[HEAP32[descriptor+20>>2]],magFilter:WebGPU.FilterMode[HEAP32[descriptor+24>>2]],minFilter:WebGPU.FilterMode[HEAP32[descriptor+28>>2]],mipmapFilter:WebGPU.MipmapFilterMode[HEAP32[descriptor+32>>2]],lodMinClamp:HEAPF32[descriptor+36>>2],lodMaxClamp:HEAPF32[descriptor+40>>2],compare:WebGPU.CompareFunction[HEAP32[descriptor+44>>2]],maxAnisotropy:HEAPU16[descriptor+48>>1]}}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateSampler(0);WebGPU.Internals.jsObjectInsert(ptr,device.createSampler(desc));return ptr};var _wgpuDeviceCreateTexture=(devicePtr,descriptor)=>{var desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),size:WebGPU.makeExtent3D(descriptor+28),mipLevelCount:HEAPU32[descriptor+44>>2],sampleCount:HEAPU32[descriptor+48>>2],dimension:WebGPU.TextureDimension[HEAP32[descriptor+24>>2]],format:WebGPU.TextureFormat[HEAP32[descriptor+40>>2]],usage:HEAPU32[descriptor+16>>2]};var viewFormatCount=HEAPU32[descriptor+52>>2];if(viewFormatCount){var viewFormatsPtr=HEAPU32[descriptor+56>>2];desc[\"viewFormats\"]=Array.from(HEAP32.subarray(viewFormatsPtr>>2,viewFormatsPtr+viewFormatCount*4>>2),format=>WebGPU.TextureFormat[format])}var device=WebGPU.getJsObject(devicePtr);var ptr=_emwgpuCreateTexture(0);WebGPU.Internals.jsObjectInsert(ptr,device.createTexture(desc));return ptr};var _wgpuQueueSubmit=(queuePtr,commandCount,commands)=>{var queue=WebGPU.getJsObject(queuePtr);var cmds=Array.from(HEAP32.subarray(commands>>2,commands+commandCount*4>>2),id=>WebGPU.getJsObject(id));queue.submit(cmds)};function _wgpuQueueWriteBuffer(queuePtr,bufferPtr,bufferOffset_low,bufferOffset_high,data,size){var bufferOffset=convertI32PairToI53Checked(bufferOffset_low,bufferOffset_high);var queue=WebGPU.getJsObject(queuePtr);var buffer=WebGPU.getJsObject(bufferPtr);var subarray=HEAPU8.subarray(data,data+size);queue.writeBuffer(buffer,bufferOffset,subarray,0,size)}var _wgpuQueueWriteTexture=(queuePtr,destinationPtr,data,dataSize,dataLayoutPtr,writeSizePtr)=>{var queue=WebGPU.getJsObject(queuePtr);var destination=WebGPU.makeTexelCopyTextureInfo(destinationPtr);var dataLayout=WebGPU.makeTexelCopyBufferLayout(dataLayoutPtr);var writeSize=WebGPU.makeExtent3D(writeSizePtr);var subarray=HEAPU8.subarray(data,data+dataSize);queue.writeTexture(destination,subarray,dataLayout,writeSize)};var _wgpuRenderPassEncoderDraw=(passPtr,vertexCount,instanceCount,firstVertex,firstInstance)=>{firstVertex>>>=0;firstInstance>>>=0;var pass=WebGPU.getJsObject(passPtr);pass.draw(vertexCount,instanceCount,firstVertex,firstInstance)};var _wgpuRenderPassEncoderEnd=encoderPtr=>{var encoder=WebGPU.getJsObject(encoderPtr);encoder.end()};var _wgpuRenderPassEncoderSetBindGroup=(passPtr,groupIndex,groupPtr,dynamicOffsetCount,dynamicOffsetsPtr)=>{var pass=WebGPU.getJsObject(passPtr);var group=WebGPU.getJsObject(groupPtr);if(dynamicOffsetCount==0){pass.setBindGroup(groupIndex,group)}else{pass.setBindGroup(groupIndex,group,HEAPU32,dynamicOffsetsPtr>>2,dynamicOffsetCount)}};var _wgpuRenderPassEncoderSetPipeline=(passPtr,pipelinePtr)=>{var pass=WebGPU.getJsObject(passPtr);var pipeline=WebGPU.getJsObject(pipelinePtr);pass.setPipeline(pipeline)};var _wgpuRenderPipelineGetBindGroupLayout=(pipelinePtr,groupIndex)=>{var pipeline=WebGPU.getJsObject(pipelinePtr);var ptr=_emwgpuCreateBindGroupLayout(0);WebGPU.Internals.jsObjectInsert(ptr,pipeline.getBindGroupLayout(groupIndex));return ptr};var _wgpuTextureCreateView=(texturePtr,descriptor)=>{var desc;if(descriptor){var swizzle;var nextInChainPtr=HEAPU32[descriptor>>2];if(nextInChainPtr!==0){var sType=HEAP32[nextInChainPtr+4>>2];var swizzleDescriptor=nextInChainPtr;var swizzlePtr=swizzleDescriptor+8;var r=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr>>2]]||\"r\";var g=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+4>>2]]||\"g\";var b=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+8>>2]]||\"b\";var a=WebGPU.ComponentSwizzle[HEAP32[swizzlePtr+12>>2]]||\"a\";swizzle=`${r}${g}${b}${a}`}var mipLevelCount=HEAPU32[descriptor+24>>2];var arrayLayerCount=HEAPU32[descriptor+32>>2];desc={label:WebGPU.makeStringFromOptionalStringView(descriptor+4),format:WebGPU.TextureFormat[HEAP32[descriptor+12>>2]],dimension:WebGPU.TextureViewDimension[HEAP32[descriptor+16>>2]],baseMipLevel:HEAPU32[descriptor+20>>2],mipLevelCount:mipLevelCount===4294967295?undefined:mipLevelCount,baseArrayLayer:HEAPU32[descriptor+28>>2],arrayLayerCount:arrayLayerCount===4294967295?undefined:arrayLayerCount,aspect:WebGPU.TextureAspect[HEAP32[descriptor+36>>2]],swizzle}}var texture=WebGPU.getJsObject(texturePtr);var ptr=_emwgpuCreateTextureView(0);WebGPU.Internals.jsObjectInsert(ptr,texture.createView(desc));return ptr};var _wgpuTextureDestroy=texturePtr=>{WebGPU.getJsObject(texturePtr).destroy()};var _wgpuTextureGetFormat=texturePtr=>{var texture=WebGPU.getJsObject(texturePtr);return WebGPU.TextureFormat.indexOf(texture.format)};var getCFunc=ident=>{var func=Module[\"_\"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType===\"string\"){return UTF8ToString(ret)}if(returnType===\"boolean\")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var FS_createPath=(...args)=>FS.createPath(...args);var FS_unlink=(...args)=>FS.unlink(...args);var FS_createLazyFile=(...args)=>FS.createLazyFile(...args);var FS_createDevice=(...args)=>FS.createDevice(...args);FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();registerPreMainLoop(()=>GL.newRenderingFrameStarted());for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module[\"preloadPlugins\"])preloadPlugins=Module[\"preloadPlugins\"];if(Module[\"noExitRuntime\"])noExitRuntime=Module[\"noExitRuntime\"];if(Module[\"print\"])out=Module[\"print\"];if(Module[\"printErr\"])err=Module[\"printErr\"];if(Module[\"wasmBinary\"])wasmBinary=Module[\"wasmBinary\"];if(Module[\"arguments\"])arguments_=Module[\"arguments\"];if(Module[\"thisProgram\"])thisProgram=Module[\"thisProgram\"];if(Module[\"preInit\"]){if(typeof Module[\"preInit\"]==\"function\")Module[\"preInit\"]=[Module[\"preInit\"]];while(Module[\"preInit\"].length>0){Module[\"preInit\"].shift()()}}}Module[\"addRunDependency\"]=addRunDependency;Module[\"removeRunDependency\"]=removeRunDependency;Module[\"ccall\"]=ccall;Module[\"stringToNewUTF8\"]=stringToNewUTF8;Module[\"FS_preloadFile\"]=FS_preloadFile;Module[\"FS_unlink\"]=FS_unlink;Module[\"FS_createPath\"]=FS_createPath;Module[\"FS_createDevice\"]=FS_createDevice;Module[\"FS_createDataFile\"]=FS_createDataFile;Module[\"FS_createLazyFile\"]=FS_createLazyFile;var ASM_CONSTS={1484533:$0=>{const canvas=Emval.toValue($0);const context=canvas.getContext(\"webgpu\");return WebGPU.importJsTexture(context.getCurrentTexture())},1484676:($0,$1,$2,$3,$4)=>{const drawable=Emval.toValue($0);const device=WebGPU.getJsObject($1);const texture=WebGPU.getJsObject($2);const width=$3;const height=$4;device.queue.copyExternalImageToTexture({source:drawable},{texture},[width,height])},1484935:($0,$1,$2,$3)=>{const sourceExtTex=Emval.toValue($0);const device=WebGPU.getJsObject($1);const sampler=WebGPU.getJsObject($2);const bgLayout=WebGPU.getJsObject($3);const bindGroup=device.createBindGroup({layout:bgLayout,entries:[{binding:0,resource:sampler},{binding:1,resource:sourceExtTex}]});return WebGPU.importJsBindGroup(bindGroup)},1485305:($0,$1)=>{const input=Emval.toValue($0);const output=Emval.toValue($1);const ctx=output.getContext(\"2d\");ctx.drawImage(input,0,0,output.width,output.height)},1485470:($0,$1)=>{const inputArray=Emval.toValue($0);const output=Emval.toValue($1);const ctx=output.getContext(\"2d\");const image_data=new ImageData(inputArray,output.width,output.height);ctx.putImageData(image_data,0,0)},1485694:($0,$1)=>{const input=Emval.toValue($0);const outputArray=Emval.toValue($1);const ctx=input.getContext(\"2d\");const data=ctx.getImageData(0,0,input.width,input.height);outputArray.set(data.data)},1485898:()=>typeof HTMLCanvasElement!==\"undefined\",1485953:()=>!!Module[\"preinitializedWebGPUDevice\"],1486004:()=>{specialHTMLTargets[\"#canvas\"]=Module.canvas}};function BeginGlQueryTiming(calc_name,num_repetitions){const gl=Module.canvas.getContext(\"webgl2\");const query=gl.createQuery();Module.WEBGL_SHADER_CALC_METRICS=Module.WEBGL_SHADER_CALC_METRICS||{};Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)]={query,repetitions:num_repetitions};Module.WEBGL_QUERY_TIMER_EXT=Module.WEBGL_QUERY_TIMER_EXT||gl.getExtension(\"EXT_disjoint_timer_query_webgl2\");gl.beginQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT,query)}function EndGlQueryTiming(calc_name){const gl=Module.canvas.getContext(\"webgl2\");gl.endQuery(Module.WEBGL_QUERY_TIMER_EXT.TIME_ELAPSED_EXT,Module.WEBGL_SHADER_CALC_METRICS[UTF8ToString(calc_name)].query)}function JsWrapImageConverter(){if(!Module._imageConverter){Module._imageConverter=(binaryPtr,binarySize,width,height,numChannels,makeDeepCopy,outputType)=>{const imageData=new outputType(makeDeepCopy?Module.HEAPU8.slice(binaryPtr,binaryPtr+binarySize).buffer:Module.HEAPU8.buffer,binaryPtr,width*height*numChannels);return{data:imageData,width,height}}}}function JsOnUint8ArrayImageListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Uint8Array);Module._wrapSimpleListenerOutput(output_stream_name,image,timestamp_ms)}function JsOnFloat32ArrayImageListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Float32Array);Module._wrapSimpleListenerOutput(output_stream_name,image,timestamp_ms)}function JsOnWebGLTextureListener(output_stream_name,name,width,height,timestamp_ms){Module._wrapSimpleListenerOutput(output_stream_name,{data:GL.textures[name],width,height},timestamp_ms)}function JsOnUint8ArrayImageVectorListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Uint8Array);Module._wrapSimpleListenerOutput(output_stream_name,image,false,timestamp_ms)}function JsOnFloat32ArrayImageVectorListener(output_stream_name,binary_ptr,binary_size,width,height,num_channels,make_deep_copy,timestamp_ms){const image=Module._imageConverter(binary_ptr,binary_size,width,height,num_channels,make_deep_copy,Float32Array);Module._wrapSimpleListenerOutput(output_stream_name,image,false,timestamp_ms)}function JsOnWebGLTextureVectorListener(output_stream_name,name,width,height,timestamp_ms){Module._wrapSimpleListenerOutput(output_stream_name,{data:GL.textures[name],width,height},false,timestamp_ms)}function JsOnEmptyPacketListener(output_stream_name,timestamp){Module._wrapEmptyPacketListenerOutput(output_stream_name,timestamp)}function JsOnVectorFinishedListener(output_stream_name,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,undefined,true,timestamp)}function JsOnSimpleListenerBool(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerBool(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerInt(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerInt(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerUint(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerUint(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerDouble(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerDouble(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerFloat(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,timestamp)}function JsOnVectorListenerFloat(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,out_data,false,timestamp)}function JsOnSimpleListenerString(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,UTF8ToString(out_data),timestamp)}function JsOnVectorListenerString(output_stream_name,out_data,timestamp){Module._wrapSimpleListenerOutput(output_stream_name,UTF8ToString(out_data),false,timestamp)}function JsOnVectorListenerProto(output_stream_name,proto_ptr,proto_size,make_deep_copy,timestamp){const newProtoArray=make_deep_copy?Module.HEAPU8.slice(proto_ptr,proto_ptr+proto_size):new Uint8Array(Module.HEAPU8.buffer,proto_ptr,proto_size);Module._wrapSimpleListenerOutput(output_stream_name,newProtoArray,false,timestamp)}function JsWrapSimpleListeners(){if(!Module._wrapSimpleListenerOutput){Module._wrapSimpleListenerOutput=(outputStreamName,...args)=>{if(Module.simpleListeners){const streamName=UTF8ToString(outputStreamName);if(Module.simpleListeners[streamName]){Module.simpleListeners[streamName](...args)}}}}if(!Module._wrapEmptyPacketListenerOutput){Module._wrapEmptyPacketListenerOutput=(outputStreamName,timestamp)=>{if(Module.emptyPacketListeners){const streamName=UTF8ToString(outputStreamName);if(Module.emptyPacketListeners[streamName]){Module.emptyPacketListeners[streamName](timestamp)}}}}}function JsOnSimpleListenerBinaryArray(output_stream_name,binary_ptr,binary_size,make_deep_copy,timestamp){const newProtoArray=make_deep_copy?Module.HEAPU8.slice(binary_ptr,binary_ptr+binary_size):new Uint8Array(Module.HEAPU8.buffer,binary_ptr,binary_size);Module._wrapSimpleListenerOutput(output_stream_name,newProtoArray,timestamp)}function mediapipe_import_external_texture(device_handle,source_handle){const device=WebGPU.getJsObject(device_handle);const source=Emval.toValue(source_handle);const externalTexture=device.importExternalTexture({source});return Emval.toHandle(externalTexture)}function mediapipe_create_utility_canvas2d(){let canvas;if(typeof HTMLCanvasElement!==\"undefined\"){canvas=document.createElement(\"canvas\");canvas.style.display=\"none\"}else{canvas=new OffscreenCanvas(0,0)}return Emval.toHandle(canvas)}function GetAdapterArchitecture(){const device=Module[\"preinitializedWebGPUDevice\"];const architecture=device.adapterInfo?device.adapterInfo.architecture:\"Unknown\";return stringToNewUTF8(architecture)}function GetAdapterDescription(){const device=Module[\"preinitializedWebGPUDevice\"];const description=device.adapterInfo?device.adapterInfo.description:\"Unknown\";return stringToNewUTF8(description)}function GetAdapterDeviceName(){const device=Module[\"preinitializedWebGPUDevice\"];const deviceName=device.adapterInfo?device.adapterInfo.device:\"Unknown\";return stringToNewUTF8(deviceName)}function GetAdapterVendor(){const device=Module[\"preinitializedWebGPUDevice\"];const vendor=device.adapterInfo?device.adapterInfo.vendor:\"Unknown\";return stringToNewUTF8(vendor)}function __asyncjs__mediapipe_map_buffer_jspi(buffer_handle,data){return Asyncify.handleAsync(async()=>{const buffer=WebGPU.getJsObject(buffer_handle);if(\"mapSync\"in buffer){buffer.mapSync(GPUMapMode.READ)}else{await buffer.mapAsync(GPUMapMode.READ)}const mapped=buffer.getMappedRange();HEAPU8.set(new Uint8Array(mapped),data);buffer.unmap()})}function JsWrapErrorListener(code,message){if(Module.errorListener){const stringMessage=UTF8ToString(message);Module.errorListener(code,stringMessage)}}function UseBottomLeftGpuOrigin(){return Module&&Module.gpuOriginForWebTexturesIsBottomLeft}function custom_emscripten_dbgn(str,len){if(typeof dbg!==\"undefined\"){dbg(UTF8ToString(str,len))}else{if(typeof custom_dbg===\"undefined\"){function custom_dbg(text){console.warn.apply(console,arguments)}}custom_dbg(UTF8ToString(str,len))}}var _free,_malloc,_wgpuDeviceAddRef,_addBoundTextureAsImageToStream,_attachImageListener,_attachImageVectorListener,_registerModelResourcesGraphService,_bindTextureToStream,_addBoundTextureToStream,_addDoubleToInputStream,_addFloatToInputStream,_addBoolToInputStream,_addIntToInputStream,_addUintToInputStream,_addStringToInputStream,_addRawDataSpanToInputStream,_allocateBoolVector,_allocateFloatVector,_allocateDoubleVector,_allocateIntVector,_allocateUintVector,_allocateStringVector,_addBoolVectorEntry,_addFloatVectorEntry,_addDoubleVectorEntry,_addIntVectorEntry,_addUintVectorEntry,_addStringVectorEntry,_addBoolVectorToInputStream,_addFloatVectorToInputStream,_addDoubleVectorToInputStream,_addIntVectorToInputStream,_addUintVectorToInputStream,_addStringVectorToInputStream,_addFlatHashMapToInputStream,_addProtoToInputStream,_addEmptyPacketToInputStream,_addBoolToInputSidePacket,_addDoubleToInputSidePacket,_addFloatToInputSidePacket,_addIntToInputSidePacket,_addUintToInputSidePacket,_addStringToInputSidePacket,_addRawDataSpanToInputSidePacket,_addProtoToInputSidePacket,_addBoolVectorToInputSidePacket,_addDoubleVectorToInputSidePacket,_addFloatVectorToInputSidePacket,_addIntVectorToInputSidePacket,_addUintVectorToInputSidePacket,_addStringVectorToInputSidePacket,_attachBoolListener,_attachBoolVectorListener,_attachDoubleListener,_attachDoubleVectorListener,_attachFloatListener,_attachFloatVectorListener,_attachIntListener,_attachIntVectorListener,_attachUintListener,_attachUintVectorListener,_attachStringListener,_attachStringVectorListener,_attachProtoListener,_attachProtoVectorListener,_getGraphConfig,___getTypeName,_emwgpuCreateBindGroup,_emwgpuCreateBindGroupLayout,_emwgpuCreateCommandBuffer,_emwgpuCreateCommandEncoder,_emwgpuCreateComputePassEncoder,_emwgpuCreateComputePipeline,_emwgpuCreateExternalTexture,_emwgpuCreatePipelineLayout,_emwgpuCreateQuerySet,_emwgpuCreateRenderBundle,_emwgpuCreateRenderBundleEncoder,_emwgpuCreateRenderPassEncoder,_emwgpuCreateRenderPipeline,_emwgpuCreateSampler,_emwgpuCreateSurface,_emwgpuCreateTexture,_emwgpuCreateTextureView,_emwgpuCreateAdapter,_emwgpuCreateBuffer,_emwgpuCreateDevice,_emwgpuCreateQueue,_emwgpuCreateShaderModule,_emwgpuOnCreateComputePipelineCompleted,_emwgpuOnCreateRenderPipelineCompleted,_clearSubgraphs,_pushBinarySubgraph,_pushTextSubgraph,_changeBinaryGraph,_changeTextGraph,_processGl,_process,_bindTextureToCanvas,_requestShaderRefreshOnGraphChange,_waitUntilIdle,_closeGraph,_setAutoRenderToScreen,_emscripten_builtin_memalign,_memalign,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,dynCall_ji,dynCall_jii,dynCall_iiiijij,dynCall_viiji,dynCall_viji,dynCall_iiiji,dynCall_jjj,dynCall_iiiijj,dynCall_viijj,dynCall_viiijjj,dynCall_vij,dynCall_viijii,dynCall_viiiji,dynCall_vijjj,dynCall_vj,dynCall_viij,dynCall_jiji,dynCall_iiiiij,dynCall_iiiiijj,dynCall_iiiiiijj,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_free=Module[\"_free\"]=wasmExports[\"kd\"];_malloc=Module[\"_malloc\"]=wasmExports[\"ld\"];_wgpuDeviceAddRef=wasmExports[\"md\"];_addBoundTextureAsImageToStream=Module[\"_addBoundTextureAsImageToStream\"]=wasmExports[\"nd\"];_attachImageListener=Module[\"_attachImageListener\"]=wasmExports[\"od\"];_attachImageVectorListener=Module[\"_attachImageVectorListener\"]=wasmExports[\"pd\"];_registerModelResourcesGraphService=Module[\"_registerModelResourcesGraphService\"]=wasmExports[\"qd\"];_bindTextureToStream=Module[\"_bindTextureToStream\"]=wasmExports[\"rd\"];_addBoundTextureToStream=Module[\"_addBoundTextureToStream\"]=wasmExports[\"sd\"];_addDoubleToInputStream=Module[\"_addDoubleToInputStream\"]=wasmExports[\"td\"];_addFloatToInputStream=Module[\"_addFloatToInputStream\"]=wasmExports[\"ud\"];_addBoolToInputStream=Module[\"_addBoolToInputStream\"]=wasmExports[\"vd\"];_addIntToInputStream=Module[\"_addIntToInputStream\"]=wasmExports[\"wd\"];_addUintToInputStream=Module[\"_addUintToInputStream\"]=wasmExports[\"xd\"];_addStringToInputStream=Module[\"_addStringToInputStream\"]=wasmExports[\"yd\"];_addRawDataSpanToInputStream=Module[\"_addRawDataSpanToInputStream\"]=wasmExports[\"zd\"];_allocateBoolVector=Module[\"_allocateBoolVector\"]=wasmExports[\"Ad\"];_allocateFloatVector=Module[\"_allocateFloatVector\"]=wasmExports[\"Bd\"];_allocateDoubleVector=Module[\"_allocateDoubleVector\"]=wasmExports[\"Cd\"];_allocateIntVector=Module[\"_allocateIntVector\"]=wasmExports[\"Dd\"];_allocateUintVector=Module[\"_allocateUintVector\"]=wasmExports[\"Ed\"];_allocateStringVector=Module[\"_allocateStringVector\"]=wasmExports[\"Fd\"];_addBoolVectorEntry=Module[\"_addBoolVectorEntry\"]=wasmExports[\"Gd\"];_addFloatVectorEntry=Module[\"_addFloatVectorEntry\"]=wasmExports[\"Hd\"];_addDoubleVectorEntry=Module[\"_addDoubleVectorEntry\"]=wasmExports[\"Id\"];_addIntVectorEntry=Module[\"_addIntVectorEntry\"]=wasmExports[\"Jd\"];_addUintVectorEntry=Module[\"_addUintVectorEntry\"]=wasmExports[\"Kd\"];_addStringVectorEntry=Module[\"_addStringVectorEntry\"]=wasmExports[\"Ld\"];_addBoolVectorToInputStream=Module[\"_addBoolVectorToInputStream\"]=wasmExports[\"Md\"];_addFloatVectorToInputStream=Module[\"_addFloatVectorToInputStream\"]=wasmExports[\"Nd\"];_addDoubleVectorToInputStream=Module[\"_addDoubleVectorToInputStream\"]=wasmExports[\"Od\"];_addIntVectorToInputStream=Module[\"_addIntVectorToInputStream\"]=wasmExports[\"Pd\"];_addUintVectorToInputStream=Module[\"_addUintVectorToInputStream\"]=wasmExports[\"Qd\"];_addStringVectorToInputStream=Module[\"_addStringVectorToInputStream\"]=wasmExports[\"Rd\"];_addFlatHashMapToInputStream=Module[\"_addFlatHashMapToInputStream\"]=wasmExports[\"Sd\"];_addProtoToInputStream=Module[\"_addProtoToInputStream\"]=wasmExports[\"Td\"];_addEmptyPacketToInputStream=Module[\"_addEmptyPacketToInputStream\"]=wasmExports[\"Ud\"];_addBoolToInputSidePacket=Module[\"_addBoolToInputSidePacket\"]=wasmExports[\"Vd\"];_addDoubleToInputSidePacket=Module[\"_addDoubleToInputSidePacket\"]=wasmExports[\"Wd\"];_addFloatToInputSidePacket=Module[\"_addFloatToInputSidePacket\"]=wasmExports[\"Xd\"];_addIntToInputSidePacket=Module[\"_addIntToInputSidePacket\"]=wasmExports[\"Yd\"];_addUintToInputSidePacket=Module[\"_addUintToInputSidePacket\"]=wasmExports[\"Zd\"];_addStringToInputSidePacket=Module[\"_addStringToInputSidePacket\"]=wasmExports[\"_d\"];_addRawDataSpanToInputSidePacket=Module[\"_addRawDataSpanToInputSidePacket\"]=wasmExports[\"$d\"];_addProtoToInputSidePacket=Module[\"_addProtoToInputSidePacket\"]=wasmExports[\"ae\"];_addBoolVectorToInputSidePacket=Module[\"_addBoolVectorToInputSidePacket\"]=wasmExports[\"be\"];_addDoubleVectorToInputSidePacket=Module[\"_addDoubleVectorToInputSidePacket\"]=wasmExports[\"ce\"];_addFloatVectorToInputSidePacket=Module[\"_addFloatVectorToInputSidePacket\"]=wasmExports[\"de\"];_addIntVectorToInputSidePacket=Module[\"_addIntVectorToInputSidePacket\"]=wasmExports[\"ee\"];_addUintVectorToInputSidePacket=Module[\"_addUintVectorToInputSidePacket\"]=wasmExports[\"fe\"];_addStringVectorToInputSidePacket=Module[\"_addStringVectorToInputSidePacket\"]=wasmExports[\"ge\"];_attachBoolListener=Module[\"_attachBoolListener\"]=wasmExports[\"he\"];_attachBoolVectorListener=Module[\"_attachBoolVectorListener\"]=wasmExports[\"ie\"];_attachDoubleListener=Module[\"_attachDoubleListener\"]=wasmExports[\"je\"];_attachDoubleVectorListener=Module[\"_attachDoubleVectorListener\"]=wasmExports[\"ke\"];_attachFloatListener=Module[\"_attachFloatListener\"]=wasmExports[\"le\"];_attachFloatVectorListener=Module[\"_attachFloatVectorListener\"]=wasmExports[\"me\"];_attachIntListener=Module[\"_attachIntListener\"]=wasmExports[\"ne\"];_attachIntVectorListener=Module[\"_attachIntVectorListener\"]=wasmExports[\"oe\"];_attachUintListener=Module[\"_attachUintListener\"]=wasmExports[\"pe\"];_attachUintVectorListener=Module[\"_attachUintVectorListener\"]=wasmExports[\"qe\"];_attachStringListener=Module[\"_attachStringListener\"]=wasmExports[\"re\"];_attachStringVectorListener=Module[\"_attachStringVectorListener\"]=wasmExports[\"se\"];_attachProtoListener=Module[\"_attachProtoListener\"]=wasmExports[\"te\"];_attachProtoVectorListener=Module[\"_attachProtoVectorListener\"]=wasmExports[\"ue\"];_getGraphConfig=Module[\"_getGraphConfig\"]=wasmExports[\"ve\"];___getTypeName=wasmExports[\"we\"];_emwgpuCreateBindGroup=wasmExports[\"xe\"];_emwgpuCreateBindGroupLayout=wasmExports[\"ye\"];_emwgpuCreateCommandBuffer=wasmExports[\"ze\"];_emwgpuCreateCommandEncoder=wasmExports[\"Ae\"];_emwgpuCreateComputePassEncoder=wasmExports[\"Be\"];_emwgpuCreateComputePipeline=wasmExports[\"Ce\"];_emwgpuCreateExternalTexture=wasmExports[\"De\"];_emwgpuCreatePipelineLayout=wasmExports[\"Ee\"];_emwgpuCreateQuerySet=wasmExports[\"Fe\"];_emwgpuCreateRenderBundle=wasmExports[\"Ge\"];_emwgpuCreateRenderBundleEncoder=wasmExports[\"He\"];_emwgpuCreateRenderPassEncoder=wasmExports[\"Ie\"];_emwgpuCreateRenderPipeline=wasmExports[\"Je\"];_emwgpuCreateSampler=wasmExports[\"Ke\"];_emwgpuCreateSurface=wasmExports[\"Le\"];_emwgpuCreateTexture=wasmExports[\"Me\"];_emwgpuCreateTextureView=wasmExports[\"Ne\"];_emwgpuCreateAdapter=wasmExports[\"Oe\"];_emwgpuCreateBuffer=wasmExports[\"Pe\"];_emwgpuCreateDevice=wasmExports[\"Qe\"];_emwgpuCreateQueue=wasmExports[\"Re\"];_emwgpuCreateShaderModule=wasmExports[\"Se\"];_emwgpuOnCreateComputePipelineCompleted=wasmExports[\"Te\"];_emwgpuOnCreateRenderPipelineCompleted=wasmExports[\"Ue\"];_clearSubgraphs=Module[\"_clearSubgraphs\"]=wasmExports[\"Ve\"];_pushBinarySubgraph=Module[\"_pushBinarySubgraph\"]=wasmExports[\"We\"];_pushTextSubgraph=Module[\"_pushTextSubgraph\"]=wasmExports[\"Xe\"];_changeBinaryGraph=Module[\"_changeBinaryGraph\"]=wasmExports[\"Ye\"];_changeTextGraph=Module[\"_changeTextGraph\"]=wasmExports[\"Ze\"];_processGl=Module[\"_processGl\"]=wasmExports[\"_e\"];_process=Module[\"_process\"]=wasmExports[\"$e\"];_bindTextureToCanvas=Module[\"_bindTextureToCanvas\"]=wasmExports[\"af\"];_requestShaderRefreshOnGraphChange=Module[\"_requestShaderRefreshOnGraphChange\"]=wasmExports[\"bf\"];_waitUntilIdle=Module[\"_waitUntilIdle\"]=wasmExports[\"cf\"];_closeGraph=Module[\"_closeGraph\"]=wasmExports[\"df\"];_setAutoRenderToScreen=Module[\"_setAutoRenderToScreen\"]=wasmExports[\"ef\"];_emscripten_builtin_memalign=wasmExports[\"ff\"];_memalign=wasmExports[\"gf\"];__emscripten_tempret_set=wasmExports[\"hf\"];__emscripten_stack_restore=wasmExports[\"jf\"];__emscripten_stack_alloc=wasmExports[\"kf\"];_emscripten_stack_get_current=wasmExports[\"lf\"];dynCall_ji=wasmExports[\"dynCall_ji\"];dynCall_jii=wasmExports[\"dynCall_jii\"];dynCall_iiiijij=wasmExports[\"dynCall_iiiijij\"];dynCall_viiji=wasmExports[\"dynCall_viiji\"];dynCall_viji=wasmExports[\"dynCall_viji\"];dynCall_iiiji=wasmExports[\"dynCall_iiiji\"];dynCall_jjj=wasmExports[\"dynCall_jjj\"];dynCall_iiiijj=wasmExports[\"dynCall_iiiijj\"];dynCall_viijj=wasmExports[\"dynCall_viijj\"];dynCall_viiijjj=wasmExports[\"dynCall_viiijjj\"];dynCall_vij=wasmExports[\"dynCall_vij\"];dynCall_viijii=wasmExports[\"dynCall_viijii\"];dynCall_viiiji=wasmExports[\"dynCall_viiiji\"];dynCall_vijjj=wasmExports[\"dynCall_vijjj\"];dynCall_vj=wasmExports[\"dynCall_vj\"];dynCall_viij=wasmExports[\"dynCall_viij\"];dynCall_jiji=wasmExports[\"dynCall_jiji\"];dynCall_iiiiij=wasmExports[\"dynCall_iiiiij\"];dynCall_iiiiijj=wasmExports[\"dynCall_iiiiijj\"];dynCall_iiiiiijj=wasmExports[\"dynCall_iiiiiijj\"];memory=wasmMemory=wasmExports[\"hd\"];__indirect_function_table=wasmTable=wasmExports[\"jd\"]}var _kVersionStampBuildChangelistStr=Module[\"_kVersionStampBuildChangelistStr\"]=1024;var _kVersionStampCitcSnapshotStr=Module[\"_kVersionStampCitcSnapshotStr\"]=1056;var _kVersionStampCitcWorkspaceIdStr=Module[\"_kVersionStampCitcWorkspaceIdStr\"]=1088;var _kVersionStampSourceUriStr=Module[\"_kVersionStampSourceUriStr\"]=1600;var _kVersionStampBuildClientStr=Module[\"_kVersionStampBuildClientStr\"]=2112;var _kVersionStampBuildClientMintStatusStr=Module[\"_kVersionStampBuildClientMintStatusStr\"]=2624;var _kVersionStampBuildCompilerStr=Module[\"_kVersionStampBuildCompilerStr\"]=2656;var _kVersionStampBuildDateTimePstStr=Module[\"_kVersionStampBuildDateTimePstStr\"]=3168;var _kVersionStampBuildDepotPathStr=Module[\"_kVersionStampBuildDepotPathStr\"]=3200;var _kVersionStampBuildIdStr=Module[\"_kVersionStampBuildIdStr\"]=3712;var _kVersionStampBuildInfoStr=Module[\"_kVersionStampBuildInfoStr\"]=4224;var _kVersionStampBuildLabelStr=Module[\"_kVersionStampBuildLabelStr\"]=4736;var _kVersionStampBuildTargetStr=Module[\"_kVersionStampBuildTargetStr\"]=5248;var _kVersionStampBuildTimestampStr=Module[\"_kVersionStampBuildTimestampStr\"]=5760;var _kVersionStampBuildToolStr=Module[\"_kVersionStampBuildToolStr\"]=5792;var _kVersionStampG3BuildTargetStr=Module[\"_kVersionStampG3BuildTargetStr\"]=6304;var _kVersionStampVerifiableStr=Module[\"_kVersionStampVerifiableStr\"]=6816;var _kVersionStampBuildFdoTypeStr=Module[\"_kVersionStampBuildFdoTypeStr\"]=6848;var _kVersionStampBuildBaselineChangelistStr=Module[\"_kVersionStampBuildBaselineChangelistStr\"]=6880;var _kVersionStampBuildLtoTypeStr=Module[\"_kVersionStampBuildLtoTypeStr\"]=6912;var _kVersionStampBuildPropellerTypeStr=Module[\"_kVersionStampBuildPropellerTypeStr\"]=6944;var _kVersionStampBuildPghoTypeStr=Module[\"_kVersionStampBuildPghoTypeStr\"]=6976;var _kVersionStampBuildUsernameStr=Module[\"_kVersionStampBuildUsernameStr\"]=7008;var _kVersionStampBuildHostnameStr=Module[\"_kVersionStampBuildHostnameStr\"]=7520;var _kVersionStampBuildDirectoryStr=Module[\"_kVersionStampBuildDirectoryStr\"]=8032;var _kVersionStampBuildChangelistInt=Module[\"_kVersionStampBuildChangelistInt\"]=8544;var _kVersionStampCitcSnapshotInt=Module[\"_kVersionStampCitcSnapshotInt\"]=8552;var _kVersionStampBuildClientMintStatusInt=Module[\"_kVersionStampBuildClientMintStatusInt\"]=8556;var _kVersionStampBuildTimestampInt=Module[\"_kVersionStampBuildTimestampInt\"]=8560;var _kVersionStampVerifiableInt=Module[\"_kVersionStampVerifiableInt\"]=8568;var _kVersionStampBuildCoverageEnabledInt=Module[\"_kVersionStampBuildCoverageEnabledInt\"]=8572;var _kVersionStampBuildBaselineChangelistInt=Module[\"_kVersionStampBuildBaselineChangelistInt\"]=8576;var _kVersionStampPrecookedTimestampStr=Module[\"_kVersionStampPrecookedTimestampStr\"]=8592;var _kVersionStampPrecookedClientInfoStr=Module[\"_kVersionStampPrecookedClientInfoStr\"]=9104;var wasmImports={gd:BeginGlQueryTiming,fd:EndGlQueryTiming,ed:GetAdapterArchitecture,dd:GetAdapterDescription,cd:GetAdapterDeviceName,bd:GetAdapterVendor,ad:JsOnEmptyPacketListener,$c:JsOnFloat32ArrayImageListener,_c:JsOnFloat32ArrayImageVectorListener,ob:JsOnSimpleListenerBinaryArray,Zc:JsOnSimpleListenerBool,Yc:JsOnSimpleListenerDouble,Xc:JsOnSimpleListenerFloat,Wc:JsOnSimpleListenerInt,Vc:JsOnSimpleListenerString,Uc:JsOnSimpleListenerUint,Tc:JsOnUint8ArrayImageListener,Sc:JsOnUint8ArrayImageVectorListener,O:JsOnVectorFinishedListener,Rc:JsOnVectorListenerBool,Qc:JsOnVectorListenerDouble,Pc:JsOnVectorListenerFloat,Oc:JsOnVectorListenerInt,Nc:JsOnVectorListenerProto,Mc:JsOnVectorListenerString,Lc:JsOnVectorListenerUint,Kc:JsOnWebGLTextureListener,Jc:JsOnWebGLTextureVectorListener,Oa:JsWrapErrorListener,nb:JsWrapImageConverter,u:JsWrapSimpleListeners,mb:UseBottomLeftGpuOrigin,xb:__asyncjs__mediapipe_map_buffer_jspi,o:___cxa_throw,Ic:___syscall_dup,Hc:___syscall_faccessat,lb:___syscall_fcntl64,Gc:___syscall_fstat64,Lb:___syscall_ftruncate64,Fc:___syscall_ioctl,Ec:___syscall_lstat64,Dc:___syscall_newfstatat,kb:___syscall_openat,Cc:___syscall_stat64,xc:__abort_js,Ib:__embind_register_bigint,wc:__embind_register_bool,vc:__embind_register_emval,ib:__embind_register_float,I:__embind_register_integer,r:__embind_register_memory_view,uc:__embind_register_std_string,La:__embind_register_std_wstring,tc:__embind_register_void,$:__emval_create_invoker,q:__emval_decref,Ka:__emval_get_global,hb:__emval_get_property,ja:__emval_incref,Ja:__emval_instanceof,_:__emval_invoke,ua:__emval_new_cstring,Z:__emval_run_destructors,gb:__emval_set_property,sc:__emval_typeof,Hb:__gmtime_js,Gb:__localtime_js,Fb:__mktime_js,Eb:__mmap_js,Db:__munmap_js,rc:__tzset_js,Kb:_clock_time_get,qc:custom_emscripten_dbgn,Y:_emscripten_asm_const_int,fb:_emscripten_asm_const_ptr,Ia:_emscripten_errn,pc:_emscripten_get_heap_max,y:_emscripten_get_now,ia:_emscripten_has_asyncify,oc:_emscripten_outn,nc:_emscripten_pc_get_function,mc:_emscripten_resize_heap,eb:_emscripten_stack_snapshot,lc:_emscripten_stack_unwind_buffer,kc:_emscripten_webgl_create_context,jc:_emscripten_webgl_destroy_context,ic:_emscripten_webgl_get_context_attributes,ha:_emscripten_webgl_get_current_context,hc:_emscripten_webgl_make_context_current,Q:_emscripten_webgpu_get_device,gc:_emwgpuBufferDestroy,fc:_emwgpuBufferGetMappedRange,ec:_emwgpuBufferUnmap,x:_emwgpuDelete,dc:_emwgpuDeviceCreateBuffer,Cb:_emwgpuDeviceCreateComputePipelineAsync,Bb:_emwgpuDeviceCreateRenderPipelineAsync,cc:_emwgpuDeviceCreateShaderModule,bc:_emwgpuDeviceDestroy,ac:_emwgpuWaitAny,Bc:_environ_get,Ac:_environ_sizes_get,db:_exit,Na:_fd_close,jb:_fd_read,Jb:_fd_seek,Ma:_fd_write,b:_glActiveTexture,ta:_glAttachShader,$b:_glBindAttribLocation,c:_glBindBuffer,cb:_glBindBufferBase,t:_glBindFramebuffer,a:_glBindTexture,m:_glBindVertexArray,bb:_glBlendEquation,_b:_glBlendFunc,j:_glBufferData,H:_glClear,X:_glClearColor,da:_glClientWaitSync,ga:_glColorMask,ab:_glCompileShader,$a:_glCreateProgram,_a:_glCreateShader,p:_glDeleteBuffers,P:_glDeleteFramebuffers,h:_glDeleteProgram,sa:_glDeleteShader,ra:_glDeleteSync,D:_glDeleteTextures,A:_glDeleteVertexArrays,Za:_glDetachShader,G:_glDisable,n:_glDisableVertexAttribArray,i:_glDrawArrays,fa:_glDrawBuffers,Zb:_glEnable,l:_glEnableVertexAttribArray,Ya:_glFenceSync,qa:_glFinish,v:_glFlush,C:_glFramebufferTexture2D,Xa:_glFramebufferTextureLayer,s:_glGenBuffers,W:_glGenFramebuffers,F:_glGenTextures,B:_glGenVertexArrays,Wa:_glGetAttribLocation,ea:_glGetError,Yb:_glGetFloatv,w:_glGetIntegerv,Xb:_glGetProgramiv,Wb:_glGetShaderInfoLog,Vb:_glGetShaderiv,N:_glGetString,Ub:_glGetUniformBlockIndex,d:_glGetUniformLocation,Tb:_glLineWidth,Va:_glLinkProgram,pa:_glPixelStorei,oa:_glReadPixels,Ua:_glShaderSource,E:_glTexImage2D,na:_glTexParameterf,Ta:_glTexParameterfv,f:_glTexParameteri,ma:_glTexStorage2D,Sb:_glTexStorage3D,V:_glTexSubImage2D,Rb:_glTexSubImage3D,M:_glUniform1f,la:_glUniform1fv,e:_glUniform1i,U:_glUniform2f,Qb:_glUniform2fv,Ha:_glUniform3f,Sa:_glUniform4f,T:_glUniform4fv,Pb:_glUniform4iv,Ob:_glUniformBlockBinding,Nb:_glUniformMatrix2fv,Mb:_glUniformMatrix3fv,Ga:_glUniformMatrix4fv,g:_glUseProgram,k:_glVertexAttribPointer,S:_glViewport,Ab:mediapipe_create_utility_canvas2d,zb:_mediapipe_find_canvas_event_target,yb:mediapipe_import_external_texture,wb:_mediapipe_webgl_tex_image_drawable,zc:_proc_exit,yc:_random_get,Fa:_wgpuCommandEncoderBeginComputePass,Ea:_wgpuCommandEncoderBeginRenderPass,vb:_wgpuCommandEncoderCopyBufferToTexture,ub:_wgpuCommandEncoderCopyTextureToBuffer,tb:_wgpuCommandEncoderCopyTextureToTexture,L:_wgpuCommandEncoderFinish,Da:_wgpuComputePassEncoderDispatchWorkgroups,Ca:_wgpuComputePassEncoderEnd,Ba:_wgpuComputePassEncoderSetBindGroup,Aa:_wgpuComputePassEncoderSetPipeline,za:_wgpuComputePipelineGetBindGroupLayout,ca:_wgpuDeviceCreateBindGroup,sb:_wgpuDeviceCreateBindGroupLayout,K:_wgpuDeviceCreateCommandEncoder,rb:_wgpuDeviceCreateComputePipeline,qb:_wgpuDeviceCreatePipelineLayout,Ra:_wgpuDeviceCreateRenderPipeline,R:_wgpuDeviceCreateSampler,ba:_wgpuDeviceCreateTexture,J:_wgpuQueueSubmit,ka:_wgpuQueueWriteBuffer,pb:_wgpuQueueWriteTexture,ya:_wgpuRenderPassEncoderDraw,xa:_wgpuRenderPassEncoderEnd,wa:_wgpuRenderPassEncoderSetBindGroup,va:_wgpuRenderPassEncoderSetPipeline,Qa:_wgpuRenderPipelineGetBindGroupLayout,z:_wgpuTextureCreateView,Pa:_wgpuTextureDestroy,aa:_wgpuTextureGetFormat};function run(){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module[\"calledRun\"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module[\"onRuntimeInitialized\"]?.();postRun()}if(Module[\"setStatus\"]){Module[\"setStatus\"](\"Running...\");setTimeout(()=>{setTimeout(()=>Module[\"setStatus\"](\"\"),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}\n;return moduleRtn}})();if(typeof exports===\"object\"&&typeof module===\"object\"){module.exports=ModuleFactory;module.exports.default=ModuleFactory}else if(typeof define===\"function\"&&define[\"amd\"])define([],()=>ModuleFactory);\n"
  },
  {
    "path": "src/assets/satoshi.css",
    "content": "/**\n * @license\n *\n * Font Family: Satoshi\n * Designed by: Deni Anggara\n * URL: https://www.fontshare.com/fonts/satoshi\n * © 2023 Indian Type Foundry\n *\n * Font Styles:\n * Satoshi Variable(Variable font)\n * Satoshi Variable Italic(Variable font)\n * Satoshi Light\n * Satoshi Light Italic\n * Satoshi Regular\n * Satoshi Italic\n * Satoshi Medium\n * Satoshi Medium Italic\n * Satoshi Bold\n * Satoshi Bold Italic\n * Satoshi Black\n * Satoshi Black Italic\n *\n*/\n\n\n/**\n* This is a variable font\n* You can controll variable axes as shown below:\n* font-variation-settings: 'wght' 900.0;\n*\n* available axes:\n\n* 'wght' (range from 300.0 to 900.0)\n\n*/\n\n@font-face {\n  font-family: 'Satoshi-Variable';\n  src: url('../fonts/Satoshi-Variable.woff2') format('woff2'),\n       url('../fonts/Satoshi-Variable.woff') format('woff'),\n       url('../fonts/Satoshi-Variable.ttf') format('truetype');\n       font-weight: 300 900;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n/**\n* This is a variable font\n* You can controll variable axes as shown below:\n* font-variation-settings: 'wght' 900.0;\n*\n* available axes:\n\n* 'wght' (range from 300.0 to 900.0)\n\n*/\n\n@font-face {\n  font-family: 'Satoshi-VariableItalic';\n  src: url('../fonts/Satoshi-VariableItalic.woff2') format('woff2'),\n       url('../fonts/Satoshi-VariableItalic.woff') format('woff'),\n       url('../fonts/Satoshi-VariableItalic.ttf') format('truetype');\n       font-weight: 300 900;\n       font-display: swap;\n       font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Light';\n  src: url('../fonts/Satoshi-Light.woff2') format('woff2'),\n       url('../fonts/Satoshi-Light.woff') format('woff'),\n       url('../fonts/Satoshi-Light.ttf') format('truetype');\n       font-weight: 300;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-LightItalic';\n  src: url('../fonts/Satoshi-LightItalic.woff2') format('woff2'),\n       url('../fonts/Satoshi-LightItalic.woff') format('woff'),\n       url('../fonts/Satoshi-LightItalic.ttf') format('truetype');\n       font-weight: 300;\n       font-display: swap;\n       font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Regular';\n  src: url('../fonts/Satoshi-Regular.woff2') format('woff2'),\n       url('../fonts/Satoshi-Regular.woff') format('woff'),\n       url('../fonts/Satoshi-Regular.ttf') format('truetype');\n       font-weight: 400;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Italic';\n  src: url('../fonts/Satoshi-Italic.woff2') format('woff2'),\n       url('../fonts/Satoshi-Italic.woff') format('woff'),\n       url('../fonts/Satoshi-Italic.ttf') format('truetype');\n       font-weight: 400;\n       font-display: swap;\n       font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Medium';\n  src: url('../fonts/Satoshi-Medium.woff2') format('woff2'),\n       url('../fonts/Satoshi-Medium.woff') format('woff'),\n       url('../fonts/Satoshi-Medium.ttf') format('truetype');\n       font-weight: 500;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-MediumItalic';\n  src: url('../fonts/Satoshi-MediumItalic.woff2') format('woff2'),\n       url('../fonts/Satoshi-MediumItalic.woff') format('woff'),\n       url('../fonts/Satoshi-MediumItalic.ttf') format('truetype');\n       font-weight: 500;\n       font-display: swap;\n       font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Bold';\n  src: url('../fonts/Satoshi-Bold.woff2') format('woff2'),\n       url('../fonts/Satoshi-Bold.woff') format('woff'),\n       url('../fonts/Satoshi-Bold.ttf') format('truetype');\n       font-weight: 700;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-BoldItalic';\n  src: url('../fonts/Satoshi-BoldItalic.woff2') format('woff2'),\n       url('../fonts/Satoshi-BoldItalic.woff') format('woff'),\n       url('../fonts/Satoshi-BoldItalic.ttf') format('truetype');\n       font-weight: 700;\n       font-display: swap;\n       font-style: italic;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-Black';\n  src: url('../fonts/Satoshi-Black.woff2') format('woff2'),\n       url('../fonts/Satoshi-Black.woff') format('woff'),\n       url('../fonts/Satoshi-Black.ttf') format('truetype');\n       font-weight: 900;\n       font-display: swap;\n       font-style: normal;\n}\n\n\n@font-face {\n  font-family: 'Satoshi-BlackItalic';\n  src: url('../fonts/Satoshi-BlackItalic.woff2') format('woff2'),\n       url('../fonts/Satoshi-BlackItalic.woff') format('woff'),\n       url('../fonts/Satoshi-BlackItalic.ttf') format('truetype');\n       font-weight: 900;\n       font-display: swap;\n       font-style: italic;\n}\n\n"
  },
  {
    "path": "src/assets/vendor/ffmpeg-core.js",
    "content": "\nvar createFFmpegCore = (function() {\n  var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;\n  if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;\n  return (\nfunction(createFFmpegCore) {\n  createFFmpegCore = createFFmpegCore || {};\n\n\nvar f;f||(f=typeof createFFmpegCore !== 'undefined' ? createFFmpegCore : {});var ba,ca;f.ready=new Promise(function(a,b){ba=a;ca=b});var da={},ea;for(ea in f)f.hasOwnProperty(ea)&&(da[ea]=f[ea]);var fa=[],ha=\"./this.program\";function ja(a,b){throw b;}var ka=!1,la=!1,h=!1,ma=!1;ka=\"object\"===typeof window;la=\"function\"===typeof importScripts;h=\"object\"===typeof process&&\"object\"===typeof process.versions&&\"string\"===typeof process.versions.node;ma=!ka&&!h&&!la;var l=f.ENVIRONMENT_IS_PTHREAD||!1;\nl&&(oa=f.buffer);var pa=\"\";function qa(a){return f.locateFile?f.locateFile(a,pa):pa+a}var ra,sa,ta,va;\nif(h){pa=la?require(\"path\").dirname(pa)+\"/\":__dirname+\"/\";ra=function(a,b){ta||(ta=require(\"fs\"));va||(va=require(\"path\"));a=va.normalize(a);return ta.readFileSync(a,b?null:\"utf8\")};sa=function(a){a=ra(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a};1<process.argv.length&&(ha=process.argv[1].replace(/\\\\/g,\"/\"));fa=process.argv.slice(2);process.on(\"uncaughtException\",function(a){if(!(a instanceof wa))throw a;});process.on(\"unhandledRejection\",n);ja=function(a){process.exit(a)};f.inspect=\nfunction(){return\"[Emscripten Module object]\"};var xa;try{xa=require(\"worker_threads\")}catch(a){throw console.error('The \"worker_threads\" module is not supported in this node.js build - perhaps a newer version is needed?'),a;}global.Worker=xa.Worker}else if(ma)\"undefined\"!=typeof read&&(ra=function(a){return read(a)}),sa=function(a){if(\"function\"===typeof readbuffer)return new Uint8Array(readbuffer(a));a=read(a,\"binary\");assert(\"object\"===typeof a);return a},\"undefined\"!=typeof scriptArgs?fa=scriptArgs:\n\"undefined\"!=typeof arguments&&(fa=arguments),\"function\"===typeof quit&&(ja=function(a){quit(a)}),\"undefined\"!==typeof print&&(\"undefined\"===typeof console&&(console={}),console.log=print,console.warn=console.error=\"undefined\"!==typeof printErr?printErr:print);else if(ka||la)la?pa=self.location.href:\"undefined\"!==typeof document&&document.currentScript&&(pa=document.currentScript.src),_scriptDir&&(pa=_scriptDir),0!==pa.indexOf(\"blob:\")?pa=pa.substr(0,pa.lastIndexOf(\"/\")+1):pa=\"\",h?(ra=function(a,\nb){ta||(ta=require(\"fs\"));va||(va=require(\"path\"));a=va.normalize(a);return ta.readFileSync(a,b?null:\"utf8\")},sa=function(a){a=ra(a,!0);a.buffer||(a=new Uint8Array(a));assert(a.buffer);return a}):(ra=function(a){var b=new XMLHttpRequest;b.open(\"GET\",a,!1);b.send(null);return b.responseText},la&&(sa=function(a){var b=new XMLHttpRequest;b.open(\"GET\",a,!1);b.responseType=\"arraybuffer\";b.send(null);return new Uint8Array(b.response)}));h&&\"undefined\"===typeof performance&&(global.performance=require(\"perf_hooks\").performance);\nvar ya=f.print||console.log.bind(console),u=f.printErr||console.warn.bind(console);for(ea in da)da.hasOwnProperty(ea)&&(f[ea]=da[ea]);da=null;f.arguments&&(fa=f.arguments);f.thisProgram&&(ha=f.thisProgram);f.quit&&(ja=f.quit);var za,Aa=0,Ba;f.wasmBinary&&(Ba=f.wasmBinary);var noExitRuntime;f.noExitRuntime&&(noExitRuntime=f.noExitRuntime);\"object\"!==typeof WebAssembly&&n(\"no native wasm support detected\");var Ca,Da,threadInfoStruct=0,selfThreadId=0,Ea=!1;\nfunction assert(a,b){a||n(\"Assertion failed: \"+b)}function Fa(a){var b=f[\"_\"+a];assert(b,\"Cannot call unknown function \"+a+\", make sure it is exported\");return b}\nfunction Ga(a,b,c,d){var e={string:function(q){var t=0;if(null!==q&&void 0!==q&&0!==q){var w=(q.length<<2)+1;t=Ha(w);Ia(q,v,t,w)}return t},array:function(q){var t=Ha(q.length);y.set(q,t);return t}},g=Fa(a),k=[];a=0;if(d)for(var m=0;m<d.length;m++){var r=e[c[m]];r?(0===a&&(a=A()),k[m]=r(d[m])):k[m]=d[m]}c=g.apply(null,k);c=\"string\"===b?C(c):\"boolean\"===b?!!c:c;0!==a&&D(a);return c}\nfunction Ja(a,b,c){c=b+c;for(var d=\"\";!(b>=c);){var e=a[b++];if(!e)break;if(e&128){var g=a[b++]&63;if(192==(e&224))d+=String.fromCharCode((e&31)<<6|g);else{var k=a[b++]&63;e=224==(e&240)?(e&15)<<12|g<<6|k:(e&7)<<18|g<<12|k<<6|a[b++]&63;65536>e?d+=String.fromCharCode(e):(e-=65536,d+=String.fromCharCode(55296|e>>10,56320|e&1023))}}else d+=String.fromCharCode(e)}return d}function C(a,b){return a?Ja(v,a,b):\"\"}\nfunction Ia(a,b,c,d){if(!(0<d))return 0;var e=c;d=c+d-1;for(var g=0;g<a.length;++g){var k=a.charCodeAt(g);if(55296<=k&&57343>=k){var m=a.charCodeAt(++g);k=65536+((k&1023)<<10)|m&1023}if(127>=k){if(c>=d)break;b[c++]=k}else{if(2047>=k){if(c+1>=d)break;b[c++]=192|k>>6}else{if(65535>=k){if(c+2>=d)break;b[c++]=224|k>>12}else{if(c+3>=d)break;b[c++]=240|k>>18;b[c++]=128|k>>12&63}b[c++]=128|k>>6&63}b[c++]=128|k&63}}b[c]=0;return c-e}\nfunction Ka(a){for(var b=0,c=0;c<a.length;++c){var d=a.charCodeAt(c);55296<=d&&57343>=d&&(d=65536+((d&1023)<<10)|a.charCodeAt(++c)&1023);127>=d?++b:b=2047>=d?b+2:65535>=d?b+3:b+4}return b}function La(a){var b=Ka(a)+1,c=Ma(b);c&&Ia(a,y,c,b);return c}function Na(a){var b=Ka(a)+1,c=Ha(b);Ia(a,y,c,b);return c}function Pa(a,b,c){for(var d=0;d<a.length;++d)y[b++>>0]=a.charCodeAt(d);c||(y[b>>0]=0)}var oa,y,v,Qa,Ra,E,F,G,Sa,Ta=f.INITIAL_MEMORY||2146435072;\nif(l)Ca=f.wasmMemory,oa=f.buffer;else if(f.wasmMemory)Ca=f.wasmMemory;else if(Ca=new WebAssembly.Memory({initial:Ta/65536,maximum:Ta/65536,shared:!0}),!(Ca.buffer instanceof SharedArrayBuffer))throw u(\"requested a shared WebAssembly.Memory but the returned buffer is not a SharedArrayBuffer, indicating that while the browser has SharedArrayBuffer it does not have WebAssembly threads support - you may need to set a flag\"),h&&console.log(\"(on node you may need: --experimental-wasm-threads --experimental-wasm-bulk-memory and also use a recent version)\"),\nError(\"bad memory\");Ca&&(oa=Ca.buffer);Ta=oa.byteLength;var Ua=oa;oa=Ua;f.HEAP8=y=new Int8Array(Ua);f.HEAP16=Qa=new Int16Array(Ua);f.HEAP32=E=new Int32Array(Ua);f.HEAPU8=v=new Uint8Array(Ua);f.HEAPU16=Ra=new Uint16Array(Ua);f.HEAPU32=F=new Uint32Array(Ua);f.HEAPF32=G=new Float32Array(Ua);f.HEAPF64=Sa=new Float64Array(Ua);var H,Va=[],Wa=[],Xa=[],Ya=[],Za=[];function $a(){var a=f.preRun.shift();Va.unshift(a)}var ab=0,bb=null,cb=null;\nfunction eb(){assert(!l,\"addRunDependency cannot be used in a pthread worker\");ab++;f.monitorRunDependencies&&f.monitorRunDependencies(ab)}function fb(){ab--;f.monitorRunDependencies&&f.monitorRunDependencies(ab);if(0==ab&&(null!==bb&&(clearInterval(bb),bb=null),cb)){var a=cb;cb=null;a()}}f.preloadedImages={};f.preloadedAudios={};\nfunction n(a){if(f.onAbort)f.onAbort(a);l&&console.error(\"Pthread aborting at \"+Error().stack);u(a);Ea=!0;a=new WebAssembly.RuntimeError(\"abort(\"+a+\"). Build with -s ASSERTIONS=1 for more info.\");ca(a);throw a;}function gb(a){var b=hb;return String.prototype.startsWith?b.startsWith(a):0===b.indexOf(a)}function ib(){return gb(\"data:application/octet-stream;base64,\")}var hb=\"ffmpeg-core.wasm\";ib()||(hb=qa(hb));\nfunction jb(){try{if(Ba)return new Uint8Array(Ba);if(sa)return sa(hb);throw\"both async and sync fetching of the wasm failed\";}catch(a){n(a)}}function kb(){return Ba||!ka&&!la||\"function\"!==typeof fetch||gb(\"file://\")?Promise.resolve().then(jb):fetch(hb,{credentials:\"same-origin\"}).then(function(a){if(!a.ok)throw\"failed to load wasm binary file at '\"+hb+\"'\";return a.arrayBuffer()}).catch(function(){return jb()})}\nvar J,L,mb={5244604:function(){throw\"Canceled!\";},5244824:function(a,b){setTimeout(function(){lb(a,b)},0)},5244926:function(){return 5242880}};function nb(a){for(;0<a.length;){var b=a.shift();if(\"function\"==typeof b)b(f);else{var c=b.uh;\"number\"===typeof c?void 0===b.Sf?H.get(c)():H.get(c)(b.Sf):c(void 0===b.Sf?null:b.Sf)}}}function ob(a){return a.replace(/\\b_Z[\\w\\d_]+/g,function(b){return b===b?b:b+\" [\"+b+\"]\"})}\nf.dynCall=function(a,b,c){var d;-1!=a.indexOf(\"j\")?d=c&&c.length?f[\"dynCall_\"+a].apply(null,[b].concat(c)):f[\"dynCall_\"+a].call(null,b):d=H.get(b).apply(null,c);return d};var pb=0,qb=0,rb=0;function sb(a,b,c){pb=a|0;rb=b|0;qb=c|0}f.registerPthreadPtr=sb;\nfunction tb(a,b){if(0>=a||a>y.length||a&1||0>b)return-28;if(0==b)return 0;2147483647<=b&&(b=Infinity);var c=Atomics.load(E,M.Uf>>2),d=0;if(c==a&&Atomics.compareExchange(E,M.Uf>>2,c,0)==c&&(--b,d=1,0>=b))return 1;a=Atomics.notify(E,a>>2,b);if(0<=a)return a+d;throw\"Atomics.notify returned an unexpected value \"+a;}f._emscripten_futex_wake=tb;\nfunction ub(a){if(l)throw\"Internal Error! cancelThread() can only ever be called from main application thread!\";if(!a)throw\"Internal Error! Null pthread_ptr in cancelThread!\";M.Df[a].worker.postMessage({cmd:\"cancel\"})}function vb(a){if(l)throw\"Internal Error! cleanupThread() can only ever be called from main application thread!\";if(!a)throw\"Internal Error! Null pthread_ptr in cleanupThread!\";E[a+12>>2]=0;(a=M.Df[a])&&M.zg(a.worker)}\nvar M={Oh:1,mj:{Hh:0,Ih:0},Ff:[],Jf:[],kj:function(){},oi:function(){M.wf=Ma(232);for(var a=0;58>a;++a)F[M.wf/4+a]=0;E[M.wf+12>>2]=M.wf;a=M.wf+156;E[a>>2]=a;var b=Ma(512);for(a=0;128>a;++a)F[b/4+a]=0;Atomics.store(F,M.wf+104>>2,b);Atomics.store(F,M.wf+40>>2,M.wf);Atomics.store(F,M.wf+44>>2,42);M.Bh();sb(M.wf,!la,1);wb(M.wf)},pi:function(){M.Bh();ba(f);M.receiveObjectTransfer=M.Hi;M.setThreadStatus=M.Ki;M.threadCancel=M.Oi;M.threadExit=M.Pi},Bh:function(){M.Uf=xb},Df:{},Cg:[],Ki:function(){},dh:function(){for(;0<\nM.Cg.length;)M.Cg.pop()();l&&threadInfoStruct&&yb()},Pi:function(a){var b=pb|0;b&&(Atomics.store(F,b+4>>2,a),Atomics.store(F,b+0>>2,1),Atomics.store(F,b+60>>2,1),Atomics.store(F,b+64>>2,0),M.dh(),tb(b+0,2147483647),sb(0,0,0),threadInfoStruct=0,l&&postMessage({cmd:\"exit\"}))},Oi:function(){M.dh();Atomics.store(F,threadInfoStruct+4>>2,-1);Atomics.store(F,threadInfoStruct+0>>2,1);tb(threadInfoStruct+0,2147483647);threadInfoStruct=selfThreadId=0;sb(0,0,0);postMessage({cmd:\"cancelDone\"})},Ni:function(){for(var a in M.Df){var b=\nM.Df[a];b&&b.worker&&M.zg(b.worker)}M.Df={};for(a=0;a<M.Ff.length;++a){var c=M.Ff[a];c.terminate()}M.Ff=[];for(a=0;a<M.Jf.length;++a)c=M.Jf[a],b=c.xf,M.Og(b),c.terminate();M.Jf=[]},Og:function(a){if(a){if(a.threadInfoStruct){var b=E[a.threadInfoStruct+104>>2];E[a.threadInfoStruct+104>>2]=0;zb(b);zb(a.threadInfoStruct)}a.threadInfoStruct=0;a.Jg&&a.Qf&&zb(a.Qf);a.Qf=0;a.worker&&(a.worker.xf=null)}},zg:function(a){delete M.Df[a.xf.Kh];M.Ff.push(a);M.Jf.splice(M.Jf.indexOf(a),1);M.Og(a.xf);a.xf=void 0},\nHi:function(){},ui:function(a,b){a.onmessage=function(c){var d=c.data,e=d.cmd;a.xf&&(M.Lg=a.xf.threadInfoStruct);if(d.targetThread&&d.targetThread!=(pb|0)){var g=M.Df[d.wj];g?g.worker.postMessage(c.data,d.transferList):console.error('Internal error! Worker sent a message \"'+e+'\" to target pthread '+d.targetThread+\", but that thread no longer exists!\")}else if(\"processQueuedMainThreadWork\"===e)Ab();else if(\"spawnThread\"===e)Bb(c.data);else if(\"cleanupThread\"===e)vb(d.thread);else if(\"killThread\"===\ne){c=d.thread;if(l)throw\"Internal Error! killThread() can only ever be called from main application thread!\";if(!c)throw\"Internal Error! Null pthread_ptr in killThread!\";E[c+12>>2]=0;c=M.Df[c];c.worker.terminate();M.Og(c);M.Jf.splice(M.Jf.indexOf(c.worker),1);c.worker.xf=void 0}else if(\"cancelThread\"===e)ub(d.thread);else if(\"loaded\"===e)a.loaded=!0,b&&b(a),a.ng&&(a.ng(),delete a.ng);else if(\"print\"===e)ya(\"Thread \"+d.threadId+\": \"+d.text);else if(\"printErr\"===e)u(\"Thread \"+d.threadId+\": \"+d.text);\nelse if(\"alert\"===e)alert(\"Thread \"+d.threadId+\": \"+d.text);else if(\"exit\"===e)a.xf&&Atomics.load(F,a.xf.Kh+68>>2)&&M.zg(a);else if(\"exitProcess\"===e){noExitRuntime=!1;try{Cb(d.returnCode)}catch(k){if(k instanceof wa)return;throw k;}}else\"cancelDone\"===e?M.zg(a):\"objectTransfer\"!==e&&(\"setimmediate\"===c.data.target?a.postMessage(c.data):u(\"worker sent an unknown command \"+e));M.Lg=void 0};a.onerror=function(c){u(\"pthread sent an error! \"+c.filename+\":\"+c.lineno+\": \"+c.message)};h&&(a.on(\"message\",\nfunction(c){a.onmessage({data:c})}),a.on(\"error\",function(c){a.onerror(c)}),a.on(\"exit\",function(){}));a.postMessage({cmd:\"load\",urlOrBlob:f.mainScriptUrlOrBlob||_scriptDir,wasmMemory:Ca,wasmModule:Da})},Vh:function(){var a=qa(\"ffmpeg-core.worker.js\");M.Ff.push(new Worker(a))},ki:function(){0==M.Ff.length&&(M.Vh(),M.ui(M.Ff[0]));return 0<M.Ff.length?M.Ff.pop():null},Yi:function(a){for(a=performance.now()+a;performance.now()<a;);}};f.establishStackSpace=function(a){D(a)};f.getNoExitRuntime=function(){return noExitRuntime};\nvar Db;h?Db=function(){var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:l?Db=function(){return performance.now()-f.__performance_now_clock_drift}:\"undefined\"!==typeof dateNow?Db=dateNow:Db=function(){return performance.now()};function Eb(a){return E[Fb()>>2]=a}function Gb(a,b){if(0===a)a=Date.now();else if(1===a||4===a)a=Db();else return Eb(28),-1;E[b>>2]=a/1E3|0;E[b+4>>2]=a%1E3*1E6|0;return 0}function Hb(a,b){if(l)return N(1,1,a,b);Ya.unshift({uh:a,Sf:b})}\nfunction Ib(a,b){a=new Date(1E3*E[a>>2]);E[b>>2]=a.getUTCSeconds();E[b+4>>2]=a.getUTCMinutes();E[b+8>>2]=a.getUTCHours();E[b+12>>2]=a.getUTCDate();E[b+16>>2]=a.getUTCMonth();E[b+20>>2]=a.getUTCFullYear()-1900;E[b+24>>2]=a.getUTCDay();E[b+36>>2]=0;E[b+32>>2]=0;E[b+28>>2]=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0;Ib.hh||(Ib.hh=La(\"GMT\"));E[b+40>>2]=Ib.hh;return b}\nfunction Jb(){function a(k){return(k=k.toTimeString().match(/\\(([A-Za-z ]+)\\)$/))?k[1]:\"GMT\"}if(l)return N(2,1);if(!Jb.Xh){Jb.Xh=!0;var b=(new Date).getFullYear(),c=new Date(b,0,1),d=new Date(b,6,1);b=c.getTimezoneOffset();var e=d.getTimezoneOffset(),g=Math.max(b,e);E[Kb()>>2]=60*g;E[Lb()>>2]=Number(b!=e);c=a(c);d=a(d);c=La(c);d=La(d);e<b?(E[Mb()>>2]=c,E[Mb()+4>>2]=d):(E[Mb()>>2]=d,E[Mb()+4>>2]=c)}}\nfunction Nb(a,b){Jb();a=new Date(1E3*E[a>>2]);E[b>>2]=a.getSeconds();E[b+4>>2]=a.getMinutes();E[b+8>>2]=a.getHours();E[b+12>>2]=a.getDate();E[b+16>>2]=a.getMonth();E[b+20>>2]=a.getFullYear()-1900;E[b+24>>2]=a.getDay();var c=new Date(a.getFullYear(),0,1);E[b+28>>2]=(a.getTime()-c.getTime())/864E5|0;E[b+36>>2]=-(60*a.getTimezoneOffset());var d=(new Date(a.getFullYear(),6,1)).getTimezoneOffset();c=c.getTimezoneOffset();a=(d!=c&&a.getTimezoneOffset()==Math.min(c,d))|0;E[b+32>>2]=a;a=E[Mb()+(a?4:0)>>2];\nE[b+40>>2]=a;return b}function Ob(a,b){for(var c=0,d=a.length-1;0<=d;d--){var e=a[d];\".\"===e?a.splice(d,1):\"..\"===e?(a.splice(d,1),c++):c&&(a.splice(d,1),c--)}if(b)for(;c;c--)a.unshift(\"..\");return a}function Pb(a){var b=\"/\"===a.charAt(0),c=\"/\"===a.substr(-1);(a=Ob(a.split(\"/\").filter(function(d){return!!d}),!b).join(\"/\"))||b||(a=\".\");a&&c&&(a+=\"/\");return(b?\"/\":\"\")+a}\nfunction Qb(a){var b=/^(\\/?|)([\\s\\S]*?)((?:\\.{1,2}|[^\\/]+?|)(\\.[^.\\/]*|))(?:[\\/]*)$/.exec(a).slice(1);a=b[0];b=b[1];if(!a&&!b)return\".\";b&&(b=b.substr(0,b.length-1));return a+b}function Rb(a){if(\"/\"===a)return\"/\";a=Pb(a);a=a.replace(/\\/$/,\"\");var b=a.lastIndexOf(\"/\");return-1===b?a:a.substr(b+1)}function Sb(a,b){return Pb(a+\"/\"+b)}\nfunction Tb(){if(\"object\"===typeof crypto&&\"function\"===typeof crypto.getRandomValues){var a=new Uint8Array(1);return function(){crypto.getRandomValues(a);return a[0]}}if(h)try{var b=require(\"crypto\");return function(){return b.randomBytes(1)[0]}}catch(c){}return function(){n(\"randomDevice\")}}\nfunction Ub(){for(var a=\"\",b=!1,c=arguments.length-1;-1<=c&&!b;c--){b=0<=c?arguments[c]:O.cwd();if(\"string\"!==typeof b)throw new TypeError(\"Arguments to path.resolve must be strings\");if(!b)return\"\";a=b+\"/\"+a;b=\"/\"===b.charAt(0)}a=Ob(a.split(\"/\").filter(function(d){return!!d}),!b).join(\"/\");return(b?\"/\":\"\")+a||\".\"}\nfunction Vb(a,b){function c(k){for(var m=0;m<k.length&&\"\"===k[m];m++);for(var r=k.length-1;0<=r&&\"\"===k[r];r--);return m>r?[]:k.slice(m,r-m+1)}a=Ub(a).substr(1);b=Ub(b).substr(1);a=c(a.split(\"/\"));b=c(b.split(\"/\"));for(var d=Math.min(a.length,b.length),e=d,g=0;g<d;g++)if(a[g]!==b[g]){e=g;break}d=[];for(g=e;g<a.length;g++)d.push(\"..\");d=d.concat(b.slice(e));return d.join(\"/\")}var Wb=[];function Xb(a,b){Wb[a]={input:[],output:[],Xf:b};O.bh(a,Yb)}\nvar Yb={open:function(a){var b=Wb[a.node.rdev];if(!b)throw new O.$e(43);a.tty=b;a.seekable=!1},close:function(a){a.tty.Xf.flush(a.tty)},flush:function(a){a.tty.Xf.flush(a.tty)},read:function(a,b,c,d){if(!a.tty||!a.tty.Xf.wh)throw new O.$e(60);for(var e=0,g=0;g<d;g++){try{var k=a.tty.Xf.wh(a.tty)}catch(m){throw new O.$e(29);}if(void 0===k&&0===e)throw new O.$e(6);if(null===k||void 0===k)break;e++;b[c+g]=k}e&&(a.node.timestamp=Date.now());return e},write:function(a,b,c,d){if(!a.tty||!a.tty.Xf.Yg)throw new O.$e(60);\ntry{for(var e=0;e<d;e++)a.tty.Xf.Yg(a.tty,b[c+e])}catch(g){throw new O.$e(29);}d&&(a.node.timestamp=Date.now());return e}},cc={wh:function(a){if(!a.input.length){var b=null;if(h){var c=Buffer.Rf?Buffer.Rf(256):new Buffer(256),d=0;try{d=ta.readSync(process.stdin.fd,c,0,256,null)}catch(e){if(-1!=e.toString().indexOf(\"EOF\"))d=0;else throw e;}0<d?b=c.slice(0,d).toString(\"utf-8\"):b=null}else\"undefined\"!=typeof window&&\"function\"==typeof window.prompt?(b=window.prompt(\"Input: \"),null!==b&&(b+=\"\\n\")):\"function\"==\ntypeof readline&&(b=readline(),null!==b&&(b+=\"\\n\"));if(!b)return null;a.input=Zb(b,!0)}return a.input.shift()},Yg:function(a,b){null===b||10===b?(ya(Ja(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(ya(Ja(a.output,0)),a.output=[])}},dc={Yg:function(a,b){null===b||10===b?(u(Ja(a.output,0)),a.output=[]):0!=b&&a.output.push(b)},flush:function(a){a.output&&0<a.output.length&&(u(Ja(a.output,0)),a.output=[])}},P={Cf:null,hf:function(){return P.createNode(null,\n\"/\",16895,0)},createNode:function(a,b,c,d){if(O.ri(c)||O.isFIFO(c))throw new O.$e(63);P.Cf||(P.Cf={dir:{node:{zf:P.bf.zf,mf:P.bf.mf,lookup:P.bf.lookup,Ef:P.bf.Ef,rename:P.bf.rename,unlink:P.bf.unlink,rmdir:P.bf.rmdir,readdir:P.bf.readdir,symlink:P.bf.symlink},stream:{sf:P.cf.sf}},file:{node:{zf:P.bf.zf,mf:P.bf.mf},stream:{sf:P.cf.sf,read:P.cf.read,write:P.cf.write,eg:P.cf.eg,Vf:P.cf.Vf,Wf:P.cf.Wf}},link:{node:{zf:P.bf.zf,mf:P.bf.mf,readlink:P.bf.readlink},stream:{}},kh:{node:{zf:P.bf.zf,mf:P.bf.mf},\nstream:O.Zh}});c=O.createNode(a,b,c,d);O.jf(c.mode)?(c.bf=P.Cf.dir.node,c.cf=P.Cf.dir.stream,c.af={}):O.isFile(c.mode)?(c.bf=P.Cf.file.node,c.cf=P.Cf.file.stream,c.ff=0,c.af=null):O.Lf(c.mode)?(c.bf=P.Cf.link.node,c.cf=P.Cf.link.stream):O.gg(c.mode)&&(c.bf=P.Cf.kh.node,c.cf=P.Cf.kh.stream);c.timestamp=Date.now();a&&(a.af[b]=c);return c},fj:function(a){if(a.af&&a.af.subarray){for(var b=[],c=0;c<a.ff;++c)b.push(a.af[c]);return b}return a.af},gj:function(a){return a.af?a.af.subarray?a.af.subarray(0,\na.ff):new Uint8Array(a.af):new Uint8Array(0)},rh:function(a,b){var c=a.af?a.af.length:0;c>=b||(b=Math.max(b,c*(1048576>c?2:1.125)>>>0),0!=c&&(b=Math.max(b,256)),c=a.af,a.af=new Uint8Array(b),0<a.ff&&a.af.set(c.subarray(0,a.ff),0))},Ii:function(a,b){if(a.ff!=b)if(0==b)a.af=null,a.ff=0;else{if(!a.af||a.af.subarray){var c=a.af;a.af=new Uint8Array(b);c&&a.af.set(c.subarray(0,Math.min(b,a.ff)))}else if(a.af||(a.af=[]),a.af.length>b)a.af.length=b;else for(;a.af.length<b;)a.af.push(0);a.ff=b}},bf:{zf:function(a){var b=\n{};b.dev=O.gg(a.mode)?a.id:1;b.ino=a.id;b.mode=a.mode;b.nlink=1;b.uid=0;b.gid=0;b.rdev=a.rdev;O.jf(a.mode)?b.size=4096:O.isFile(a.mode)?b.size=a.ff:O.Lf(a.mode)?b.size=a.link.length:b.size=0;b.atime=new Date(a.timestamp);b.mtime=new Date(a.timestamp);b.ctime=new Date(a.timestamp);b.Wh=4096;b.blocks=Math.ceil(b.size/b.Wh);return b},mf:function(a,b){void 0!==b.mode&&(a.mode=b.mode);void 0!==b.timestamp&&(a.timestamp=b.timestamp);void 0!==b.size&&P.Ii(a,b.size)},lookup:function(){throw O.Pg[44];},Ef:function(a,\nb,c,d){return P.createNode(a,b,c,d)},rename:function(a,b,c){if(O.jf(a.mode)){try{var d=O.Af(b,c)}catch(g){}if(d)for(var e in d.af)throw new O.$e(55);}delete a.parent.af[a.name];a.name=c;b.af[c]=a;a.parent=b},unlink:function(a,b){delete a.af[b]},rmdir:function(a,b){var c=O.Af(a,b),d;for(d in c.af)throw new O.$e(55);delete a.af[b]},readdir:function(a){var b=[\".\",\"..\"],c;for(c in a.af)a.af.hasOwnProperty(c)&&b.push(c);return b},symlink:function(a,b,c){a=P.createNode(a,b,41471,0);a.link=c;return a},readlink:function(a){if(!O.Lf(a.mode))throw new O.$e(28);\nreturn a.link}},cf:{read:function(a,b,c,d,e){var g=a.node.af;if(e>=a.node.ff)return 0;a=Math.min(a.node.ff-e,d);if(8<a&&g.subarray)b.set(g.subarray(e,e+a),c);else for(d=0;d<a;d++)b[c+d]=g[e+d];return a},write:function(a,b,c,d,e,g){if(!d)return 0;a=a.node;a.timestamp=Date.now();if(b.subarray&&(!a.af||a.af.subarray)){if(g)return a.af=b.subarray(c,c+d),a.ff=d;if(0===a.ff&&0===e)return a.af=b.slice(c,c+d),a.ff=d;if(e+d<=a.ff)return a.af.set(b.subarray(c,c+d),e),d}P.rh(a,e+d);if(a.af.subarray&&b.subarray)a.af.set(b.subarray(c,\nc+d),e);else for(g=0;g<d;g++)a.af[e+g]=b[c+g];a.ff=Math.max(a.ff,e+d);return d},sf:function(a,b,c){1===c?b+=a.position:2===c&&O.isFile(a.node.mode)&&(b+=a.node.ff);if(0>b)throw new O.$e(28);return b},eg:function(a,b,c){P.rh(a.node,b+c);a.node.ff=Math.max(a.node.ff,b+c)},Vf:function(a,b,c,d,e,g){assert(0===b);if(!O.isFile(a.node.mode))throw new O.$e(43);a=a.node.af;if(g&2||a.buffer!==oa){if(0<d||d+c<a.length)a.subarray?a=a.subarray(d,d+c):a=Array.prototype.slice.call(a,d,d+c);d=!0;g=16384*Math.ceil(c/\n16384);for(b=Ma(g);c<g;)y[b+c++]=0;c=b;if(!c)throw new O.$e(48);y.set(a,c)}else d=!1,c=a.byteOffset;return{Gi:c,Ig:d}},Wf:function(a,b,c,d,e){if(!O.isFile(a.node.mode))throw new O.$e(43);if(e&2)return 0;P.cf.write(a,b,0,d,c,!1);return 0}}},O={root:null,lg:[],oh:{},streams:[],zi:1,Bf:null,nh:\"/\",Sg:!1,Ah:!0,lf:{},Lh:{Fh:{Qh:1,Rh:2}},$e:null,Pg:{},hi:null,Bg:0,jj:function(a){if(!(a instanceof O.$e)){a:{var b=Error();if(!b.stack){try{throw Error();}catch(c){b=c}if(!b.stack){b=\"(no stack trace available)\";\nbreak a}}b=b.stack.toString()}f.extraStackTrace&&(b+=\"\\n\"+f.extraStackTrace());b=ob(b);throw a+\" : \"+b;}return Eb(a.df)},ef:function(a,b){a=Ub(O.cwd(),a);b=b||{};if(!a)return{path:\"\",node:null};var c={Ng:!0,$g:0},d;for(d in c)void 0===b[d]&&(b[d]=c[d]);if(8<b.$g)throw new O.$e(32);a=Ob(a.split(\"/\").filter(function(k){return!!k}),!1);var e=O.root;c=\"/\";for(d=0;d<a.length;d++){var g=d===a.length-1;if(g&&b.parent)break;e=O.Af(e,a[d]);c=Sb(c,a[d]);O.Mf(e)&&(!g||g&&b.Ng)&&(e=e.kg.root);if(!g||b.vf)for(g=\n0;O.Lf(e.mode);)if(e=O.readlink(c),c=Ub(Qb(c),e),e=O.ef(c,{$g:b.$g}).node,40<g++)throw new O.$e(32);}return{path:c,node:e}},Hf:function(a){for(var b;;){if(O.vg(a))return a=a.hf.Dh,b?\"/\"!==a[a.length-1]?a+\"/\"+b:a+b:a;b=b?a.name+\"/\"+b:a.name;a=a.parent}},Rg:function(a,b){for(var c=0,d=0;d<b.length;d++)c=(c<<5)-c+b.charCodeAt(d)|0;return(a+c>>>0)%O.Bf.length},yh:function(a){var b=O.Rg(a.parent.id,a.name);a.Of=O.Bf[b];O.Bf[b]=a},zh:function(a){var b=O.Rg(a.parent.id,a.name);if(O.Bf[b]===a)O.Bf[b]=a.Of;\nelse for(b=O.Bf[b];b;){if(b.Of===a){b.Of=a.Of;break}b=b.Of}},Af:function(a,b){var c=O.xi(a);if(c)throw new O.$e(c,a);for(c=O.Bf[O.Rg(a.id,b)];c;c=c.Of){var d=c.name;if(c.parent.id===a.id&&d===b)return c}return O.lookup(a,b)},createNode:function(a,b,c,d){a=new O.Nh(a,b,c,d);O.yh(a);return a},Mg:function(a){O.zh(a)},vg:function(a){return a===a.parent},Mf:function(a){return!!a.kg},isFile:function(a){return 32768===(a&61440)},jf:function(a){return 16384===(a&61440)},Lf:function(a){return 40960===(a&61440)},\ngg:function(a){return 8192===(a&61440)},ri:function(a){return 24576===(a&61440)},isFIFO:function(a){return 4096===(a&61440)},isSocket:function(a){return 49152===(a&49152)},ii:{r:0,rs:1052672,\"r+\":2,w:577,wx:705,xw:705,\"w+\":578,\"wx+\":706,\"xw+\":706,a:1089,ax:1217,xa:1217,\"a+\":1090,\"ax+\":1218,\"xa+\":1218},Ch:function(a){var b=O.ii[a];if(\"undefined\"===typeof b)throw Error(\"Unknown file open mode: \"+a);return b},sh:function(a){var b=[\"r\",\"w\",\"rw\"][a&3];a&512&&(b+=\"w\");return b},If:function(a,b){if(O.Ah)return 0;\nif(-1===b.indexOf(\"r\")||a.mode&292){if(-1!==b.indexOf(\"w\")&&!(a.mode&146)||-1!==b.indexOf(\"x\")&&!(a.mode&73))return 2}else return 2;return 0},xi:function(a){var b=O.If(a,\"x\");return b?b:a.bf.lookup?0:2},Xg:function(a,b){try{return O.Af(a,b),20}catch(c){}return O.If(a,\"wx\")},wg:function(a,b,c){try{var d=O.Af(a,b)}catch(e){return e.df}if(a=O.If(a,\"wx\"))return a;if(c){if(!O.jf(d.mode))return 54;if(O.vg(d)||O.Hf(d)===O.cwd())return 10}else if(O.jf(d.mode))return 31;return 0},yi:function(a,b){return a?\nO.Lf(a.mode)?32:O.jf(a.mode)&&(\"r\"!==O.sh(b)||b&512)?31:O.If(a,O.sh(b)):44},Ph:4096,Ai:function(a,b){b=b||O.Ph;for(a=a||0;a<=b;a++)if(!O.streams[a])return a;throw new O.$e(33);},yf:function(a){return O.streams[a]},mh:function(a,b,c){O.Gg||(O.Gg=function(){},O.Gg.prototype={object:{get:function(){return this.node},set:function(g){this.node=g}}});var d=new O.Gg,e;for(e in a)d[e]=a[e];a=d;b=O.Ai(b,c);a.fd=b;return O.streams[b]=a},$h:function(a){O.streams[a]=null},Zh:{open:function(a){a.cf=O.ji(a.node.rdev).cf;\na.cf.open&&a.cf.open(a)},sf:function(){throw new O.$e(70);}},Vg:function(a){return a>>8},nj:function(a){return a&255},Nf:function(a,b){return a<<8|b},bh:function(a,b){O.oh[a]={cf:b}},ji:function(a){return O.oh[a]},vh:function(a){var b=[];for(a=[a];a.length;){var c=a.pop();b.push(c);a.push.apply(a,c.lg)}return b},Jh:function(a,b){function c(k){O.Bg--;return b(k)}function d(k){if(k){if(!d.fi)return d.fi=!0,c(k)}else++g>=e.length&&c(null)}\"function\"===typeof a&&(b=a,a=!1);O.Bg++;1<O.Bg&&u(\"warning: \"+\nO.Bg+\" FS.syncfs operations in flight at once, probably just doing extra work\");var e=O.vh(O.root.hf),g=0;e.forEach(function(k){if(!k.type.Jh)return d(null);k.type.Jh(k,a,d)})},hf:function(a,b,c){var d=\"/\"===c,e=!c;if(d&&O.root)throw new O.$e(10);if(!d&&!e){var g=O.ef(c,{Ng:!1});c=g.path;g=g.node;if(O.Mf(g))throw new O.$e(10);if(!O.jf(g.mode))throw new O.$e(54);}b={type:a,sj:b,Dh:c,lg:[]};a=a.hf(b);a.hf=b;b.root=a;d?O.root=a:g&&(g.kg=b,g.hf&&g.hf.lg.push(b));return a},yj:function(a){a=O.ef(a,{Ng:!1});\nif(!O.Mf(a.node))throw new O.$e(28);a=a.node;var b=a.kg,c=O.vh(b);Object.keys(O.Bf).forEach(function(d){for(d=O.Bf[d];d;){var e=d.Of;-1!==c.indexOf(d.hf)&&O.Mg(d);d=e}});a.kg=null;a.hf.lg.splice(a.hf.lg.indexOf(b),1)},lookup:function(a,b){return a.bf.lookup(a,b)},Ef:function(a,b,c){var d=O.ef(a,{parent:!0}).node;a=Rb(a);if(!a||\".\"===a||\"..\"===a)throw new O.$e(28);var e=O.Xg(d,a);if(e)throw new O.$e(e);if(!d.bf.Ef)throw new O.$e(63);return d.bf.Ef(d,a,b,c)},create:function(a,b){return O.Ef(a,(void 0!==\nb?b:438)&4095|32768,0)},mkdir:function(a,b){return O.Ef(a,(void 0!==b?b:511)&1023|16384,0)},pj:function(a,b){a=a.split(\"/\");for(var c=\"\",d=0;d<a.length;++d)if(a[d]){c+=\"/\"+a[d];try{O.mkdir(c,b)}catch(e){if(20!=e.df)throw e;}}},xg:function(a,b,c){\"undefined\"===typeof c&&(c=b,b=438);return O.Ef(a,b|8192,c)},symlink:function(a,b){if(!Ub(a))throw new O.$e(44);var c=O.ef(b,{parent:!0}).node;if(!c)throw new O.$e(44);b=Rb(b);var d=O.Xg(c,b);if(d)throw new O.$e(d);if(!c.bf.symlink)throw new O.$e(63);return c.bf.symlink(c,\nb,a)},rename:function(a,b){var c=Qb(a),d=Qb(b),e=Rb(a),g=Rb(b);var k=O.ef(a,{parent:!0});var m=k.node;k=O.ef(b,{parent:!0});k=k.node;if(!m||!k)throw new O.$e(44);if(m.hf!==k.hf)throw new O.$e(75);var r=O.Af(m,e);d=Vb(a,d);if(\".\"!==d.charAt(0))throw new O.$e(28);d=Vb(b,c);if(\".\"!==d.charAt(0))throw new O.$e(55);try{var q=O.Af(k,g)}catch(t){}if(r!==q){c=O.jf(r.mode);if(e=O.wg(m,e,c))throw new O.$e(e);if(e=q?O.wg(k,g,c):O.Xg(k,g))throw new O.$e(e);if(!m.bf.rename)throw new O.$e(63);if(O.Mf(r)||q&&O.Mf(q))throw new O.$e(10);\nif(k!==m&&(e=O.If(m,\"w\")))throw new O.$e(e);try{O.lf.willMovePath&&O.lf.willMovePath(a,b)}catch(t){u(\"FS.trackingDelegate['willMovePath']('\"+a+\"', '\"+b+\"') threw an exception: \"+t.message)}O.zh(r);try{m.bf.rename(r,k,g)}catch(t){throw t;}finally{O.yh(r)}try{if(O.lf.onMovePath)O.lf.onMovePath(a,b)}catch(t){u(\"FS.trackingDelegate['onMovePath']('\"+a+\"', '\"+b+\"') threw an exception: \"+t.message)}}},rmdir:function(a){var b=O.ef(a,{parent:!0}).node,c=Rb(a),d=O.Af(b,c),e=O.wg(b,c,!0);if(e)throw new O.$e(e);\nif(!b.bf.rmdir)throw new O.$e(63);if(O.Mf(d))throw new O.$e(10);try{O.lf.willDeletePath&&O.lf.willDeletePath(a)}catch(g){u(\"FS.trackingDelegate['willDeletePath']('\"+a+\"') threw an exception: \"+g.message)}b.bf.rmdir(b,c);O.Mg(d);try{if(O.lf.onDeletePath)O.lf.onDeletePath(a)}catch(g){u(\"FS.trackingDelegate['onDeletePath']('\"+a+\"') threw an exception: \"+g.message)}},readdir:function(a){a=O.ef(a,{vf:!0}).node;if(!a.bf.readdir)throw new O.$e(54);return a.bf.readdir(a)},unlink:function(a){var b=O.ef(a,\n{parent:!0}).node,c=Rb(a),d=O.Af(b,c),e=O.wg(b,c,!1);if(e)throw new O.$e(e);if(!b.bf.unlink)throw new O.$e(63);if(O.Mf(d))throw new O.$e(10);try{O.lf.willDeletePath&&O.lf.willDeletePath(a)}catch(g){u(\"FS.trackingDelegate['willDeletePath']('\"+a+\"') threw an exception: \"+g.message)}b.bf.unlink(b,c);O.Mg(d);try{if(O.lf.onDeletePath)O.lf.onDeletePath(a)}catch(g){u(\"FS.trackingDelegate['onDeletePath']('\"+a+\"') threw an exception: \"+g.message)}},readlink:function(a){a=O.ef(a).node;if(!a)throw new O.$e(44);\nif(!a.bf.readlink)throw new O.$e(28);return Ub(O.Hf(a.parent),a.bf.readlink(a))},stat:function(a,b){a=O.ef(a,{vf:!b}).node;if(!a)throw new O.$e(44);if(!a.bf.zf)throw new O.$e(63);return a.bf.zf(a)},lstat:function(a){return O.stat(a,!0)},chmod:function(a,b,c){var d;\"string\"===typeof a?d=O.ef(a,{vf:!c}).node:d=a;if(!d.bf.mf)throw new O.$e(63);d.bf.mf(d,{mode:b&4095|d.mode&-4096,timestamp:Date.now()})},lchmod:function(a,b){O.chmod(a,b,!0)},fchmod:function(a,b){a=O.yf(a);if(!a)throw new O.$e(8);O.chmod(a.node,\nb)},chown:function(a,b,c,d){var e;\"string\"===typeof a?e=O.ef(a,{vf:!d}).node:e=a;if(!e.bf.mf)throw new O.$e(63);e.bf.mf(e,{timestamp:Date.now()})},lchown:function(a,b,c){O.chown(a,b,c,!0)},fchown:function(a,b,c){a=O.yf(a);if(!a)throw new O.$e(8);O.chown(a.node,b,c)},truncate:function(a,b){if(0>b)throw new O.$e(28);var c;\"string\"===typeof a?c=O.ef(a,{vf:!0}).node:c=a;if(!c.bf.mf)throw new O.$e(63);if(O.jf(c.mode))throw new O.$e(31);if(!O.isFile(c.mode))throw new O.$e(28);if(a=O.If(c,\"w\"))throw new O.$e(a);\nc.bf.mf(c,{size:b,timestamp:Date.now()})},ej:function(a,b){a=O.yf(a);if(!a)throw new O.$e(8);if(0===(a.flags&2097155))throw new O.$e(28);O.truncate(a.node,b)},zj:function(a,b,c){a=O.ef(a,{vf:!0}).node;a.bf.mf(a,{timestamp:Math.max(b,c)})},open:function(a,b,c,d,e){if(\"\"===a)throw new O.$e(44);b=\"string\"===typeof b?O.Ch(b):b;c=b&64?(\"undefined\"===typeof c?438:c)&4095|32768:0;if(\"object\"===typeof a)var g=a;else{a=Pb(a);try{g=O.ef(a,{vf:!(b&131072)}).node}catch(m){}}var k=!1;if(b&64)if(g){if(b&128)throw new O.$e(20);\n}else g=O.Ef(a,c,0),k=!0;if(!g)throw new O.$e(44);O.gg(g.mode)&&(b&=-513);if(b&65536&&!O.jf(g.mode))throw new O.$e(54);if(!k&&(c=O.yi(g,b)))throw new O.$e(c);b&512&&O.truncate(g,0);b&=-131713;d=O.mh({node:g,path:O.Hf(g),flags:b,seekable:!0,position:0,cf:g.cf,Vi:[],error:!1},d,e);d.cf.open&&d.cf.open(d);!f.logReadFiles||b&1||(O.Zg||(O.Zg={}),a in O.Zg||(O.Zg[a]=1,u(\"FS.trackingDelegate error on read file: \"+a)));try{O.lf.onOpenFile&&(e=0,1!==(b&2097155)&&(e|=O.Lh.Fh.Qh),0!==(b&2097155)&&(e|=O.Lh.Fh.Rh),\nO.lf.onOpenFile(a,e))}catch(m){u(\"FS.trackingDelegate['onOpenFile']('\"+a+\"', flags) threw an exception: \"+m.message)}return d},close:function(a){if(O.hg(a))throw new O.$e(8);a.Kf&&(a.Kf=null);try{a.cf.close&&a.cf.close(a)}catch(b){throw b;}finally{O.$h(a.fd)}a.fd=null},hg:function(a){return null===a.fd},sf:function(a,b,c){if(O.hg(a))throw new O.$e(8);if(!a.seekable||!a.cf.sf)throw new O.$e(70);if(0!=c&&1!=c&&2!=c)throw new O.$e(28);a.position=a.cf.sf(a,b,c);a.Vi=[];return a.position},read:function(a,\nb,c,d,e){if(0>d||0>e)throw new O.$e(28);if(O.hg(a))throw new O.$e(8);if(1===(a.flags&2097155))throw new O.$e(8);if(O.jf(a.node.mode))throw new O.$e(31);if(!a.cf.read)throw new O.$e(28);var g=\"undefined\"!==typeof e;if(!g)e=a.position;else if(!a.seekable)throw new O.$e(70);b=a.cf.read(a,b,c,d,e);g||(a.position+=b);return b},write:function(a,b,c,d,e,g){if(0>d||0>e)throw new O.$e(28);if(O.hg(a))throw new O.$e(8);if(0===(a.flags&2097155))throw new O.$e(8);if(O.jf(a.node.mode))throw new O.$e(31);if(!a.cf.write)throw new O.$e(28);\na.seekable&&a.flags&1024&&O.sf(a,0,2);var k=\"undefined\"!==typeof e;if(!k)e=a.position;else if(!a.seekable)throw new O.$e(70);b=a.cf.write(a,b,c,d,e,g);k||(a.position+=b);try{if(a.path&&O.lf.onWriteToFile)O.lf.onWriteToFile(a.path)}catch(m){u(\"FS.trackingDelegate['onWriteToFile']('\"+a.path+\"') threw an exception: \"+m.message)}return b},eg:function(a,b,c){if(O.hg(a))throw new O.$e(8);if(0>b||0>=c)throw new O.$e(28);if(0===(a.flags&2097155))throw new O.$e(8);if(!O.isFile(a.node.mode)&&!O.jf(a.node.mode))throw new O.$e(43);\nif(!a.cf.eg)throw new O.$e(138);a.cf.eg(a,b,c)},Vf:function(a,b,c,d,e,g){if(0!==(e&2)&&0===(g&2)&&2!==(a.flags&2097155))throw new O.$e(2);if(1===(a.flags&2097155))throw new O.$e(2);if(!a.cf.Vf)throw new O.$e(43);return a.cf.Vf(a,b,c,d,e,g)},Wf:function(a,b,c,d,e){return a&&a.cf.Wf?a.cf.Wf(a,b,c,d,e):0},rj:function(){return 0},Tf:function(a,b,c){if(!a.cf.Tf)throw new O.$e(59);return a.cf.Tf(a,b,c)},readFile:function(a,b){b=b||{};b.flags=b.flags||\"r\";b.encoding=b.encoding||\"binary\";if(\"utf8\"!==b.encoding&&\n\"binary\"!==b.encoding)throw Error('Invalid encoding type \"'+b.encoding+'\"');var c,d=O.open(a,b.flags);a=O.stat(a).size;var e=new Uint8Array(a);O.read(d,e,0,a,0);\"utf8\"===b.encoding?c=Ja(e,0):\"binary\"===b.encoding&&(c=e);O.close(d);return c},writeFile:function(a,b,c){c=c||{};c.flags=c.flags||\"w\";a=O.open(a,c.flags,c.mode);if(\"string\"===typeof b){var d=new Uint8Array(Ka(b)+1);b=Ia(b,d,0,d.length);O.write(a,d,0,b,void 0,c.Yh)}else if(ArrayBuffer.isView(b))O.write(a,b,0,b.byteLength,void 0,c.Yh);else throw Error(\"Unsupported data type\");\nO.close(a)},cwd:function(){return O.nh},chdir:function(a){a=O.ef(a,{vf:!0});if(null===a.node)throw new O.$e(44);if(!O.jf(a.node.mode))throw new O.$e(54);var b=O.If(a.node,\"x\");if(b)throw new O.$e(b);O.nh=a.path},bi:function(){O.mkdir(\"/tmp\");O.mkdir(\"/home\");O.mkdir(\"/home/web_user\")},ai:function(){O.mkdir(\"/dev\");O.bh(O.Nf(1,3),{read:function(){return 0},write:function(b,c,d,e){return e}});O.xg(\"/dev/null\",O.Nf(1,3));Xb(O.Nf(5,0),cc);Xb(O.Nf(6,0),dc);O.xg(\"/dev/tty\",O.Nf(5,0));O.xg(\"/dev/tty1\",O.Nf(6,\n0));var a=Tb();O.Gf(\"/dev\",\"random\",a);O.Gf(\"/dev\",\"urandom\",a);O.mkdir(\"/dev/shm\");O.mkdir(\"/dev/shm/tmp\")},di:function(){O.mkdir(\"/proc\");O.mkdir(\"/proc/self\");O.mkdir(\"/proc/self/fd\");O.hf({hf:function(){var a=O.createNode(\"/proc/self\",\"fd\",16895,73);a.bf={lookup:function(b,c){var d=O.yf(+c);if(!d)throw new O.$e(8);b={parent:null,hf:{Dh:\"fake\"},bf:{readlink:function(){return d.path}}};return b.parent=b}};return a}},{},\"/proc/self/fd\")},ei:function(){f.stdin?O.Gf(\"/dev\",\"stdin\",f.stdin):O.symlink(\"/dev/tty\",\n\"/dev/stdin\");f.stdout?O.Gf(\"/dev\",\"stdout\",null,f.stdout):O.symlink(\"/dev/tty\",\"/dev/stdout\");f.stderr?O.Gf(\"/dev\",\"stderr\",null,f.stderr):O.symlink(\"/dev/tty1\",\"/dev/stderr\");O.open(\"/dev/stdin\",\"r\");O.open(\"/dev/stdout\",\"w\");O.open(\"/dev/stderr\",\"w\")},qh:function(){O.$e||(O.$e=function(a,b){this.node=b;this.Ji=function(c){this.df=c};this.Ji(a);this.message=\"FS error\"},O.$e.prototype=Error(),O.$e.prototype.constructor=O.$e,[44].forEach(function(a){O.Pg[a]=new O.$e(a);O.Pg[a].stack=\"<generic error, no stack>\"}))},\nMi:function(){O.qh();O.Bf=Array(4096);O.hf(P,{},\"/\");O.bi();O.ai();O.di();O.hi={MEMFS:P}},fg:function(a,b,c){O.fg.Sg=!0;O.qh();f.stdin=a||f.stdin;f.stdout=b||f.stdout;f.stderr=c||f.stderr;O.ei()},quit:function(){O.fg.Sg=!1;var a=f._fflush;a&&a(0);for(a=0;a<O.streams.length;a++){var b=O.streams[a];b&&O.close(b)}},Qg:function(a,b){var c=0;a&&(c|=365);b&&(c|=146);return c},dj:function(a,b){a=O.Kg(a,b);if(a.exists)return a.object;Eb(a.error);return null},Kg:function(a,b){try{var c=O.ef(a,{vf:!b});a=c.path}catch(e){}var d=\n{vg:!1,exists:!1,error:0,name:null,path:null,object:null,Bi:!1,Di:null,Ci:null};try{c=O.ef(a,{parent:!0}),d.Bi=!0,d.Di=c.path,d.Ci=c.node,d.name=Rb(a),c=O.ef(a,{vf:!b}),d.exists=!0,d.path=c.path,d.object=c.node,d.name=c.node.name,d.vg=\"/\"===c.path}catch(e){d.error=e.df}return d},bj:function(a,b){a=\"string\"===typeof a?a:O.Hf(a);for(b=b.split(\"/\").reverse();b.length;){var c=b.pop();if(c){var d=Sb(a,c);try{O.mkdir(d)}catch(e){}a=d}}return d},ci:function(a,b,c,d,e){a=Sb(\"string\"===typeof a?a:O.Hf(a),\nb);return O.create(a,O.Qg(d,e))},lh:function(a,b,c,d,e,g){a=b?Sb(\"string\"===typeof a?a:O.Hf(a),b):a;d=O.Qg(d,e);e=O.create(a,d);if(c){if(\"string\"===typeof c){a=Array(c.length);b=0;for(var k=c.length;b<k;++b)a[b]=c.charCodeAt(b);c=a}O.chmod(e,d|146);a=O.open(e,\"w\");O.write(a,c,0,c.length,0,g);O.close(a);O.chmod(e,d)}return e},Gf:function(a,b,c,d){a=Sb(\"string\"===typeof a?a:O.Hf(a),b);b=O.Qg(!!c,!!d);O.Gf.Vg||(O.Gf.Vg=64);var e=O.Nf(O.Gf.Vg++,0);O.bh(e,{open:function(g){g.seekable=!1},close:function(){d&&\nd.buffer&&d.buffer.length&&d(10)},read:function(g,k,m,r){for(var q=0,t=0;t<r;t++){try{var w=c()}catch(B){throw new O.$e(29);}if(void 0===w&&0===q)throw new O.$e(6);if(null===w||void 0===w)break;q++;k[m+t]=w}q&&(g.node.timestamp=Date.now());return q},write:function(g,k,m,r){for(var q=0;q<r;q++)try{d(k[m+q])}catch(t){throw new O.$e(29);}r&&(g.node.timestamp=Date.now());return q}});return O.xg(a,b,e)},th:function(a){if(a.Tg||a.si||a.link||a.af)return!0;var b=!0;if(\"undefined\"!==typeof XMLHttpRequest)throw Error(\"Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.\");\nif(ra)try{a.af=Zb(ra(a.url),!0),a.ff=a.af.length}catch(c){b=!1}else throw Error(\"Cannot load without read() or XMLHttpRequest.\");b||Eb(29);return b},aj:function(a,b,c,d,e){function g(){this.Ug=!1;this.Rf=[]}g.prototype.get=function(q){if(!(q>this.length-1||0>q)){var t=q%this.chunkSize;return this.xh(q/this.chunkSize|0)[t]}};g.prototype.Uh=function(q){this.xh=q};g.prototype.jh=function(){var q=new XMLHttpRequest;q.open(\"HEAD\",c,!1);q.send(null);if(!(200<=q.status&&300>q.status||304===q.status))throw Error(\"Couldn't load \"+\nc+\". Status: \"+q.status);var t=Number(q.getResponseHeader(\"Content-length\")),w,B=(w=q.getResponseHeader(\"Accept-Ranges\"))&&\"bytes\"===w;q=(w=q.getResponseHeader(\"Content-Encoding\"))&&\"gzip\"===w;var p=1048576;B||(p=t);var x=this;x.Uh(function(z){var I=z*p,W=(z+1)*p-1;W=Math.min(W,t-1);if(\"undefined\"===typeof x.Rf[z]){var db=x.Rf;if(I>W)throw Error(\"invalid range (\"+I+\", \"+W+\") or no bytes requested!\");if(W>t-1)throw Error(\"only \"+t+\" bytes available! programmer error!\");var K=new XMLHttpRequest;K.open(\"GET\",\nc,!1);t!==p&&K.setRequestHeader(\"Range\",\"bytes=\"+I+\"-\"+W);\"undefined\"!=typeof Uint8Array&&(K.responseType=\"arraybuffer\");K.overrideMimeType&&K.overrideMimeType(\"text/plain; charset=x-user-defined\");K.send(null);if(!(200<=K.status&&300>K.status||304===K.status))throw Error(\"Couldn't load \"+c+\". Status: \"+K.status);I=void 0!==K.response?new Uint8Array(K.response||[]):Zb(K.responseText||\"\",!0);db[z]=I}if(\"undefined\"===typeof x.Rf[z])throw Error(\"doXHR failed!\");return x.Rf[z]});if(q||!t)p=t=1,p=t=this.xh(0).length,\nya(\"LazyFiles on gzip forces download of the whole file when length is accessed\");this.Th=t;this.Sh=p;this.Ug=!0};if(\"undefined\"!==typeof XMLHttpRequest){if(!la)throw\"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc\";var k=new g;Object.defineProperties(k,{length:{get:function(){this.Ug||this.jh();return this.Th}},chunkSize:{get:function(){this.Ug||this.jh();return this.Sh}}});k={Tg:!1,af:k}}else k={Tg:!1,url:c};var m=O.ci(a,b,k,d,\ne);k.af?m.af=k.af:k.url&&(m.af=null,m.url=k.url);Object.defineProperties(m,{ff:{get:function(){return this.af.length}}});var r={};Object.keys(m.cf).forEach(function(q){var t=m.cf[q];r[q]=function(){if(!O.th(m))throw new O.$e(29);return t.apply(null,arguments)}});r.read=function(q,t,w,B,p){if(!O.th(m))throw new O.$e(29);q=q.node.af;if(p>=q.length)return 0;B=Math.min(q.length-p,B);if(q.slice)for(var x=0;x<B;x++)t[w+x]=q[p+x];else for(x=0;x<B;x++)t[w+x]=q.get(p+x);return B};m.cf=r;return m},cj:function(a,\nb,c,d,e,g,k,m,r,q){function t(B){function p(z){q&&q();m||O.lh(a,b,z,d,e,r);g&&g();fb()}var x=!1;f.preloadPlugins.forEach(function(z){!x&&z.canHandle(w)&&(z.handle(B,w,p,function(){k&&k();fb()}),x=!0)});x||p(B)}ec.fg();var w=b?Ub(Sb(a,b)):a;eb();\"string\"==typeof c?ec.Wi(c,function(B){t(B)},k):t(c)},indexedDB:function(){return window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB},fh:function(){return\"EM_FS_\"+window.location.pathname},gh:20,dg:\"FILE_DATA\",vj:function(a,b,\nc){b=b||function(){};c=c||function(){};var d=O.indexedDB();try{var e=d.open(O.fh(),O.gh)}catch(g){return c(g)}e.onupgradeneeded=function(){ya(\"creating db\");e.result.createObjectStore(O.dg)};e.onsuccess=function(){var g=e.result.transaction([O.dg],\"readwrite\"),k=g.objectStore(O.dg),m=0,r=0,q=a.length;a.forEach(function(t){t=k.put(O.Kg(t).object.af,t);t.onsuccess=function(){m++;m+r==q&&(0==r?b():c())};t.onerror=function(){r++;m+r==q&&(0==r?b():c())}});g.onerror=c};e.onerror=c},lj:function(a,b,c){b=\nb||function(){};c=c||function(){};var d=O.indexedDB();try{var e=d.open(O.fh(),O.gh)}catch(g){return c(g)}e.onupgradeneeded=c;e.onsuccess=function(){var g=e.result;try{var k=g.transaction([O.dg],\"readonly\")}catch(w){c(w);return}var m=k.objectStore(O.dg),r=0,q=0,t=a.length;a.forEach(function(w){var B=m.get(w);B.onsuccess=function(){O.Kg(w).exists&&O.unlink(w);O.lh(Qb(w),Rb(w),B.result,!0,!0,!0);r++;r+q==t&&(0==q?b():c())};B.onerror=function(){q++;r+q==t&&(0==q?b():c())}});k.onerror=c};e.onerror=c}},\nfc={};\nfunction hc(a,b,c){try{var d=a(b)}catch(e){if(e&&e.node&&Pb(b)!==Pb(O.Hf(e.node)))return-54;throw e;}E[c>>2]=d.dev;E[c+4>>2]=0;E[c+8>>2]=d.ino;E[c+12>>2]=d.mode;E[c+16>>2]=d.nlink;E[c+20>>2]=d.uid;E[c+24>>2]=d.gid;E[c+28>>2]=d.rdev;E[c+32>>2]=0;L=[d.size>>>0,(J=d.size,1<=+Math.abs(J)?0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[c+40>>2]=L[0];E[c+44>>2]=L[1];E[c+48>>2]=4096;E[c+52>>2]=d.blocks;E[c+56>>2]=d.atime.getTime()/1E3|0;E[c+60>>\n2]=0;E[c+64>>2]=d.mtime.getTime()/1E3|0;E[c+68>>2]=0;E[c+72>>2]=d.ctime.getTime()/1E3|0;E[c+76>>2]=0;L=[d.ino>>>0,(J=d.ino,1<=+Math.abs(J)?0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[c+80>>2]=L[0];E[c+84>>2]=L[1];return 0}var ic=void 0;function Q(){ic+=4;return E[ic-4>>2]}function jc(a){a=O.yf(a);if(!a)throw new O.$e(8);return a}\nfunction kc(a,b,c,d,e){if(l)return N(3,1,a,b,c,d,e);try{e=0;for(var g=b?E[b>>2]:0,k=b?E[b+4>>2]:0,m=c?E[c>>2]:0,r=c?E[c+4>>2]:0,q=d?E[d>>2]:0,t=d?E[d+4>>2]:0,w=0,B=0,p=0,x=0,z=0,I=0,W=(b?E[b>>2]:0)|(c?E[c>>2]:0)|(d?E[d>>2]:0),db=(b?E[b+4>>2]:0)|(c?E[c+4>>2]:0)|(d?E[d+4>>2]:0),K=0;K<a;K++){var Y=1<<K%32;if(32>K?W&Y:db&Y){var ia=O.yf(K);if(!ia)throw new O.$e(8);var na=5;ia.cf.Yf&&(na=ia.cf.Yf(ia));na&1&&(32>K?g&Y:k&Y)&&(32>K?w|=Y:B|=Y,e++);na&4&&(32>K?m&Y:r&Y)&&(32>K?p|=Y:x|=Y,e++);na&2&&(32>K?q&Y:\nt&Y)&&(32>K?z|=Y:I|=Y,e++)}}b&&(E[b>>2]=w,E[b+4>>2]=B);c&&(E[c>>2]=p,E[c+4>>2]=x);d&&(E[d>>2]=z,E[d+4>>2]=I);return e}catch(ua){return\"undefined\"!==typeof O&&ua instanceof O.$e||n(ua),-ua.df}}function lc(a,b){if(l)return N(4,1,a,b);try{a=C(a);if(b&-8)var c=-28;else{var d;(d=O.ef(a,{vf:!0}).node)?(a=\"\",b&4&&(a+=\"r\"),b&2&&(a+=\"w\"),b&1&&(a+=\"x\"),c=a&&O.If(d,a)?-2:0):c=-44}return c}catch(e){return\"undefined\"!==typeof O&&e instanceof O.$e||n(e),-e.df}}\nfunction mc(a,b,c){if(l)return N(5,1,a,b,c);ic=c;try{var d=jc(a);switch(b){case 0:var e=Q();return 0>e?-28:O.open(d.path,d.flags,0,e).fd;case 1:case 2:return 0;case 3:return d.flags;case 4:return e=Q(),d.flags|=e,0;case 12:return e=Q(),Qa[e+0>>1]=2,0;case 13:case 14:return 0;case 16:case 8:return-28;case 9:return Eb(28),-1;default:return-28}}catch(g){return\"undefined\"!==typeof O&&g instanceof O.$e||n(g),-g.df}}\nfunction nc(a,b){if(l)return N(6,1,a,b);try{var c=jc(a);return hc(O.stat,c.path,b)}catch(d){return\"undefined\"!==typeof O&&d instanceof O.$e||n(d),-d.df}}\nfunction oc(a,b,c){if(l)return N(7,1,a,b,c);try{var d=jc(a);d.Kf||(d.Kf=O.readdir(d.path));a=0;for(var e=O.sf(d,0,1),g=Math.floor(e/280);g<d.Kf.length&&a+280<=c;){var k=d.Kf[g];if(\".\"===k[0]){var m=1;var r=4}else{var q=O.Af(d.node,k);m=q.id;r=O.gg(q.mode)?2:O.jf(q.mode)?4:O.Lf(q.mode)?10:8}L=[m>>>0,(J=m,1<=+Math.abs(J)?0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[b+a>>2]=L[0];E[b+a+4>>2]=L[1];L=[280*(g+1)>>>0,(J=280*(g+1),1<=+Math.abs(J)?\n0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[b+a+8>>2]=L[0];E[b+a+12>>2]=L[1];Qa[b+a+16>>1]=280;y[b+a+18>>0]=r;Ia(k,v,b+a+19,256);a+=280;g+=1}O.sf(d,280*g,0);return a}catch(t){return\"undefined\"!==typeof O&&t instanceof O.$e||n(t),-t.df}}function pc(a,b){if(l)return N(8,1,a,b);try{return qc(b,0,136),E[b>>2]=1,E[b+4>>2]=2,E[b+8>>2]=3,E[b+12>>2]=4,0}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}}\nfunction rc(a,b,c){if(l)return N(9,1,a,b,c);ic=c;try{var d=jc(a);switch(b){case 21509:case 21505:return d.tty?0:-59;case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:return d.tty?0:-59;case 21519:if(!d.tty)return-59;var e=Q();return E[e>>2]=0;case 21520:return d.tty?-28:-59;case 21531:return e=Q(),O.Tf(d,b,e);case 21523:return d.tty?0:-59;case 21524:return d.tty?0:-59;default:n(\"bad ioctl syscall \"+b)}}catch(g){return\"undefined\"!==typeof O&&g instanceof O.$e||n(g),-g.df}}\nfunction sc(a,b){if(l)return N(10,1,a,b);try{return a=C(a),hc(O.lstat,a,b)}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}}function tc(a,b){if(l)return N(11,1,a,b);try{return a=C(a),a=Pb(a),\"/\"===a[a.length-1]&&(a=a.substr(0,a.length-1)),O.mkdir(a,b,0),0}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}}\nfunction uc(a,b,c,d,e,g){if(l)return N(12,1,a,b,c,d,e,g);try{a:{g<<=12;var k=!1;if(0!==(d&16)&&0!==a%16384)var m=-28;else{if(0!==(d&32)){var r=vc(16384,b);if(!r){m=-48;break a}qc(r,0,b);k=!0}else{var q=O.yf(e);if(!q){m=-8;break a}var t=O.Vf(q,a,b,g,c,d);r=t.Gi;k=t.Ig}fc[r]={wi:r,ti:b,Ig:k,fd:e,Fi:c,flags:d,offset:g};m=r}}return m}catch(w){return\"undefined\"!==typeof O&&w instanceof O.$e||n(w),-w.df}}\nfunction wc(a,b){if(l)return N(13,1,a,b);try{if(-1===(a|0)||0===b)var c=-28;else{var d=fc[a];if(d&&b===d.ti){var e=O.yf(d.fd);if(d.Fi&2){var g=d.flags,k=d.offset,m=v.slice(a,a+b);O.Wf(e,m,k,b,g)}fc[a]=null;d.Ig&&zb(d.wi)}c=0}return c}catch(r){return\"undefined\"!==typeof O&&r instanceof O.$e||n(r),-r.df}}function xc(a,b,c){if(l)return N(14,1,a,b,c);ic=c;try{var d=C(a),e=Q();return O.open(d,b,e).fd}catch(g){return\"undefined\"!==typeof O&&g instanceof O.$e||n(g),-g.df}}\nfunction yc(a,b,c){if(l)return N(15,1,a,b,c);try{for(var d=c=0;d<b;d++){var e=a+8*d,g=Qa[e+4>>1],k=32,m=O.yf(E[e>>2]);m&&(k=5,m.cf.Yf&&(k=m.cf.Yf(m)));(k&=g|24)&&c++;Qa[e+6>>1]=k}return c}catch(r){return\"undefined\"!==typeof O&&r instanceof O.$e||n(r),-r.df}}function zc(a,b,c,d){if(l)return N(16,1,a,b,c,d);try{return d&&(E[d>>2]=-1,E[d+4>>2]=-1,E[d+8>>2]=-1,E[d+12>>2]=-1),0}catch(e){return\"undefined\"!==typeof O&&e instanceof O.$e||n(e),-e.df}}\nfunction Ac(a,b,c){if(l)return N(17,1,a,b,c);try{var d=jc(a);return O.read(d,y,b,c)}catch(e){return\"undefined\"!==typeof O&&e instanceof O.$e||n(e),-e.df}}function Bc(a,b){if(l)return N(18,1,a,b);try{return a=C(a),b=C(b),O.rename(a,b),0}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}}function Cc(a){if(l)return N(19,1,a);try{return a=C(a),O.rmdir(a),0}catch(b){return\"undefined\"!==typeof O&&b instanceof O.$e||n(b),-b.df}}\nvar R={hf:function(){f.websocket=f.websocket&&\"object\"===typeof f.websocket?f.websocket:{};f.websocket.Hg={};f.websocket.on=function(a,b){\"function\"===typeof b&&(this.Hg[a]=b);return this};f.websocket.emit=function(a,b){\"function\"===typeof this.Hg[a]&&this.Hg[a].call(this,b)};return O.createNode(null,\"/\",16895,0)},createSocket:function(a,b,c){b&=-526337;c&&assert(1==b==(6==c));a={family:a,type:b,protocol:c,kf:null,error:null,mg:{},pending:[],$f:[],nf:R.pf};b=R.yg();c=O.createNode(R.root,b,49152,0);\nc.ag=a;b=O.mh({path:b,node:c,flags:O.Ch(\"r+\"),seekable:!1,cf:R.cf});a.stream=b;return a},li:function(a){return(a=O.yf(a))&&O.isSocket(a.node.mode)?a.node.ag:null},cf:{Yf:function(a){a=a.node.ag;return a.nf.Yf(a)},Tf:function(a,b,c){a=a.node.ag;return a.nf.Tf(a,b,c)},read:function(a,b,c,d){a=a.node.ag;d=a.nf.ah(a,d);if(!d)return 0;b.set(d.buffer,c);return d.buffer.length},write:function(a,b,c,d){a=a.node.ag;return a.nf.eh(a,b,c,d)},close:function(a){a=a.node.ag;a.nf.close(a)}},yg:function(){R.yg.current||\n(R.yg.current=0);return\"socket[\"+R.yg.current++ +\"]\"},pf:{sg:function(a,b,c){if(\"object\"===typeof b){var d=b;c=b=null}if(d)if(d._socket)b=d._socket.remoteAddress,c=d._socket.remotePort;else{c=/ws[s]?:\\/\\/([^:]+):(\\d+)/.exec(d.url);if(!c)throw Error(\"WebSocket URL must be in the format ws(s)://address:port\");b=c[1];c=parseInt(c[2],10)}else try{var e=f.websocket&&\"object\"===typeof f.websocket,g=\"ws:#\".replace(\"#\",\"//\");e&&\"string\"===typeof f.websocket.url&&(g=f.websocket.url);if(\"ws://\"===g||\"wss://\"===\ng){var k=b.split(\"/\");g=g+k[0]+\":\"+c+\"/\"+k.slice(1).join(\"/\")}k=\"binary\";e&&\"string\"===typeof f.websocket.subprotocol&&(k=f.websocket.subprotocol);var m=void 0;\"null\"!==k&&(k=k.replace(/^ +| +$/g,\"\").split(/ *, */),m=h?{protocol:k.toString()}:k);e&&null===f.websocket.subprotocol&&(m=void 0);d=new (h?require(\"ws\"):WebSocket)(g,m);d.binaryType=\"arraybuffer\"}catch(r){throw new O.$e(23);}b={gf:b,port:c,socket:d,tg:[]};R.pf.ih(a,b);R.pf.mi(a,b);2===a.type&&\"undefined\"!==typeof a.Pf&&b.tg.push(new Uint8Array([255,\n255,255,255,112,111,114,116,(a.Pf&65280)>>8,a.Pf&255]));return b},ug:function(a,b,c){return a.mg[b+\":\"+c]},ih:function(a,b){a.mg[b.gf+\":\"+b.port]=b},Gh:function(a,b){delete a.mg[b.gf+\":\"+b.port]},mi:function(a,b){function c(){f.websocket.emit(\"open\",a.stream.fd);try{for(var g=b.tg.shift();g;)b.socket.send(g),g=b.tg.shift()}catch(k){b.socket.close()}}function d(g){if(\"string\"===typeof g)g=(new TextEncoder).encode(g);else{assert(void 0!==g.byteLength);if(0==g.byteLength)return;g=new Uint8Array(g)}var k=\ne;e=!1;k&&10===g.length&&255===g[0]&&255===g[1]&&255===g[2]&&255===g[3]&&112===g[4]&&111===g[5]&&114===g[6]&&116===g[7]?(g=g[8]<<8|g[9],R.pf.Gh(a,b),b.port=g,R.pf.ih(a,b)):(a.$f.push({gf:b.gf,port:b.port,data:g}),f.websocket.emit(\"message\",a.stream.fd))}var e=!0;h?(b.socket.on(\"open\",c),b.socket.on(\"message\",function(g,k){k.Xi&&d((new Uint8Array(g)).buffer)}),b.socket.on(\"close\",function(){f.websocket.emit(\"close\",a.stream.fd)}),b.socket.on(\"error\",function(){a.error=14;f.websocket.emit(\"error\",[a.stream.fd,\na.error,\"ECONNREFUSED: Connection refused\"])})):(b.socket.onopen=c,b.socket.onclose=function(){f.websocket.emit(\"close\",a.stream.fd)},b.socket.onmessage=function(g){d(g.data)},b.socket.onerror=function(){a.error=14;f.websocket.emit(\"error\",[a.stream.fd,a.error,\"ECONNREFUSED: Connection refused\"])})},Yf:function(a){if(1===a.type&&a.kf)return a.pending.length?65:0;var b=0,c=1===a.type?R.pf.ug(a,a.rf,a.uf):null;if(a.$f.length||!c||c&&c.socket.readyState===c.socket.CLOSING||c&&c.socket.readyState===c.socket.CLOSED)b|=\n65;if(!c||c&&c.socket.readyState===c.socket.OPEN)b|=4;if(c&&c.socket.readyState===c.socket.CLOSING||c&&c.socket.readyState===c.socket.CLOSED)b|=16;return b},Tf:function(a,b,c){switch(b){case 21531:return b=0,a.$f.length&&(b=a.$f[0].data.length),E[c>>2]=b,0;default:return 28}},close:function(a){if(a.kf){try{a.kf.close()}catch(e){}a.kf=null}for(var b=Object.keys(a.mg),c=0;c<b.length;c++){var d=a.mg[b[c]];try{d.socket.close()}catch(e){}R.pf.Gh(a,d)}return 0},bind:function(a,b,c){if(\"undefined\"!==typeof a.Ag||\n\"undefined\"!==typeof a.Pf)throw new O.$e(28);a.Ag=b;a.Pf=c;if(2===a.type){a.kf&&(a.kf.close(),a.kf=null);try{a.nf.listen(a,0)}catch(d){if(!(d instanceof O.$e))throw d;if(138!==d.df)throw d;}}},connect:function(a,b,c){if(a.kf)throw new O.$e(138);if(\"undefined\"!==typeof a.rf&&\"undefined\"!==typeof a.uf){var d=R.pf.ug(a,a.rf,a.uf);if(d){if(d.socket.readyState===d.socket.CONNECTING)throw new O.$e(7);throw new O.$e(30);}}b=R.pf.sg(a,b,c);a.rf=b.gf;a.uf=b.port;throw new O.$e(26);},listen:function(a){if(!h)throw new O.$e(138);\nif(a.kf)throw new O.$e(28);var b=require(\"ws\").Server;a.kf=new b({host:a.Ag,port:a.Pf});f.websocket.emit(\"listen\",a.stream.fd);a.kf.on(\"connection\",function(c){if(1===a.type){var d=R.createSocket(a.family,a.type,a.protocol);c=R.pf.sg(d,c);d.rf=c.gf;d.uf=c.port;a.pending.push(d);f.websocket.emit(\"connection\",d.stream.fd)}else R.pf.sg(a,c),f.websocket.emit(\"connection\",a.stream.fd)});a.kf.on(\"closed\",function(){f.websocket.emit(\"close\",a.stream.fd);a.kf=null});a.kf.on(\"error\",function(){a.error=23;\nf.websocket.emit(\"error\",[a.stream.fd,a.error,\"EHOSTUNREACH: Host is unreachable\"])})},accept:function(a){if(!a.kf)throw new O.$e(28);var b=a.pending.shift();b.stream.flags=a.stream.flags;return b},hj:function(a,b){if(b){if(void 0===a.rf||void 0===a.uf)throw new O.$e(53);b=a.rf;a=a.uf}else b=a.Ag||0,a=a.Pf||0;return{gf:b,port:a}},eh:function(a,b,c,d,e,g){if(2===a.type){if(void 0===e||void 0===g)e=a.rf,g=a.uf;if(void 0===e||void 0===g)throw new O.$e(17);}else e=a.rf,g=a.uf;var k=R.pf.ug(a,e,g);if(1===\na.type){if(!k||k.socket.readyState===k.socket.CLOSING||k.socket.readyState===k.socket.CLOSED)throw new O.$e(53);if(k.socket.readyState===k.socket.CONNECTING)throw new O.$e(6);}ArrayBuffer.isView(b)&&(c+=b.byteOffset,b=b.buffer);var m;b instanceof SharedArrayBuffer?m=(new Uint8Array(new Uint8Array(b.slice(c,c+d)))).buffer:m=b.slice(c,c+d);if(2===a.type&&(!k||k.socket.readyState!==k.socket.OPEN))return k&&k.socket.readyState!==k.socket.CLOSING&&k.socket.readyState!==k.socket.CLOSED||(k=R.pf.sg(a,e,\ng)),k.tg.push(m),d;try{return k.socket.send(m),d}catch(r){throw new O.$e(28);}},ah:function(a,b){if(1===a.type&&a.kf)throw new O.$e(53);var c=a.$f.shift();if(!c){if(1===a.type){if(a=R.pf.ug(a,a.rf,a.uf)){if(a.socket.readyState===a.socket.CLOSING||a.socket.readyState===a.socket.CLOSED)return null;throw new O.$e(6);}throw new O.$e(53);}throw new O.$e(6);}var d=c.data.byteLength||c.data.length,e=c.data.byteOffset||0,g=c.data.buffer||c.data;b=Math.min(b,d);var k={buffer:new Uint8Array(g,e,b),gf:c.gf,\nport:c.port};1===a.type&&b<d&&(c.data=new Uint8Array(g,e+b,d-b),a.$f.unshift(c));return k}}};function Dc(a){a=a.split(\".\");for(var b=0;4>b;b++){var c=Number(a[b]);if(isNaN(c))return null;a[b]=c}return(a[0]|a[1]<<8|a[2]<<16|a[3]<<24)>>>0}\nfunction Ec(a){var b,c,d=[];if(!/^((?=.*::)(?!.*::.+::)(::)?([\\dA-F]{1,4}:(:|\\b)|){5}|([\\dA-F]{1,4}:){6})((([\\dA-F]{1,4}((?!\\3)::|:\\b|$))|(?!\\2\\3)){2}|(((2[0-4]|1\\d|[1-9])?\\d|25[0-5])\\.?\\b){4})$/i.test(a))return null;if(\"::\"===a)return[0,0,0,0,0,0,0,0];a=0===a.indexOf(\"::\")?a.replace(\"::\",\"Z:\"):a.replace(\"::\",\":Z:\");0<a.indexOf(\".\")?(a=a.replace(/[.]/g,\":\"),a=a.split(\":\"),a[a.length-4]=parseInt(a[a.length-4])+256*parseInt(a[a.length-3]),a[a.length-3]=parseInt(a[a.length-2])+256*parseInt(a[a.length-\n1]),a=a.slice(0,a.length-2)):a=a.split(\":\");for(b=c=0;b<a.length;b++)if(\"string\"===typeof a[b])if(\"Z\"===a[b]){for(c=0;c<8-a.length+1;c++)d[b+c]=0;--c}else d[b+c]=Fc(parseInt(a[b],16));else d[b+c]=a[b];return[d[1]<<16|d[0],d[3]<<16|d[2],d[5]<<16|d[4],d[7]<<16|d[6]]}var Gc=1,Hc={},Ic={};\nfunction Jc(a){var b=Dc(a);if(null!==b)return a;b=Ec(a);if(null!==b)return a;Hc[a]?b=Hc[a]:(b=Gc++,assert(65535>b,\"exceeded max address mappings of 65535\"),b=\"172.29.\"+(b&255)+\".\"+(b&65280),Ic[b]=a,Hc[a]=b);return b}function Kc(a){return Ic[a]?Ic[a]:null}function Lc(a){return(a&255)+\".\"+(a>>8&255)+\".\"+(a>>16&255)+\".\"+(a>>24&255)}\nfunction Mc(a){var b=\"\",c,d=0,e=0,g=0,k=0;a=[a[0]&65535,a[0]>>16,a[1]&65535,a[1]>>16,a[2]&65535,a[2]>>16,a[3]&65535,a[3]>>16];var m=!0;for(c=0;5>c;c++)if(0!==a[c]){m=!1;break}if(m){c=Lc(a[6]|a[7]<<16);if(-1===a[5])return\"::ffff:\"+c;if(0===a[5])return\"0.0.0.0\"===c&&(c=\"\"),\"0.0.0.1\"===c&&(c=\"1\"),\"::\"+c}for(c=0;8>c;c++)0===a[c]&&(1<c-e&&(k=0),e=c,k++),k>d&&(d=k,g=c-d+1);for(c=0;8>c;c++)1<d&&0===a[c]&&c>=g&&c<g+d?c===g&&(b+=\":\",0===g&&(b+=\":\")):(b+=Number(Nc(a[c]&65535)).toString(16),b+=7>c?\":\":\"\");return b}\nfunction Oc(a,b){var c=Qa[a>>1],d=Nc(Ra[a+2>>1]);switch(c){case 2:if(16!==b)return{df:28};a=E[a+4>>2];a=Lc(a);break;case 10:if(28!==b)return{df:28};a=[E[a+8>>2],E[a+12>>2],E[a+16>>2],E[a+20>>2]];a=Mc(a);break;default:return{df:5}}return{family:c,gf:a,port:d}}\nfunction Pc(a,b,c,d){switch(b){case 2:c=Dc(c);Qa[a>>1]=b;E[a+4>>2]=c;Qa[a+2>>1]=Fc(d);break;case 10:c=Ec(c);E[a>>2]=b;E[a+8>>2]=c[0];E[a+12>>2]=c[1];E[a+16>>2]=c[2];E[a+20>>2]=c[3];Qa[a+2>>1]=Fc(d);E[a+4>>2]=0;E[a+24>>2]=0;break;default:return{df:5}}return{}}\nfunction Qc(a,b){if(l)return N(20,1,a,b);try{ic=b;b=function(){var aa=R.li(Q());if(!aa)throw new O.$e(8);return aa};var c=function(aa){var pd=Q(),ge=Q();if(aa&&0===pd)return null;aa=Oc(pd,ge);if(aa.df)throw new O.$e(aa.df);aa.gf=Kc(aa.gf)||aa.gf;return aa};switch(a){case 1:var d=Q(),e=Q(),g=Q(),k=R.createSocket(d,e,g);return k.stream.fd;case 2:k=b();var m=c();k.nf.bind(k,m.gf,m.port);return 0;case 3:return k=b(),m=c(),k.nf.connect(k,m.gf,m.port),0;case 4:k=b();var r=Q();k.nf.listen(k,r);return 0;\ncase 5:k=b();var q=Q();Q();var t=k.nf.accept(k);q&&Pc(q,t.family,Jc(t.rf),t.uf);return t.stream.fd;case 6:return k=b(),q=Q(),Q(),Pc(q,k.family,Jc(k.Ag||\"0.0.0.0\"),k.Pf),0;case 7:k=b();q=Q();Q();if(!k.rf)return-53;Pc(q,k.family,Jc(k.rf),k.uf);return 0;case 11:k=b();var w=Q(),B=Q();Q();var p=c(!0);return p?k.nf.eh(k,y,w,B,p.gf,p.port):O.write(k.stream,y,w,B);case 12:k=b();var x=Q(),z=Q();Q();q=Q();Q();var I=k.nf.ah(k,z);if(!I)return 0;q&&Pc(q,k.family,Jc(I.gf),I.port);v.set(I.buffer,x);return I.buffer.byteLength;\ncase 14:return-50;case 15:k=b();var W=Q(),db=Q(),K=Q(),Y=Q();return 1===W&&4===db?(E[K>>2]=k.error,E[Y>>2]=4,k.error=null,0):-50;case 16:k=b();w=Q();Q();var ia=E[w+8>>2],na=E[w+12>>2],ua=E[w>>2],he=E[w+4>>2];if(ua){m=Oc(ua,he);if(m.df)return-m.df;var ie=m.port;q=Kc(m.gf)||m.gf}for(var Oa=0,X=0;X<na;X++)Oa+=E[ia+(8*X+4)>>2];var qd=new Uint8Array(Oa);for(X=B=0;X<na;X++){var $b=E[ia+8*X>>2],ac=E[ia+(8*X+4)>>2];for(x=0;x<ac;x++)qd[B++]=y[$b+x>>0]}return k.nf.eh(k,qd,0,Oa,q,ie);case 17:k=b();w=Q();Q();\nia=E[w+8>>2];na=E[w+12>>2];for(X=Oa=0;X<na;X++)Oa+=E[ia+(8*X+4)>>2];I=k.nf.ah(k,Oa);if(!I)return 0;(ua=E[w>>2])&&Pc(ua,k.family,Jc(I.gf),I.port);k=0;var bc=I.buffer.byteLength;for(X=0;0<bc&&X<na;X++)if($b=E[ia+8*X>>2],ac=E[ia+(8*X+4)>>2])B=Math.min(ac,bc),x=I.buffer.subarray(k,k+B),v.set(x,$b+k),k+=B,bc-=B;return k;default:return-52}}catch(aa){return\"undefined\"!==typeof O&&aa instanceof O.$e||n(aa),-aa.df}}\nfunction Rc(a,b){if(l)return N(21,1,a,b);try{return a=C(a),hc(O.stat,a,b)}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}}function Sc(a){if(l)return N(22,1,a);try{return a=C(a),O.unlink(a),0}catch(b){return\"undefined\"!==typeof O&&b instanceof O.$e||n(b),-b.df}}function Tc(){void 0===Tc.start&&(Tc.start=Date.now());return 1E3*(Date.now()-Tc.start)|0}\nfunction Uc(){h||la||(za||(za={}),za[\"Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread\"]||(za[\"Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread\"]=1,u(\"Blocking on the main thread is very dangerous, see https://emscripten.org/docs/porting/pthreads.html#blocking-on-the-main-browser-thread\")))}\nfunction Vc(a,b,c){if(0>=a||a>y.length||a&1)return-28;if(ka){if(Atomics.load(E,a>>2)!=b)return-6;var d=performance.now();c=d+c;for(Atomics.exchange(E,M.Uf>>2,a);;){d=performance.now();if(d>c)return Atomics.exchange(E,M.Uf>>2,0),-73;d=Atomics.exchange(E,M.Uf>>2,0);if(0==d)break;Ab();if(Atomics.load(E,a>>2)!=b)return-6;Atomics.exchange(E,M.Uf>>2,a)}return 0}a=Atomics.wait(E,a>>2,b,c);if(\"timed-out\"===a)return-73;if(\"not-equal\"===a)return-6;if(\"ok\"===a)return 0;throw\"Atomics.wait returned an unexpected value \"+\na;}function Wc(a){var b=a.getExtension(\"ANGLE_instanced_arrays\");b&&(a.vertexAttribDivisor=function(c,d){b.vertexAttribDivisorANGLE(c,d)},a.drawArraysInstanced=function(c,d,e,g){b.drawArraysInstancedANGLE(c,d,e,g)},a.drawElementsInstanced=function(c,d,e,g,k){b.drawElementsInstancedANGLE(c,d,e,g,k)})}\nfunction Xc(a){var b=a.getExtension(\"OES_vertex_array_object\");b&&(a.createVertexArray=function(){return b.createVertexArrayOES()},a.deleteVertexArray=function(c){b.deleteVertexArrayOES(c)},a.bindVertexArray=function(c){b.bindVertexArrayOES(c)},a.isVertexArray=function(c){return b.isVertexArrayOES(c)})}function Yc(a){var b=a.getExtension(\"WEBGL_draw_buffers\");b&&(a.drawBuffers=function(c,d){b.drawBuffersWEBGL(c,d)})}var Zc=1,$c=[],S=[],ad=[],bd=[],cd=[],T=[],dd=[],ed=[],fd=[],gd={},hd={},id=4;\nfunction U(a){jd||(jd=a)}function kd(a){for(var b=Zc++,c=a.length;c<b;c++)a[c]=null;return b}\nfunction ld(a){a||(a=md);if(!a.ni){a.ni=!0;var b=a.pg;Wc(b);Xc(b);Yc(b);b.tf=b.getExtension(\"EXT_disjoint_timer_query\");b.qj=b.getExtension(\"WEBGL_multi_draw\");var c=\"OES_texture_float OES_texture_half_float OES_standard_derivatives OES_vertex_array_object WEBGL_compressed_texture_s3tc WEBGL_depth_texture OES_element_index_uint EXT_texture_filter_anisotropic EXT_frag_depth WEBGL_draw_buffers ANGLE_instanced_arrays OES_texture_float_linear OES_texture_half_float_linear EXT_blend_minmax EXT_shader_texture_lod EXT_texture_norm16 WEBGL_compressed_texture_pvrtc EXT_color_buffer_half_float WEBGL_color_buffer_float EXT_sRGB WEBGL_compressed_texture_etc1 EXT_disjoint_timer_query WEBGL_compressed_texture_etc WEBGL_compressed_texture_astc EXT_color_buffer_float WEBGL_compressed_texture_s3tc_srgb EXT_disjoint_timer_query_webgl2 WEBKIT_WEBGL_compressed_texture_pvrtc\".split(\" \");(b.getSupportedExtensions()||\n[]).forEach(function(d){-1!=c.indexOf(d)&&b.getExtension(d)})}}var jd,md,nd=[];function od(a,b,c,d){for(var e=0;e<a;e++){var g=V[c](),k=g&&kd(d);g?(g.name=k,d[k]=g):U(1282);E[b+4*e>>2]=k}}function rd(a,b,c,d,e,g,k,m){b=S[b];if(a=V[a](b,c))d=m&&Ia(a.name,v,m,d),e&&(E[e>>2]=d),g&&(E[g>>2]=a.size),k&&(E[k>>2]=a.type)}function sd(a,b){F[a>>2]=b;F[a+4>>2]=(b-F[a>>2])/4294967296}\nfunction td(a,b,c){if(b){var d=void 0;switch(a){case 36346:d=1;break;case 36344:0!=c&&1!=c&&U(1280);return;case 36345:d=0;break;case 34466:var e=V.getParameter(34467);d=e?e.length:0}if(void 0===d)switch(e=V.getParameter(a),typeof e){case \"number\":d=e;break;case \"boolean\":d=e?1:0;break;case \"string\":U(1280);return;case \"object\":if(null===e)switch(a){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 34068:d=0;break;default:U(1280);return}else{if(e instanceof Float32Array||\ne instanceof Uint32Array||e instanceof Int32Array||e instanceof Array){for(a=0;a<e.length;++a)switch(c){case 0:E[b+4*a>>2]=e[a];break;case 2:G[b+4*a>>2]=e[a];break;case 4:y[b+a>>0]=e[a]?1:0}return}try{d=e.name|0}catch(g){U(1280);u(\"GL_INVALID_ENUM in glGet\"+c+\"v: Unknown object returned from WebGL getParameter(\"+a+\")! (error: \"+g+\")\");return}}break;default:U(1280);u(\"GL_INVALID_ENUM in glGet\"+c+\"v: Native code calling glGet\"+c+\"v(\"+a+\") and it returns \"+e+\" of type \"+typeof e+\"!\");return}switch(c){case 1:sd(b,\nd);break;case 0:E[b>>2]=d;break;case 2:G[b>>2]=d;break;case 4:y[b>>0]=d?1:0}}else U(1281)}function ud(a){var b=Ka(a)+1,c=Ma(b);Ia(a,v,c,b);return c}function vd(a,b,c,d){if(c)if(a=V.getUniform(S[a],T[b]),\"number\"==typeof a||\"boolean\"==typeof a)switch(d){case 0:E[c>>2]=a;break;case 2:G[c>>2]=a}else for(b=0;b<a.length;b++)switch(d){case 0:E[c+4*b>>2]=a[b];break;case 2:G[c+4*b>>2]=a[b]}else U(1281)}\nfunction wd(a,b,c,d){if(c)if(a=V.getVertexAttrib(a,b),34975==b)E[c>>2]=a&&a.name;else if(\"number\"==typeof a||\"boolean\"==typeof a)switch(d){case 0:E[c>>2]=a;break;case 2:G[c>>2]=a;break;case 5:E[c>>2]=Math.fround(a)}else for(b=0;b<a.length;b++)switch(d){case 0:E[c+4*b>>2]=a[b];break;case 2:G[c+4*b>>2]=a[b];break;case 5:E[c+4*b>>2]=Math.fround(a[b])}else U(1281)}\nfunction xd(a,b,c,d,e){a-=5120;a=1==a?v:4==a?E:6==a?G:5==a||28922==a?F:Ra;var g=31-Math.clz32(a.BYTES_PER_ELEMENT),k=id;return a.subarray(e>>g,e+d*(c*({5:3,6:4,8:2,29502:3,29504:4}[b-6402]||1)*(1<<g)+k-1&-k)>>g)}var yd=[],zd=[];function N(a,b){for(var c=arguments.length-2,d=A(),e=Ha(8*c),g=e>>3,k=0;k<c;k++)Sa[g+k]=arguments[2+k];c=Ad(a,c,e,b);D(d);return c}var Bd=[],Cd=[],Dd=[0,\"undefined\"!==typeof document?document:0,\"undefined\"!==typeof window?window:0];\nfunction Ed(a){a=2<a?C(a):a;return Dd[a]||(\"undefined\"!==typeof document?document.querySelector(a):void 0)}\nfunction Fd(a,b,c){var d=Ed(a);if(!d)return-4;d.rg&&(E[d.rg>>2]=b,E[d.rg+4>>2]=c);if(d.Eh||!d.$i)d.Eh&&(d=d.Eh),a=!1,d.qg&&d.qg.pg&&(a=d.qg.pg.getParameter(2978),a=0===a[0]&&0===a[1]&&a[2]===d.width&&a[3]===d.height),d.width=b,d.height=c,a&&d.qg.pg.viewport(0,0,b,c);else{if(d.rg){a=a?C(a):\"\";d=E[d.rg+8>>2];var e=A(),g=Ha(12),k=0;a&&(k=ud(a));E[g>>2]=k;E[g+4>>2]=b;E[g+8>>2]=c;Gd(0,d,657457152,0,k,g);D(e);return 1}return-4}return 0}function Hd(a,b,c){return l?N(23,1,a,b,c):Fd(a,b,c)}\nvar Id=[\"default\",\"low-power\",\"high-performance\"],Jd={};function Kd(){if(!Ld){var a={USER:\"web_user\",LOGNAME:\"web_user\",PATH:\"/\",PWD:\"/\",HOME:\"/home/web_user\",LANG:(\"object\"===typeof navigator&&navigator.languages&&navigator.languages[0]||\"C\").replace(\"-\",\"_\")+\".UTF-8\",_:ha||\"./this.program\"},b;for(b in Jd)a[b]=Jd[b];var c=[];for(b in a)c.push(b+\"=\"+a[b]);Ld=c}return Ld}var Ld;\nfunction Md(a){if(l)return N(24,1,a);try{var b=jc(a);O.close(b);return 0}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),c.df}}function Nd(a,b){if(l)return N(25,1,a,b);try{var c=jc(a);y[b>>0]=c.tty?2:O.jf(c.mode)?3:O.Lf(c.mode)?7:4;return 0}catch(d){return\"undefined\"!==typeof O&&d instanceof O.$e||n(d),d.df}}\nfunction Od(a,b,c,d){if(l)return N(26,1,a,b,c,d);try{a:{for(var e=jc(a),g=a=0;g<c;g++){var k=E[b+(8*g+4)>>2],m=O.read(e,y,E[b+8*g>>2],k,void 0);if(0>m){var r=-1;break a}a+=m;if(m<k)break}r=a}E[d>>2]=r;return 0}catch(q){return\"undefined\"!==typeof O&&q instanceof O.$e||n(q),q.df}}\nfunction Pd(a,b,c,d,e){if(l)return N(27,1,a,b,c,d,e);try{var g=jc(a);a=4294967296*c+(b>>>0);if(-9007199254740992>=a||9007199254740992<=a)return-61;O.sf(g,a,d);L=[g.position>>>0,(J=g.position,1<=+Math.abs(J)?0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[e>>2]=L[0];E[e+4>>2]=L[1];g.Kf&&0===a&&0===d&&(g.Kf=null);return 0}catch(k){return\"undefined\"!==typeof O&&k instanceof O.$e||n(k),k.df}}\nfunction Qd(a,b,c,d){if(l)return N(28,1,a,b,c,d);try{a:{for(var e=jc(a),g=a=0;g<c;g++){var k=O.write(e,y,E[b+8*g>>2],E[b+(8*g+4)>>2],void 0);if(0>k){var m=-1;break a}a+=k}m=a}E[d>>2]=m;return 0}catch(r){return\"undefined\"!==typeof O&&r instanceof O.$e||n(r),r.df}}var Rd={};\nfunction Sd(a){Sd.buffer||(Sd.buffer=Ma(256),Rd[\"0\"]=\"Success\",Rd[\"-1\"]=\"Invalid value for 'ai_flags' field\",Rd[\"-2\"]=\"NAME or SERVICE is unknown\",Rd[\"-3\"]=\"Temporary failure in name resolution\",Rd[\"-4\"]=\"Non-recoverable failure in name res\",Rd[\"-6\"]=\"'ai_family' not supported\",Rd[\"-7\"]=\"'ai_socktype' not supported\",Rd[\"-8\"]=\"SERVICE not supported for 'ai_socktype'\",Rd[\"-10\"]=\"Memory allocation failure\",Rd[\"-11\"]=\"System error returned in 'errno'\",Rd[\"-12\"]=\"Argument buffer overflow\");var b=\"Unknown error\";\na in Rd&&(255<Rd[a].length?b=\"Message too long\":b=Rd[a]);Pa(b,Sd.buffer);return Sd.buffer}\nfunction Td(a,b,c,d){function e(w,B,p,x,z,I){var W=10===w?28:16;z=10===w?Mc(z):Lc(z);W=Ma(W);z=Pc(W,w,z,I);assert(!z.df);z=Ma(32);E[z+4>>2]=w;E[z+8>>2]=B;E[z+12>>2]=p;E[z+24>>2]=x;E[z+20>>2]=W;E[z+16>>2]=10===w?28:16;E[z+28>>2]=0;return z}if(l)return N(29,1,a,b,c,d);var g=0,k=0,m=0,r=0,q=0,t=0;c&&(m=E[c>>2],r=E[c+4>>2],q=E[c+8>>2],t=E[c+12>>2]);q&&!t&&(t=2===q?17:6);!q&&t&&(q=17===t?2:1);0===t&&(t=6);0===q&&(q=1);if(!a&&!b)return-2;if(m&-1088||0!==c&&E[c>>2]&2&&!a)return-1;if(m&32)return-2;if(0!==\nq&&1!==q&&2!==q)return-7;if(0!==r&&2!==r&&10!==r)return-6;if(b&&(b=C(b),k=parseInt(b,10),isNaN(k)))return m&1024?-2:-8;if(!a)return 0===r&&(r=2),0===(m&1)&&(2===r?g=Ud(2130706433):g=[0,0,0,1]),a=e(r,q,t,null,g,k),E[d>>2]=a,0;a=C(a);g=Dc(a);if(null!==g)if(0===r||2===r)r=2;else if(10===r&&m&8)g=[0,0,Ud(65535),g],r=10;else return-2;else if(g=Ec(a),null!==g)if(0===r||10===r)r=10;else return-2;if(null!=g)return a=e(r,q,t,a,g,k),E[d>>2]=a,0;if(m&4)return-2;a=Jc(a);g=Dc(a);0===r?r=2:10===r&&(g=[0,0,Ud(65535),\ng]);a=e(r,q,t,null,g,k);E[d>>2]=a;return 0}\nfunction Bb(a){if(l)throw\"Internal Error! spawnThread() can only ever be called from main application thread!\";var b=M.ki();if(void 0!==b.xf)throw\"Internal error!\";if(!a.Zf)throw\"Internal error, no pthread ptr!\";M.Jf.push(b);for(var c=Ma(512),d=0;128>d;++d)E[c+4*d>>2]=0;var e=a.Qf+a.bg;d=M.Df[a.Zf]={worker:b,Qf:a.Qf,bg:a.bg,Jg:a.Jg,Kh:a.Zf,threadInfoStruct:a.Zf};var g=d.threadInfoStruct>>2;Atomics.store(F,g,0);Atomics.store(F,g+1,0);Atomics.store(F,g+2,0);Atomics.store(F,g+17,a.detached);Atomics.store(F,\ng+26,c);Atomics.store(F,g+12,0);Atomics.store(F,g+10,d.threadInfoStruct);Atomics.store(F,g+11,42);Atomics.store(F,g+27,a.bg);Atomics.store(F,g+21,a.bg);Atomics.store(F,g+20,e);Atomics.store(F,g+29,e);Atomics.store(F,g+30,a.detached);Atomics.store(F,g+32,a.Hh);Atomics.store(F,g+33,a.Ih);c=Vd()+40;Atomics.store(F,g+44,c);b.xf=d;var k={cmd:\"run\",start_routine:a.Li,arg:a.Sf,threadInfoStruct:a.Zf,selfThreadId:a.Zf,parentThreadId:a.Ei,stackBase:a.Qf,stackSize:a.bg};b.ng=function(){k.time=performance.now();\nb.postMessage(k,a.Ui)};b.loaded&&(b.ng(),delete b.ng)}function Wd(){return pb|0}f._pthread_self=Wd;\nfunction Xd(a,b){if(!a)return u(\"pthread_join attempted on a null thread pointer!\"),71;if(l&&selfThreadId==a)return u(\"PThread \"+a+\" is attempting to join to itself!\"),16;if(!l&&M.wf==a)return u(\"Main thread \"+a+\" is attempting to join to itself!\"),16;if(E[a+12>>2]!==a)return u(\"pthread_join attempted on thread \"+a+\", which does not point to a valid thread, or does not exist anymore!\"),71;if(Atomics.load(F,a+68>>2))return u(\"Attempted to join thread \"+a+\", which was already detached!\"),28;for(Uc();;){var c=\nAtomics.load(F,a>>2);if(1==c)return c=Atomics.load(F,a+4>>2),b&&(E[b>>2]=c),Atomics.store(F,a+68>>2,1),l?postMessage({cmd:\"cleanupThread\",thread:a}):vb(a),0;if(l&&threadInfoStruct&&!Atomics.load(F,threadInfoStruct+60>>2)&&2==Atomics.load(F,threadInfoStruct+0>>2))throw\"Canceled!\";l||Ab();Vc(a,c,l?100:1)}}function Yd(a){return 0===a%4&&(0!==a%100||0===a%400)}function Zd(a,b){for(var c=0,d=0;d<=b;c+=a[d++]);return c}var $d=[31,29,31,30,31,30,31,31,30,31,30,31],ae=[31,28,31,30,31,30,31,31,30,31,30,31];\nfunction be(a,b){for(a=new Date(a.getTime());0<b;){var c=a.getMonth(),d=(Yd(a.getFullYear())?$d:ae)[c];if(b>d-a.getDate())b-=d-a.getDate()+1,a.setDate(1),11>c?a.setMonth(c+1):(a.setMonth(0),a.setFullYear(a.getFullYear()+1));else{a.setDate(a.getDate()+b);break}}return a}\nfunction ce(a){if(l)return N(30,1,a);switch(a){case 30:return 16384;case 85:return v.length/16384;case 132:case 133:case 12:case 137:case 138:case 15:case 235:case 16:case 17:case 18:case 19:case 20:case 149:case 13:case 10:case 236:case 153:case 9:case 21:case 22:case 159:case 154:case 14:case 77:case 78:case 139:case 80:case 81:case 82:case 68:case 67:case 164:case 11:case 29:case 47:case 48:case 95:case 52:case 51:case 46:case 79:return 200809;case 27:case 246:case 127:case 128:case 23:case 24:case 160:case 161:case 181:case 182:case 242:case 183:case 184:case 243:case 244:case 245:case 165:case 178:case 179:case 49:case 50:case 168:case 169:case 175:case 170:case 171:case 172:case 97:case 76:case 32:case 173:case 35:return-1;\ncase 176:case 177:case 7:case 155:case 8:case 157:case 125:case 126:case 92:case 93:case 129:case 130:case 131:case 94:case 91:return 1;case 74:case 60:case 69:case 70:case 4:return 1024;case 31:case 42:case 72:return 32;case 87:case 26:case 33:return 2147483647;case 34:case 1:return 47839;case 38:case 36:return 99;case 43:case 37:return 2048;case 0:return 2097152;case 3:return 65536;case 28:return 32768;case 44:return 32767;case 75:return 16384;case 39:return 1E3;case 89:return 700;case 71:return 256;\ncase 40:return 255;case 2:return 100;case 180:return 64;case 25:return 20;case 5:return 16;case 6:return 6;case 73:return 4;case 84:return\"object\"===typeof navigator?navigator.hardwareConcurrency||1:1}Eb(28);return-1}function de(a,b,c,d){a||(a=this);this.parent=a;this.hf=a.hf;this.kg=null;this.id=O.zi++;this.name=b;this.mode=c;this.bf={};this.cf={};this.rdev=d}\nObject.defineProperties(de.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}},si:{get:function(){return O.jf(this.mode)}},Tg:{get:function(){return O.gg(this.mode)}}});O.Nh=de;O.Mi();for(var ec,V,ee=0;32>ee;++ee)nd.push(Array(ee));var fe=new Float32Array(288);for(ee=0;288>ee;++ee)yd[ee]=fe.subarray(0,ee+1);var je=new Int32Array(288);\nfor(ee=0;288>ee;++ee)zd[ee]=je.subarray(0,ee+1);var ke=[null,Hb,Jb,kc,lc,mc,nc,oc,pc,rc,sc,tc,uc,wc,xc,yc,zc,Ac,Bc,Cc,Qc,Rc,Sc,Hd,Md,Nd,Od,Pd,Qd,Td,ce];function Zb(a,b){var c=Array(Ka(a)+1);a=Ia(a,c,0,c.length);b&&(c.length=a);return c}l||Wa.push({uh:function(){le()}});\nvar Fe={c:function(a,b,c,d){n(\"Assertion failed: \"+C(a)+\", at: \"+[b?C(b):\"unknown filename\",c,d?C(d):\"unknown function\"])},K:function(a,b){a=me(a,b);if(!noExitRuntime)return postMessage({cmd:\"exitProcess\",returnCode:a}),a},W:function(a,b){return Gb(a,b)},aa:function(a,b){return Hb(a,b)},wa:function(a,b){return Ib(a,b)},va:function(a,b){return Nb(a,b)},Ma:kc,Ea:lc,u:mc,Na:nc,Ka:oc,Ha:pc,V:rc,Oa:sc,Pa:tc,za:uc,Aa:wc,Da:function(){return-63},Y:xc,La:yc,Ja:zc,Ca:Ac,xa:Bc,Ga:Cc,Ia:function(){return 0},\nt:Qc,X:Rc,Fa:function(a){try{if(!a)return-21;var b={__size__:390,sysname:0,nodename:65,release:130,version:195,machine:260,domainname:325};Pa(\"Emscripten\",a+b.sysname);Pa(\"emscripten\",a+b.nodename);Pa(\"1.0\",a+b.release);Pa(\"#1\",a+b.version);Pa(\"x86-JS\",a+b.machine);return 0}catch(c){return\"undefined\"!==typeof O&&c instanceof O.$e||n(c),-c.df}},Ba:Sc,qa:function(a,b){if(a==b)postMessage({cmd:\"processQueuedMainThreadWork\"});else if(l)postMessage({targetThread:a,cmd:\"processThreadQueue\"});else{a=(a=\nM.Df[a])&&a.worker;if(!a)return;a.postMessage({cmd:\"processThreadQueue\"})}return 1},b:function(){n()},Qa:Tc,Ta:Gb,$:function(){n(\"To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking\")},Ua:function(){n(\"To use dlopen, you need to use Emscripten's linking support, see https://github.com/emscripten-core/emscripten/wiki/Linking\")},F:function(a,b,c){Cd.length=0;var d;for(c>>=2;d=v[b++];)(d=105>d)&&c&1&&c++,Cd.push(d?Sa[c++>>1]:E[c]),\n++c;return mb[a].apply(null,Cd)},ra:Uc,I:function(){},A:Vc,p:tb,z:Db,Ed:function(a){V.activeTexture(a)},Dd:function(a,b){V.attachShader(S[a],dd[b])},fa:function(a,b){V.tf.beginQueryEXT(a,fd[b])},Cd:function(a,b,c){V.bindAttribLocation(S[a],b,C(c))},Bd:function(a,b){V.bindBuffer(a,$c[b])},Ad:function(a,b){V.bindFramebuffer(a,ad[b])},zd:function(a,b){V.bindRenderbuffer(a,bd[b])},yd:function(a,b){V.bindTexture(a,cd[b])},Md:function(a){V.bindVertexArray(ed[a])},xd:function(a,b,c,d){V.blendColor(a,b,c,\nd)},wd:function(a){V.blendEquation(a)},vd:function(a,b){V.blendEquationSeparate(a,b)},ud:function(a,b){V.blendFunc(a,b)},td:function(a,b,c,d){V.blendFuncSeparate(a,b,c,d)},sd:function(a,b,c,d){V.bufferData(a,c?v.subarray(c,c+b):b,d)},rd:function(a,b,c,d){V.bufferSubData(a,b,v.subarray(d,d+c))},qd:function(a){return V.checkFramebufferStatus(a)},pd:function(a){V.clear(a)},od:function(a,b,c,d){V.clearColor(a,b,c,d)},nd:function(a){V.clearDepth(a)},md:function(a){V.clearStencil(a)},ld:function(a,b,c,\nd){V.colorMask(!!a,!!b,!!c,!!d)},kd:function(a){V.compileShader(dd[a])},jd:function(a,b,c,d,e,g,k,m){V.compressedTexImage2D(a,b,c,d,e,g,m?v.subarray(m,m+k):null)},id:function(a,b,c,d,e,g,k,m,r){V.compressedTexSubImage2D(a,b,c,d,e,g,k,r?v.subarray(r,r+m):null)},hd:function(a,b,c,d,e,g,k,m){V.copyTexImage2D(a,b,c,d,e,g,k,m)},gd:function(a,b,c,d,e,g,k,m){V.copyTexSubImage2D(a,b,c,d,e,g,k,m)},fd:function(){var a=kd(S),b=V.createProgram();b.name=a;S[a]=b;return a},ed:function(a){var b=kd(dd);dd[b]=V.createShader(a);\nreturn b},dd:function(a){V.cullFace(a)},cd:function(a,b){for(var c=0;c<a;c++){var d=E[b+4*c>>2],e=$c[d];e&&(V.deleteBuffer(e),e.name=0,$c[d]=null)}},bd:function(a,b){for(var c=0;c<a;++c){var d=E[b+4*c>>2],e=ad[d];e&&(V.deleteFramebuffer(e),e.name=0,ad[d]=null)}},ad:function(a){if(a){var b=S[a];b?(V.deleteProgram(b),b.name=0,S[a]=null,gd[a]=null):U(1281)}},ha:function(a,b){for(var c=0;c<a;c++){var d=E[b+4*c>>2],e=fd[d];e&&(V.tf.deleteQueryEXT(e),fd[d]=null)}},$c:function(a,b){for(var c=0;c<a;c++){var d=\nE[b+4*c>>2],e=bd[d];e&&(V.deleteRenderbuffer(e),e.name=0,bd[d]=null)}},_c:function(a){if(a){var b=dd[a];b?(V.deleteShader(b),dd[a]=null):U(1281)}},Zc:function(a,b){for(var c=0;c<a;c++){var d=E[b+4*c>>2],e=cd[d];e&&(V.deleteTexture(e),e.name=0,cd[d]=null)}},Ld:function(a,b){for(var c=0;c<a;c++){var d=E[b+4*c>>2];V.deleteVertexArray(ed[d]);ed[d]=null}},Yc:function(a){V.depthFunc(a)},Xc:function(a){V.depthMask(!!a)},Wc:function(a,b){V.depthRange(a,b)},Vc:function(a,b){V.detachShader(S[a],dd[b])},Uc:function(a){V.disable(a)},\nTc:function(a){V.disableVertexAttribArray(a)},Sc:function(a,b,c){V.drawArrays(a,b,c)},Hd:function(a,b,c,d){V.drawArraysInstanced(a,b,c,d)},Id:function(a,b){for(var c=nd[a],d=0;d<a;d++)c[d]=E[b+4*d>>2];V.drawBuffers(c)},Rc:function(a,b,c,d){V.drawElements(a,b,c,d)},Gd:function(a,b,c,d,e){V.drawElementsInstanced(a,b,c,d,e)},Qc:function(a){V.enable(a)},Pc:function(a){V.enableVertexAttribArray(a)},ea:function(a){V.tf.endQueryEXT(a)},Oc:function(){V.finish()},Nc:function(){V.flush()},Mc:function(a,b,c,\nd){V.framebufferRenderbuffer(a,b,c,bd[d])},Lc:function(a,b,c,d,e){V.framebufferTexture2D(a,b,c,cd[d],e)},Kc:function(a){V.frontFace(a)},Jc:function(a,b){od(a,b,\"createBuffer\",$c)},Hc:function(a,b){od(a,b,\"createFramebuffer\",ad)},ia:function(a,b){for(var c=0;c<a;c++){var d=V.tf.createQueryEXT();if(!d){for(U(1282);c<a;)E[b+4*c++>>2]=0;break}var e=kd(fd);d.name=e;fd[e]=d;E[b+4*c>>2]=e}},Gc:function(a,b){od(a,b,\"createRenderbuffer\",bd)},Fc:function(a,b){od(a,b,\"createTexture\",cd)},Kd:function(a,b){od(a,\nb,\"createVertexArray\",ed)},Ic:function(a){V.generateMipmap(a)},Ec:function(a,b,c,d,e,g,k){rd(\"getActiveAttrib\",a,b,c,d,e,g,k)},Dc:function(a,b,c,d,e,g,k){rd(\"getActiveUniform\",a,b,c,d,e,g,k)},Cc:function(a,b,c,d){a=V.getAttachedShaders(S[a]);var e=a.length;e>b&&(e=b);E[c>>2]=e;for(b=0;b<e;++b)E[d+4*b>>2]=dd.indexOf(a[b])},Bc:function(a,b){return V.getAttribLocation(S[a],C(b))},Ac:function(a,b){td(a,b,4)},zc:function(a,b,c){c?E[c>>2]=V.getBufferParameter(a,b):U(1281)},yc:function(){var a=V.getError()||\njd;jd=0;return a},xc:function(a,b){td(a,b,2)},wc:function(a,b,c,d){a=V.getFramebufferAttachmentParameter(a,b,c);if(a instanceof WebGLRenderbuffer||a instanceof WebGLTexture)a=a.name|0;E[d>>2]=a},vc:function(a,b){td(a,b,0)},tc:function(a,b,c,d){a=V.getProgramInfoLog(S[a]);null===a&&(a=\"(unknown error)\");b=0<b&&d?Ia(a,v,d,b):0;c&&(E[c>>2]=b)},uc:function(a,b,c){if(c)if(a>=Zc)U(1281);else{var d=gd[a];if(d)if(35716==b)a=V.getProgramInfoLog(S[a]),null===a&&(a=\"(unknown error)\"),E[c>>2]=a.length+1;else if(35719==\nb)E[c>>2]=d.Wg;else if(35722==b){if(-1==d.ig){a=S[a];var e=V.getProgramParameter(a,35721);for(b=d.ig=0;b<e;++b)d.ig=Math.max(d.ig,V.getActiveAttrib(a,b).name.length+1)}E[c>>2]=d.ig}else if(35381==b){if(-1==d.jg)for(a=S[a],e=V.getProgramParameter(a,35382),b=d.jg=0;b<e;++b)d.jg=Math.max(d.jg,V.getActiveUniformBlockName(a,b).length+1);E[c>>2]=d.jg}else E[c>>2]=V.getProgramParameter(S[a],b);else U(1282)}else U(1281)},Od:function(a,b,c){if(c){a=V.tf.getQueryObjectEXT(fd[a],b);var d;\"boolean\"==typeof a?\nd=a?1:0:d=a;sd(c,d)}else U(1281)},Qd:function(a,b,c){if(c){a=V.tf.getQueryObjectEXT(fd[a],b);var d;\"boolean\"==typeof a?d=a?1:0:d=a;E[c>>2]=d}else U(1281)},Nd:function(a,b,c){if(c){a=V.tf.getQueryObjectEXT(fd[a],b);var d;\"boolean\"==typeof a?d=a?1:0:d=a;sd(c,d)}else U(1281)},Pd:function(a,b,c){if(c){a=V.tf.getQueryObjectEXT(fd[a],b);var d;\"boolean\"==typeof a?d=a?1:0:d=a;E[c>>2]=d}else U(1281)},ca:function(a,b,c){c?E[c>>2]=V.tf.getQueryEXT(a,b):U(1281)},sc:function(a,b,c){c?E[c>>2]=V.getRenderbufferParameter(a,\nb):U(1281)},qc:function(a,b,c,d){a=V.getShaderInfoLog(dd[a]);null===a&&(a=\"(unknown error)\");b=0<b&&d?Ia(a,v,d,b):0;c&&(E[c>>2]=b)},pc:function(a,b,c,d){a=V.getShaderPrecisionFormat(a,b);E[c>>2]=a.rangeMin;E[c+4>>2]=a.rangeMax;E[d>>2]=a.precision},oc:function(a,b,c,d){if(a=V.getShaderSource(dd[a]))b=0<b&&d?Ia(a,v,d,b):0,c&&(E[c>>2]=b)},rc:function(a,b,c){c?35716==b?(a=V.getShaderInfoLog(dd[a]),null===a&&(a=\"(unknown error)\"),E[c>>2]=a?a.length+1:0):35720==b?(a=V.getShaderSource(dd[a]),E[c>>2]=a?a.length+\n1:0):E[c>>2]=V.getShaderParameter(dd[a],b):U(1281)},nc:function(a){if(hd[a])return hd[a];switch(a){case 7939:var b=V.getSupportedExtensions()||[];b=b.concat(b.map(function(d){return\"GL_\"+d}));b=ud(b.join(\" \"));break;case 7936:case 7937:case 37445:case 37446:(b=V.getParameter(a))||U(1280);b=ud(b);break;case 7938:b=ud(\"OpenGL ES 2.0 (\"+V.getParameter(7938)+\")\");break;case 35724:b=V.getParameter(35724);var c=b.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9]?)(?:$| .*)/);null!==c&&(3==c[1].length&&(c[1]+=\"0\"),\nb=\"OpenGL ES GLSL ES \"+c[1]+\" (\"+b+\")\");b=ud(b);break;default:return U(1280),0}return hd[a]=b},mc:function(a,b,c){c?G[c>>2]=V.getTexParameter(a,b):U(1281)},lc:function(a,b,c){c?E[c>>2]=V.getTexParameter(a,b):U(1281)},ic:function(a,b){b=C(b);var c=0;if(\"]\"==b[b.length-1]){var d=b.lastIndexOf(\"[\");c=\"]\"!=b[d+1]?parseInt(b.slice(d+1)):0;b=b.slice(0,d)}return(a=gd[a]&&gd[a].Mh[b])&&0<=c&&c<a[0]?a[1]+c:-1},kc:function(a,b,c){vd(a,b,c,2)},jc:function(a,b,c){vd(a,b,c,0)},fc:function(a,b,c){c?E[c>>2]=V.getVertexAttribOffset(a,\nb):U(1281)},hc:function(a,b,c){wd(a,b,c,2)},gc:function(a,b,c){wd(a,b,c,5)},ec:function(a,b){V.hint(a,b)},dc:function(a){return(a=$c[a])?V.isBuffer(a):0},cc:function(a){return V.isEnabled(a)},bc:function(a){return(a=ad[a])?V.isFramebuffer(a):0},ac:function(a){return(a=S[a])?V.isProgram(a):0},ga:function(a){return(a=fd[a])?V.tf.isQueryEXT(a):0},$b:function(a){return(a=bd[a])?V.isRenderbuffer(a):0},_b:function(a){return(a=dd[a])?V.isShader(a):0},Zb:function(a){return(a=cd[a])?V.isTexture(a):0},Jd:function(a){return(a=\ned[a])?V.isVertexArray(a):0},Yb:function(a){V.lineWidth(a)},Xb:function(a){V.linkProgram(S[a]);var b=S[a];a=gd[a]={Mh:{},Wg:0,ig:-1,jg:-1};for(var c=a.Mh,d=V.getProgramParameter(b,35718),e=0;e<d;++e){var g=V.getActiveUniform(b,e),k=g.name;a.Wg=Math.max(a.Wg,k.length+1);\"]\"==k.slice(-1)&&(k=k.slice(0,k.lastIndexOf(\"[\")));var m=V.getUniformLocation(b,k);if(m){var r=kd(T);c[k]=[g.size,r];T[r]=m;for(var q=1;q<g.size;++q)m=V.getUniformLocation(b,k+\"[\"+q+\"]\"),r=kd(T),T[r]=m}}},Wb:function(a,b){3317==a&&\n(id=b);V.pixelStorei(a,b)},Vb:function(a,b){V.polygonOffset(a,b)},da:function(a,b){V.tf.queryCounterEXT(fd[a],b)},Ub:function(a,b,c,d,e,g,k){(k=xd(g,e,c,d,k))?V.readPixels(a,b,c,d,e,g,k):U(1280)},Tb:function(){},Sb:function(a,b,c,d){V.renderbufferStorage(a,b,c,d)},Rb:function(a,b){V.sampleCoverage(a,!!b)},Qb:function(a,b,c,d){V.scissor(a,b,c,d)},Pb:function(){U(1280)},Ob:function(a,b,c,d){for(var e=\"\",g=0;g<b;++g){var k=d?E[d+4*g>>2]:-1;e+=C(E[c+4*g>>2],0>k?void 0:k)}V.shaderSource(dd[a],e)},Nb:function(a,\nb,c){V.stencilFunc(a,b,c)},Mb:function(a,b,c,d){V.stencilFuncSeparate(a,b,c,d)},Lb:function(a){V.stencilMask(a)},Kb:function(a,b){V.stencilMaskSeparate(a,b)},Jb:function(a,b,c){V.stencilOp(a,b,c)},Ib:function(a,b,c,d){V.stencilOpSeparate(a,b,c,d)},Hb:function(a,b,c,d,e,g,k,m,r){V.texImage2D(a,b,c,d,e,g,k,m,r?xd(m,k,d,e,r):null)},Gb:function(a,b,c){V.texParameterf(a,b,c)},Fb:function(a,b,c){V.texParameterf(a,b,G[c>>2])},Eb:function(a,b,c){V.texParameteri(a,b,c)},Db:function(a,b,c){V.texParameteri(a,\nb,E[c>>2])},Cb:function(a,b,c,d,e,g,k,m,r){var q=null;r&&(q=xd(m,k,e,g,r));V.texSubImage2D(a,b,c,d,e,g,k,m,q)},Bb:function(a,b){V.uniform1f(T[a],b)},Ab:function(a,b,c){if(288>=b)for(var d=yd[b-1],e=0;e<b;++e)d[e]=G[c+4*e>>2];else d=G.subarray(c>>2,c+4*b>>2);V.uniform1fv(T[a],d)},zb:function(a,b){V.uniform1i(T[a],b)},yb:function(a,b,c){if(288>=b)for(var d=zd[b-1],e=0;e<b;++e)d[e]=E[c+4*e>>2];else d=E.subarray(c>>2,c+4*b>>2);V.uniform1iv(T[a],d)},xb:function(a,b,c){V.uniform2f(T[a],b,c)},wb:function(a,\nb,c){if(144>=b)for(var d=yd[2*b-1],e=0;e<2*b;e+=2)d[e]=G[c+4*e>>2],d[e+1]=G[c+(4*e+4)>>2];else d=G.subarray(c>>2,c+8*b>>2);V.uniform2fv(T[a],d)},vb:function(a,b,c){V.uniform2i(T[a],b,c)},ub:function(a,b,c){if(144>=b)for(var d=zd[2*b-1],e=0;e<2*b;e+=2)d[e]=E[c+4*e>>2],d[e+1]=E[c+(4*e+4)>>2];else d=E.subarray(c>>2,c+8*b>>2);V.uniform2iv(T[a],d)},tb:function(a,b,c,d){V.uniform3f(T[a],b,c,d)},sb:function(a,b,c){if(96>=b)for(var d=yd[3*b-1],e=0;e<3*b;e+=3)d[e]=G[c+4*e>>2],d[e+1]=G[c+(4*e+4)>>2],d[e+2]=\nG[c+(4*e+8)>>2];else d=G.subarray(c>>2,c+12*b>>2);V.uniform3fv(T[a],d)},rb:function(a,b,c,d){V.uniform3i(T[a],b,c,d)},qb:function(a,b,c){if(96>=b)for(var d=zd[3*b-1],e=0;e<3*b;e+=3)d[e]=E[c+4*e>>2],d[e+1]=E[c+(4*e+4)>>2],d[e+2]=E[c+(4*e+8)>>2];else d=E.subarray(c>>2,c+12*b>>2);V.uniform3iv(T[a],d)},pb:function(a,b,c,d,e){V.uniform4f(T[a],b,c,d,e)},ob:function(a,b,c){if(72>=b){var d=yd[4*b-1];c>>=2;for(var e=0;e<4*b;e+=4){var g=c+e;d[e]=G[g];d[e+1]=G[g+1];d[e+2]=G[g+2];d[e+3]=G[g+3]}}else d=G.subarray(c>>\n2,c+16*b>>2);V.uniform4fv(T[a],d)},nb:function(a,b,c,d,e){V.uniform4i(T[a],b,c,d,e)},mb:function(a,b,c){if(72>=b)for(var d=zd[4*b-1],e=0;e<4*b;e+=4)d[e]=E[c+4*e>>2],d[e+1]=E[c+(4*e+4)>>2],d[e+2]=E[c+(4*e+8)>>2],d[e+3]=E[c+(4*e+12)>>2];else d=E.subarray(c>>2,c+16*b>>2);V.uniform4iv(T[a],d)},lb:function(a,b,c,d){if(72>=b)for(var e=yd[4*b-1],g=0;g<4*b;g+=4)e[g]=G[d+4*g>>2],e[g+1]=G[d+(4*g+4)>>2],e[g+2]=G[d+(4*g+8)>>2],e[g+3]=G[d+(4*g+12)>>2];else e=G.subarray(d>>2,d+16*b>>2);V.uniformMatrix2fv(T[a],\n!!c,e)},kb:function(a,b,c,d){if(32>=b)for(var e=yd[9*b-1],g=0;g<9*b;g+=9)e[g]=G[d+4*g>>2],e[g+1]=G[d+(4*g+4)>>2],e[g+2]=G[d+(4*g+8)>>2],e[g+3]=G[d+(4*g+12)>>2],e[g+4]=G[d+(4*g+16)>>2],e[g+5]=G[d+(4*g+20)>>2],e[g+6]=G[d+(4*g+24)>>2],e[g+7]=G[d+(4*g+28)>>2],e[g+8]=G[d+(4*g+32)>>2];else e=G.subarray(d>>2,d+36*b>>2);V.uniformMatrix3fv(T[a],!!c,e)},jb:function(a,b,c,d){if(18>=b){var e=yd[16*b-1];d>>=2;for(var g=0;g<16*b;g+=16){var k=d+g;e[g]=G[k];e[g+1]=G[k+1];e[g+2]=G[k+2];e[g+3]=G[k+3];e[g+4]=G[k+4];\ne[g+5]=G[k+5];e[g+6]=G[k+6];e[g+7]=G[k+7];e[g+8]=G[k+8];e[g+9]=G[k+9];e[g+10]=G[k+10];e[g+11]=G[k+11];e[g+12]=G[k+12];e[g+13]=G[k+13];e[g+14]=G[k+14];e[g+15]=G[k+15]}}else e=G.subarray(d>>2,d+64*b>>2);V.uniformMatrix4fv(T[a],!!c,e)},ib:function(a){V.useProgram(S[a])},hb:function(a){V.validateProgram(S[a])},gb:function(a,b){V.vertexAttrib1f(a,b)},fb:function(a,b){V.vertexAttrib1f(a,G[b>>2])},eb:function(a,b,c){V.vertexAttrib2f(a,b,c)},db:function(a,b){V.vertexAttrib2f(a,G[b>>2],G[b+4>>2])},cb:function(a,\nb,c,d){V.vertexAttrib3f(a,b,c,d)},bb:function(a,b){V.vertexAttrib3f(a,G[b>>2],G[b+4>>2],G[b+8>>2])},ab:function(a,b,c,d,e){V.vertexAttrib4f(a,b,c,d,e)},$a:function(a,b){V.vertexAttrib4f(a,G[b>>2],G[b+4>>2],G[b+8>>2],G[b+12>>2])},Fd:function(a,b){V.vertexAttribDivisor(a,b)},_a:function(a,b,c,d,e,g){V.vertexAttribPointer(a,b,c,!!d,e,g)},Za:function(a,b,c,d){V.viewport(a,b,c,d)},la:function(){return\"undefined\"!==typeof SharedArrayBuffer},G:function(){return rb|0},R:function(){return qb|0},f:function(a,\nb){Z(a,b||1);throw\"longjmp\";},ka:function(a,b,c){v.copyWithin(a,b,b+c)},na:function(a,b,c){Bd.length=b;c>>=3;for(var d=0;d<b;d++)Bd[d]=Sa[c+d];return(0>a?mb[-a-1]:ke[a]).apply(null,Bd)},sa:function(){n(\"OOM\")},oa:function(a,b,c){return Ed(a)?Fd(a,b,c):Hd(a,b,c)},Q:function(){},ma:function(){},pa:function(a,b){var c={};b>>=2;c.alpha=!!E[b];c.depth=!!E[b+1];c.stencil=!!E[b+2];c.antialias=!!E[b+3];c.premultipliedAlpha=!!E[b+4];c.preserveDrawingBuffer=!!E[b+5];c.powerPreference=Id[E[b+6]];c.failIfMajorPerformanceCaveat=\n!!E[b+7];c.vi=E[b+8];c.oj=E[b+9];c.ph=E[b+10];c.gi=E[b+11];c.tj=E[b+12];c.uj=E[b+13];a=Ed(a);if(!a||c.gi)c=0;else if(a=a.getContext(\"webgl\",c)){b=Ma(8);E[b+4>>2]=pb|0;var d={ij:b,attributes:c,version:c.vi,pg:a};a.canvas&&(a.canvas.qg=d);(\"undefined\"===typeof c.ph||c.ph)&&ld(d);c=b}else c=0;return c},ta:function(a,b){var c=0;Kd().forEach(function(d,e){var g=b+c;E[a+4*e>>2]=g;Pa(d,g);c+=d.length+1});return 0},ua:function(a,b){var c=Kd();E[a>>2]=c.length;var d=0;c.forEach(function(e){d+=e.length+1});\nE[b>>2]=d;return 0},D:function(a){Cb(a)},H:Md,U:Nd,ya:Od,Va:Pd,M:Qd,B:Sd,d:function(){return Aa|0},x:Td,v:function(a,b,c,d,e,g,k){b=Oc(a,b);if(b.df)return-6;a=b.port;var m=b.gf;b=!1;if(c&&d){var r;if(k&1||!(r=Kc(m))){if(k&8)return-2}else m=r;c=Ia(m,v,c,d);c+1>=d&&(b=!0)}e&&g&&(c=Ia(\"\"+a,v,e,g),c+1>=g&&(b=!0));return b?-12:0},l:function(a){var b=Date.now();E[a>>2]=b/1E3|0;E[a+4>>2]=b%1E3*1E3|0;return 0},r:Ib,ja:function(){M.oi()},ba:ne,j:oe,h:pe,C:qe,P:re,_:se,O:te,Xa:ue,Wa:ve,k:we,w:xe,J:ye,g:ze,\nN:Ae,Sa:Be,Z:Ce,Ya:De,q:Nb,a:Ca||f.wasmMemory,T:function(a){Jb();var b=new Date(E[a+20>>2]+1900,E[a+16>>2],E[a+12>>2],E[a+8>>2],E[a+4>>2],E[a>>2],0),c=E[a+32>>2],d=b.getTimezoneOffset(),e=new Date(b.getFullYear(),0,1),g=(new Date(b.getFullYear(),6,1)).getTimezoneOffset(),k=e.getTimezoneOffset(),m=Math.min(k,g);0>c?E[a+32>>2]=Number(g!=k&&m==d):0<c!=(m==d)&&(g=Math.max(k,g),b.setTime(b.getTime()+6E4*((0<c?m:g)-d)));E[a+24>>2]=b.getDay();E[a+28>>2]=(b.getTime()-e.getTime())/864E5|0;return b.getTime()/\n1E3|0},Ra:function(a){if(a===M.Oh)return u(\"Main thread (id=\"+a+\") cannot be canceled!\"),71;if(!a)return u(\"pthread_cancel attempted on a null thread pointer!\"),71;if(E[a+12>>2]!==a)return u(\"pthread_cancel attempted on thread \"+a+\", which does not point to a valid thread, or does not exist anymore!\"),71;Atomics.compareExchange(F,a>>2,0,2);l?postMessage({cmd:\"cancelThread\",thread:a}):ub(a);return 0},S:function(a){var b=M.Cg.pop();a&&b()},L:function(a,b){M.Cg.push(function(){H.get(a)(b)})},n:function(a,\nb,c,d){if(\"undefined\"===typeof SharedArrayBuffer)return u(\"Current environment does not support SharedArrayBuffer, pthreads are not available!\"),6;if(!a)return u(\"pthread_create called with a null thread pointer!\"),28;var e=[];if(l&&0===e.length)return Ee(687865856,a,b,c,d);var g=0,k=0,m=0,r=0;if(b){var q=E[b>>2];q+=81920;g=E[b+8>>2];k=0!==E[b+12>>2];if(0===E[b+16>>2]){var t=E[b+20>>2],w=E[b+24>>2];m=b+20;r=b+24;var B=M.Lg?M.Lg:pb|0;if(m||r)if(B)if(E[B+12>>2]!==B)u(\"pthread_getschedparam attempted on thread \"+\nB+\", which does not point to a valid thread, or does not exist anymore!\");else{var p=Atomics.load(F,B+128>>2);B=Atomics.load(F,B+132>>2);m&&(E[m>>2]=p);r&&(E[r>>2]=B)}else u(\"pthread_getschedparam called with a null thread pointer!\");m=E[b+20>>2];r=E[b+24>>2];E[b+20>>2]=t;E[b+24>>2]=w}else m=E[b+20>>2],r=E[b+24>>2]}else q=2097152;(b=0==g)?g=vc(16,q):(g-=q,assert(0<g));t=Ma(232);for(w=0;58>w;++w)F[(t>>2)+w]=0;E[a>>2]=t;E[t+12>>2]=t;a=t+156;E[a>>2]=a;c={Qf:g,bg:q,Jg:b,Hh:m,Ih:r,detached:k,Li:c,Zf:t,\nEi:pb|0,Sf:d,Ui:e};l?(c.Zi=\"spawnThread\",postMessage(c,e)):Bb(c);return 0},o:function(a,b){return Xd(a,b)},i:Wd,e:function(a){Aa=a|0},E:function(){return 0},m:function(a,b,c,d){function e(p,x,z){for(p=\"number\"===typeof p?p.toString():p||\"\";p.length<x;)p=z[0]+p;return p}function g(p,x){return e(p,x,\"0\")}function k(p,x){function z(W){return 0>W?-1:0<W?1:0}var I;0===(I=z(p.getFullYear()-x.getFullYear()))&&0===(I=z(p.getMonth()-x.getMonth()))&&(I=z(p.getDate()-x.getDate()));return I}function m(p){switch(p.getDay()){case 0:return new Date(p.getFullYear()-\n1,11,29);case 1:return p;case 2:return new Date(p.getFullYear(),0,3);case 3:return new Date(p.getFullYear(),0,2);case 4:return new Date(p.getFullYear(),0,1);case 5:return new Date(p.getFullYear()-1,11,31);case 6:return new Date(p.getFullYear()-1,11,30)}}function r(p){p=be(new Date(p.qf+1900,0,1),p.Fg);var x=new Date(p.getFullYear()+1,0,4),z=m(new Date(p.getFullYear(),0,4));x=m(x);return 0>=k(z,p)?0>=k(x,p)?p.getFullYear()+1:p.getFullYear():p.getFullYear()-1}var q=E[d+40>>2];d={Si:E[d>>2],Ri:E[d+4>>\n2],Dg:E[d+8>>2],og:E[d+12>>2],cg:E[d+16>>2],qf:E[d+20>>2],Eg:E[d+24>>2],Fg:E[d+28>>2],xj:E[d+32>>2],Qi:E[d+36>>2],Ti:q?C(q):\"\"};c=C(c);q={\"%c\":\"%a %b %d %H:%M:%S %Y\",\"%D\":\"%m/%d/%y\",\"%F\":\"%Y-%m-%d\",\"%h\":\"%b\",\"%r\":\"%I:%M:%S %p\",\"%R\":\"%H:%M\",\"%T\":\"%H:%M:%S\",\"%x\":\"%m/%d/%y\",\"%X\":\"%H:%M:%S\",\"%Ec\":\"%c\",\"%EC\":\"%C\",\"%Ex\":\"%m/%d/%y\",\"%EX\":\"%H:%M:%S\",\"%Ey\":\"%y\",\"%EY\":\"%Y\",\"%Od\":\"%d\",\"%Oe\":\"%e\",\"%OH\":\"%H\",\"%OI\":\"%I\",\"%Om\":\"%m\",\"%OM\":\"%M\",\"%OS\":\"%S\",\"%Ou\":\"%u\",\"%OU\":\"%U\",\"%OV\":\"%V\",\"%Ow\":\"%w\",\"%OW\":\"%W\",\"%Oy\":\"%y\"};\nfor(var t in q)c=c.replace(new RegExp(t,\"g\"),q[t]);var w=\"Sunday Monday Tuesday Wednesday Thursday Friday Saturday\".split(\" \"),B=\"January February March April May June July August September October November December\".split(\" \");q={\"%a\":function(p){return w[p.Eg].substring(0,3)},\"%A\":function(p){return w[p.Eg]},\"%b\":function(p){return B[p.cg].substring(0,3)},\"%B\":function(p){return B[p.cg]},\"%C\":function(p){return g((p.qf+1900)/100|0,2)},\"%d\":function(p){return g(p.og,2)},\"%e\":function(p){return e(p.og,\n2,\" \")},\"%g\":function(p){return r(p).toString().substring(2)},\"%G\":function(p){return r(p)},\"%H\":function(p){return g(p.Dg,2)},\"%I\":function(p){p=p.Dg;0==p?p=12:12<p&&(p-=12);return g(p,2)},\"%j\":function(p){return g(p.og+Zd(Yd(p.qf+1900)?$d:ae,p.cg-1),3)},\"%m\":function(p){return g(p.cg+1,2)},\"%M\":function(p){return g(p.Ri,2)},\"%n\":function(){return\"\\n\"},\"%p\":function(p){return 0<=p.Dg&&12>p.Dg?\"AM\":\"PM\"},\"%S\":function(p){return g(p.Si,2)},\"%t\":function(){return\"\\t\"},\"%u\":function(p){return p.Eg||\n7},\"%U\":function(p){var x=new Date(p.qf+1900,0,1),z=0===x.getDay()?x:be(x,7-x.getDay());p=new Date(p.qf+1900,p.cg,p.og);return 0>k(z,p)?g(Math.ceil((31-z.getDate()+(Zd(Yd(p.getFullYear())?$d:ae,p.getMonth()-1)-31)+p.getDate())/7),2):0===k(z,x)?\"01\":\"00\"},\"%V\":function(p){var x=new Date(p.qf+1901,0,4),z=m(new Date(p.qf+1900,0,4));x=m(x);var I=be(new Date(p.qf+1900,0,1),p.Fg);return 0>k(I,z)?\"53\":0>=k(x,I)?\"01\":g(Math.ceil((z.getFullYear()<p.qf+1900?p.Fg+32-z.getDate():p.Fg+1-z.getDate())/7),2)},\"%w\":function(p){return p.Eg},\n\"%W\":function(p){var x=new Date(p.qf,0,1),z=1===x.getDay()?x:be(x,0===x.getDay()?1:7-x.getDay()+1);p=new Date(p.qf+1900,p.cg,p.og);return 0>k(z,p)?g(Math.ceil((31-z.getDate()+(Zd(Yd(p.getFullYear())?$d:ae,p.getMonth()-1)-31)+p.getDate())/7),2):0===k(z,x)?\"01\":\"00\"},\"%y\":function(p){return(p.qf+1900).toString().substring(2)},\"%Y\":function(p){return p.qf+1900},\"%z\":function(p){p=p.Qi;var x=0<=p;p=Math.abs(p)/60;return(x?\"+\":\"-\")+String(\"0000\"+(p/60*100+p%60)).slice(-4)},\"%Z\":function(p){return p.Ti},\n\"%%\":function(){return\"%\"}};for(t in q)0<=c.indexOf(t)&&(c=c.replace(new RegExp(t,\"g\"),q[t](d)));t=Zb(c,!1);if(t.length>b)return 0;y.set(t,a);return t.length-1},y:ce,s:function(a){var b=Date.now()/1E3|0;a&&(E[a>>2]=b);return b}};\n(function(){function a(e,g){f.asm=e.exports;H=f.asm.Rd;Da=g;l||fb()}function b(e){a(e.instance,e.module)}function c(e){return kb().then(function(g){return WebAssembly.instantiate(g,d)}).then(e,function(g){u(\"failed to asynchronously prepare wasm: \"+g);n(g)})}var d={a:Fe};l||eb();if(f.instantiateWasm)try{return f.instantiateWasm(d,a)}catch(e){return u(\"Module.instantiateWasm callback failed with error: \"+e),!1}(function(){return Ba||\"function\"!==typeof WebAssembly.instantiateStreaming||ib()||gb(\"file://\")||\n\"function\"!==typeof fetch?c(b):fetch(hb,{credentials:\"same-origin\"}).then(function(e){return WebAssembly.instantiateStreaming(e,d).then(b,function(g){u(\"wasm streaming compile failed: \"+g);u(\"falling back to ArrayBuffer instantiation\");return c(b)})})})().catch(ca);return{}})();\nvar le=f.___wasm_call_ctors=function(){return(le=f.___wasm_call_ctors=f.asm.Sd).apply(null,arguments)},zb=f._free=function(){return(zb=f._free=f.asm.Td).apply(null,arguments)},Ma=f._malloc=function(){return(Ma=f._malloc=f.asm.Ud).apply(null,arguments)},Fb=f.___errno_location=function(){return(Fb=f.___errno_location=f.asm.Vd).apply(null,arguments)},qc=f._memset=function(){return(qc=f._memset=f.asm.Wd).apply(null,arguments)};f._fflush=function(){return(f._fflush=f.asm.Xd).apply(null,arguments)};\nvar vc=f._memalign=function(){return(vc=f._memalign=f.asm.Yd).apply(null,arguments)},Nc=f._ntohs=function(){return(Nc=f._ntohs=f.asm.Zd).apply(null,arguments)},Fc=f._htons=function(){return(Fc=f._htons=f.asm._d).apply(null,arguments)},me=f._main=function(){return(me=f._main=f.asm.$d).apply(null,arguments)},Vd=f._emscripten_get_global_libc=function(){return(Vd=f._emscripten_get_global_libc=f.asm.ae).apply(null,arguments)};\nf.___em_js__initPthreadsJS=function(){return(f.___em_js__initPthreadsJS=f.asm.be).apply(null,arguments)};\nvar Ud=f._htonl=function(){return(Ud=f._htonl=f.asm.ce).apply(null,arguments)},Mb=f.__get_tzname=function(){return(Mb=f.__get_tzname=f.asm.de).apply(null,arguments)},Lb=f.__get_daylight=function(){return(Lb=f.__get_daylight=f.asm.ee).apply(null,arguments)},Kb=f.__get_timezone=function(){return(Kb=f.__get_timezone=f.asm.fe).apply(null,arguments)},A=f.stackSave=function(){return(A=f.stackSave=f.asm.ge).apply(null,arguments)},D=f.stackRestore=function(){return(D=f.stackRestore=f.asm.he).apply(null,arguments)},\nHa=f.stackAlloc=function(){return(Ha=f.stackAlloc=f.asm.ie).apply(null,arguments)},Z=f._setThrew=function(){return(Z=f._setThrew=f.asm.je).apply(null,arguments)};f._emscripten_main_browser_thread_id=function(){return(f._emscripten_main_browser_thread_id=f.asm.ke).apply(null,arguments)};\nvar yb=f.___pthread_tsd_run_dtors=function(){return(yb=f.___pthread_tsd_run_dtors=f.asm.le).apply(null,arguments)},Ab=f._emscripten_main_thread_process_queued_calls=function(){return(Ab=f._emscripten_main_thread_process_queued_calls=f.asm.me).apply(null,arguments)};f._emscripten_current_thread_process_queued_calls=function(){return(f._emscripten_current_thread_process_queued_calls=f.asm.ne).apply(null,arguments)};\nvar wb=f._emscripten_register_main_browser_thread_id=function(){return(wb=f._emscripten_register_main_browser_thread_id=f.asm.oe).apply(null,arguments)},lb=f._do_emscripten_dispatch_to_thread=function(){return(lb=f._do_emscripten_dispatch_to_thread=f.asm.pe).apply(null,arguments)};f._emscripten_async_run_in_main_thread=function(){return(f._emscripten_async_run_in_main_thread=f.asm.qe).apply(null,arguments)};\nf._emscripten_sync_run_in_main_thread=function(){return(f._emscripten_sync_run_in_main_thread=f.asm.re).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_0=function(){return(f._emscripten_sync_run_in_main_thread_0=f.asm.se).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_1=function(){return(f._emscripten_sync_run_in_main_thread_1=f.asm.te).apply(null,arguments)};\nf._emscripten_sync_run_in_main_thread_2=function(){return(f._emscripten_sync_run_in_main_thread_2=f.asm.ue).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_xprintf_varargs=function(){return(f._emscripten_sync_run_in_main_thread_xprintf_varargs=f.asm.ve).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_3=function(){return(f._emscripten_sync_run_in_main_thread_3=f.asm.we).apply(null,arguments)};\nvar Ee=f._emscripten_sync_run_in_main_thread_4=function(){return(Ee=f._emscripten_sync_run_in_main_thread_4=f.asm.xe).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_5=function(){return(f._emscripten_sync_run_in_main_thread_5=f.asm.ye).apply(null,arguments)};f._emscripten_sync_run_in_main_thread_6=function(){return(f._emscripten_sync_run_in_main_thread_6=f.asm.ze).apply(null,arguments)};\nf._emscripten_sync_run_in_main_thread_7=function(){return(f._emscripten_sync_run_in_main_thread_7=f.asm.Ae).apply(null,arguments)};var Ad=f._emscripten_run_in_main_runtime_thread_js=function(){return(Ad=f._emscripten_run_in_main_runtime_thread_js=f.asm.Be).apply(null,arguments)},Gd=f.__emscripten_call_on_thread=function(){return(Gd=f.__emscripten_call_on_thread=f.asm.Ce).apply(null,arguments)};f._proxy_main=function(){return(f._proxy_main=f.asm.De).apply(null,arguments)};\nf._emscripten_tls_init=function(){return(f._emscripten_tls_init=f.asm.Ee).apply(null,arguments)};f.dynCall_ijiii=function(){return(f.dynCall_ijiii=f.asm.Fe).apply(null,arguments)};var Ge=f.dynCall_vijjjid=function(){return(Ge=f.dynCall_vijjjid=f.asm.Ge).apply(null,arguments)},He=f.dynCall_iiiijj=function(){return(He=f.dynCall_iiiijj=f.asm.He).apply(null,arguments)};f.dynCall_iiijiii=function(){return(f.dynCall_iiijiii=f.asm.Ie).apply(null,arguments)};\nf.dynCall_jiiii=function(){return(f.dynCall_jiiii=f.asm.Je).apply(null,arguments)};f.dynCall_jii=function(){return(f.dynCall_jii=f.asm.Ke).apply(null,arguments)};var Ie=f.dynCall_iij=function(){return(Ie=f.dynCall_iij=f.asm.Le).apply(null,arguments)};f.dynCall_viiijj=function(){return(f.dynCall_viiijj=f.asm.Me).apply(null,arguments)};f.dynCall_jij=function(){return(f.dynCall_jij=f.asm.Ne).apply(null,arguments)};f.dynCall_jiji=function(){return(f.dynCall_jiji=f.asm.Oe).apply(null,arguments)};\nf.dynCall_iiiji=function(){return(f.dynCall_iiiji=f.asm.Pe).apply(null,arguments)};f.dynCall_iiiiij=function(){return(f.dynCall_iiiiij=f.asm.Qe).apply(null,arguments)};f.dynCall_jiiij=function(){return(f.dynCall_jiiij=f.asm.Re).apply(null,arguments)};f.dynCall_iiijjji=function(){return(f.dynCall_iiijjji=f.asm.Se).apply(null,arguments)};f.dynCall_iiiiiij=function(){return(f.dynCall_iiiiiij=f.asm.Te).apply(null,arguments)};f.dynCall_jiiji=function(){return(f.dynCall_jiiji=f.asm.Ue).apply(null,arguments)};\nf.dynCall_viiiiijji=function(){return(f.dynCall_viiiiijji=f.asm.Ve).apply(null,arguments)};f.dynCall_viiiji=function(){return(f.dynCall_viiiji=f.asm.We).apply(null,arguments)};f.dynCall_jiiiii=function(){return(f.dynCall_jiiiii=f.asm.Xe).apply(null,arguments)};f.dynCall_jiii=function(){return(f.dynCall_jiii=f.asm.Ye).apply(null,arguments)};f.dynCall_jiiiiii=function(){return(f.dynCall_jiiiiii=f.asm.Ze).apply(null,arguments)};f._ff_h264_cabac_tables=2115942;var xb=f._main_thread_futex=16983448;\nfunction pe(a,b,c){var d=A();try{return H.get(a)(b,c)}catch(e){D(d);if(e!==e+0&&\"longjmp\"!==e)throw e;Z(1,0)}}function we(a,b){var c=A();try{H.get(a)(b)}catch(d){D(c);if(d!==d+0&&\"longjmp\"!==d)throw d;Z(1,0)}}function ze(a,b,c,d,e){var g=A();try{H.get(a)(b,c,d,e)}catch(k){D(g);if(k!==k+0&&\"longjmp\"!==k)throw k;Z(1,0)}}function xe(a,b,c){var d=A();try{H.get(a)(b,c)}catch(e){D(d);if(e!==e+0&&\"longjmp\"!==e)throw e;Z(1,0)}}\nfunction oe(a,b){var c=A();try{return H.get(a)(b)}catch(d){D(c);if(d!==d+0&&\"longjmp\"!==d)throw d;Z(1,0)}}function re(a,b,c,d,e){var g=A();try{return H.get(a)(b,c,d,e)}catch(k){D(g);if(k!==k+0&&\"longjmp\"!==k)throw k;Z(1,0)}}function te(a,b,c,d,e,g,k,m,r){var q=A();try{return H.get(a)(b,c,d,e,g,k,m,r)}catch(t){D(q);if(t!==t+0&&\"longjmp\"!==t)throw t;Z(1,0)}}function ye(a,b,c,d){var e=A();try{H.get(a)(b,c,d)}catch(g){D(e);if(g!==g+0&&\"longjmp\"!==g)throw g;Z(1,0)}}\nfunction ne(a){var b=A();try{return H.get(a)()}catch(c){D(b);if(c!==c+0&&\"longjmp\"!==c)throw c;Z(1,0)}}function Ae(a,b,c,d,e,g){var k=A();try{H.get(a)(b,c,d,e,g)}catch(m){D(k);if(m!==m+0&&\"longjmp\"!==m)throw m;Z(1,0)}}function qe(a,b,c,d){var e=A();try{return H.get(a)(b,c,d)}catch(g){D(e);if(g!==g+0&&\"longjmp\"!==g)throw g;Z(1,0)}}function se(a,b,c,d,e,g){var k=A();try{return H.get(a)(b,c,d,e,g)}catch(m){D(k);if(m!==m+0&&\"longjmp\"!==m)throw m;Z(1,0)}}\nfunction Ce(a,b,c,d,e,g,k,m,r){var q=A();try{H.get(a)(b,c,d,e,g,k,m,r)}catch(t){D(q);if(t!==t+0&&\"longjmp\"!==t)throw t;Z(1,0)}}function Be(a,b,c,d,e,g,k){var m=A();try{H.get(a)(b,c,d,e,g,k)}catch(r){D(m);if(r!==r+0&&\"longjmp\"!==r)throw r;Z(1,0)}}function De(a,b,c,d,e,g,k,m,r,q){var t=A();try{Ge(a,b,c,d,e,g,k,m,r,q)}catch(w){D(t);if(w!==w+0&&\"longjmp\"!==w)throw w;Z(1,0)}}function ue(a,b,c,d,e,g,k,m){var r=A();try{return He(a,b,c,d,e,g,k,m)}catch(q){D(r);if(q!==q+0&&\"longjmp\"!==q)throw q;Z(1,0)}}\nfunction ve(a,b,c,d){var e=A();try{return Ie(a,b,c,d)}catch(g){D(e);if(g!==g+0&&\"longjmp\"!==g)throw g;Z(1,0)}}f.ccall=Ga;f.cwrap=function(a,b,c,d){c=c||[];var e=c.every(function(g){return\"number\"===g});return\"string\"!==b&&e&&!d?Fa(a):function(){return Ga(a,b,c,arguments,d)}};\nf.setValue=function(a,b,c){c=c||\"i8\";\"*\"===c.charAt(c.length-1)&&(c=\"i32\");switch(c){case \"i1\":y[a>>0]=b;break;case \"i8\":y[a>>0]=b;break;case \"i16\":Qa[a>>1]=b;break;case \"i32\":E[a>>2]=b;break;case \"i64\":L=[b>>>0,(J=b,1<=+Math.abs(J)?0<J?(Math.min(+Math.floor(J/4294967296),4294967295)|0)>>>0:~~+Math.ceil((J-+(~~J>>>0))/4294967296)>>>0:0)];E[a>>2]=L[0];E[a+4>>2]=L[1];break;case \"float\":G[a>>2]=b;break;case \"double\":Sa[a>>3]=b;break;default:n(\"invalid type for setValue: \"+c)}};f.writeAsciiToMemory=Pa;\nf.FS=O;f.PThread=M;f.PThread=M;f._pthread_self=Wd;f.wasmMemory=Ca;f.ExitStatus=wa;var Je;function wa(a){this.name=\"ExitStatus\";this.message=\"Program terminated with exit(\"+a+\")\";this.status=a}cb=function Ke(){Je||Le();Je||(cb=Ke)};\nfunction Le(a){function b(){if(!Je&&(Je=!0,f.calledRun=!0,!Ea)){f.noFSInit||O.fg.Sg||O.fg();R.root=O.hf(R,{},null);nb(Wa);l||(O.Ah=!1,nb(Xa));ba(f);if(f.onRuntimeInitialized)f.onRuntimeInitialized();if(Me){var c=a;c=c||[];var d=c.length+1,e=Ha(4*(d+1));E[e>>2]=Na(ha);for(var g=1;g<d;g++)E[(e>>2)+g]=Na(c[g-1]);E[(e>>2)+d]=0;f._proxy_main(d,e)}if(!l){if(f.postRun)for(\"function\"==typeof f.postRun&&(f.postRun=[f.postRun]);f.postRun.length;)c=f.postRun.shift(),Za.unshift(c);nb(Za)}}}a=a||fa;if(!(0<ab)){if(!l){if(f.preRun)for(\"function\"==\ntypeof f.preRun&&(f.preRun=[f.preRun]);f.preRun.length;)$a();nb(Va)}0<ab||(f.setStatus?(f.setStatus(\"Running...\"),setTimeout(function(){setTimeout(function(){f.setStatus(\"\")},1);b()},1)):b())}}f.run=Le;function Cb(a){if(!noExitRuntime){M.Ni();l||(nb(Ya),O.quit(),M.dh());if(f.onExit)f.onExit(a);Ea=!0}ja(a,new wa(a))}if(f.preInit)for(\"function\"==typeof f.preInit&&(f.preInit=[f.preInit]);0<f.preInit.length;)f.preInit.pop()();var Me=!1;f.noInitialRun&&(Me=!1);l?M.pi():Le();\n\n\n  return createFFmpegCore.ready\n}\n);\n})();\nif (typeof exports === 'object' && typeof module === 'object')\n      module.exports = createFFmpegCore;\n    else if (typeof define === 'function' && define['amd'])\n      define([], function() { return createFFmpegCore; });\n    else if (typeof exports === 'object')\n      exports[\"createFFmpegCore\"] = createFFmpegCore;\n    "
  },
  {
    "path": "src/assets/vendor/ffmpeg-core.worker.js",
    "content": "var threadInfoStruct=0;var selfThreadId=0;var parentThreadId=0;var Module={};function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(\" \");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(\" \");postMessage({cmd:\"alert\",text:text,threadId:selfThreadId})}var err=threadPrintErr;this.alert=threadAlert;Module[\"instantiateWasm\"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module[\"wasmModule\"],info);Module[\"wasmModule\"]=null;receiveInstance(instance);return instance.exports};this.onmessage=function(e){try{if(e.data.cmd===\"load\"){Module[\"wasmModule\"]=e.data.wasmModule;Module[\"wasmMemory\"]=e.data.wasmMemory;Module[\"buffer\"]=Module[\"wasmMemory\"].buffer;Module[\"ENVIRONMENT_IS_PTHREAD\"]=true;if(typeof e.data.urlOrBlob===\"string\"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}createFFmpegCore(Module).then(function(instance){Module=instance;postMessage({\"cmd\":\"loaded\"})})}else if(e.data.cmd===\"objectTransfer\"){Module[\"PThread\"].receiveObjectTransfer(e.data)}else if(e.data.cmd===\"run\"){Module[\"__performance_now_clock_drift\"]=performance.now()-e.data.time;threadInfoStruct=e.data.threadInfoStruct;Module[\"registerPthreadPtr\"](threadInfoStruct,/*isMainBrowserThread=*/0,/*isMainRuntimeThread=*/0);selfThreadId=e.data.selfThreadId;parentThreadId=e.data.parentThreadId;var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module[\"establishStackSpace\"](top,max);Module[\"_emscripten_tls_init\"]();Module[\"PThread\"].receiveObjectTransfer(e.data);Module[\"PThread\"].setThreadStatus(Module[\"_pthread_self\"](),1);/*EM_THREAD_STATUS_RUNNING*/try{var result=Module[\"dynCall\"](\"ii\",e.data.start_routine,[e.data.arg]);if(!Module[\"getNoExitRuntime\"]())Module[\"PThread\"].threadExit(result)}catch(ex){if(ex===\"Canceled!\"){Module[\"PThread\"].threadCancel()}else if(ex!=\"unwind\"){Atomics.store(Module[\"HEAPU32\"],(threadInfoStruct+4)>>/*C_STRUCTS.pthread.threadExitCode*/2,(ex instanceof Module[\"ExitStatus\"])?ex.status:-2);/*A custom entry specific to Emscripten denoting that the thread crashed.*/Atomics.store(Module[\"HEAPU32\"],(threadInfoStruct+0)>>/*C_STRUCTS.pthread.threadStatus*/2,1);Module[\"_emscripten_futex_wake\"](threadInfoStruct+0,/*C_STRUCTS.pthread.threadStatus*/2147483647);if(!(ex instanceof Module[\"ExitStatus\"]))throw ex}}}else if(e.data.cmd===\"cancel\"){if(threadInfoStruct){Module[\"PThread\"].threadCancel()}}else if(e.data.target===\"setimmediate\"){}else if(e.data.cmd===\"processThreadQueue\"){if(threadInfoStruct){Module[\"_emscripten_current_thread_process_queued_calls\"]()}}else{err(\"worker.js received unknown command \"+e.data.cmd);err(e.data)}}catch(ex){err(\"worker.js onmessage() captured an uncaught exception: \"+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};if(typeof process===\"object\"&&typeof process.versions===\"object\"&&typeof process.versions.node===\"string\"){self={location:{href:__filename}};var onmessage=this.onmessage;var nodeWorkerThreads=require(\"worker_threads\");global.Worker=nodeWorkerThreads.Worker;var parentPort=nodeWorkerThreads.parentPort;parentPort.on(\"message\",function(data){onmessage({data:data})});var nodeFS=require(\"fs\");var nodeRead=function(filename){return nodeFS.readFileSync(filename,\"utf8\")};function globalEval(x){global.require=require;global.Module=Module;eval.call(null,x)}importScripts=function(f){globalEval(nodeRead(f))};postMessage=function(msg){parentPort.postMessage(msg)};if(typeof performance===\"undefined\"){performance={now:function(){return Date.now()}}}}\n"
  },
  {
    "path": "src/assets/vendor/gif.js/gif.worker.js",
    "content": "// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js\n(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){var NeuQuant=require(\"./TypedNeuQuant.js\");var LZWEncoder=require(\"./LZWEncoder.js\");function ByteArray(){this.page=-1;this.pages=[];this.newPage()}ByteArray.pageSize=4096;ByteArray.charMap={};for(var i=0;i<256;i++)ByteArray.charMap[i]=String.fromCharCode(i);ByteArray.prototype.newPage=function(){this.pages[++this.page]=new Uint8Array(ByteArray.pageSize);this.cursor=0};ByteArray.prototype.getData=function(){var rv=\"\";for(var p=0;p<this.pages.length;p++){for(var i=0;i<ByteArray.pageSize;i++){rv+=ByteArray.charMap[this.pages[p][i]]}}return rv};ByteArray.prototype.writeByte=function(val){if(this.cursor>=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i<l;i++)this.writeByte(string.charCodeAt(i))};ByteArray.prototype.writeBytes=function(array,offset,length){for(var l=length||array.length,i=offset||0;i<l;i++)this.writeByte(array[i])};function GIFEncoder(width,height){this.width=~~width;this.height=~~height;this.transparent=null;this.transIndex=0;this.repeat=-1;this.delay=0;this.image=null;this.pixels=null;this.indexedPixels=null;this.colorDepth=null;this.colorTab=null;this.neuQuant=null;this.usedEntry=new Array;this.palSize=7;this.dispose=-1;this.firstFrame=true;this.sample=10;this.dither=false;this.globalPalette=false;this.out=new ByteArray}GIFEncoder.prototype.setDelay=function(milliseconds){this.delay=Math.round(milliseconds/10)};GIFEncoder.prototype.setFrameRate=function(fps){this.delay=Math.round(100/fps)};GIFEncoder.prototype.setDispose=function(disposalCode){if(disposalCode>=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither=\"FloydSteinberg\";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes(\"GIF89a\")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace(\"-serpentine\",\"\"),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j<nPix;j++){var index=this.findClosestRGB(this.pixels[k++]&255,this.pixels[k++]&255,this.pixels[k++]&255);this.usedEntry[index]=true;this.indexedPixels[j]=index}};GIFEncoder.prototype.ditherPixels=function(kernel,serpentine){var kernels={FalseFloydSteinberg:[[3/8,1,0],[3/8,0,1],[2/8,1,1]],FloydSteinberg:[[7/16,1,0],[3/16,-1,1],[5/16,0,1],[1/16,1,1]],Stucki:[[8/42,1,0],[4/42,2,0],[2/42,-2,1],[4/42,-1,1],[8/42,0,1],[4/42,1,1],[2/42,2,1],[1/42,-2,2],[2/42,-1,2],[4/42,0,2],[2/42,1,2],[1/42,2,2]],Atkinson:[[1/8,1,0],[1/8,2,0],[1/8,-1,1],[1/8,0,1],[1/8,1,1],[1/8,0,2]]};if(!kernel||!kernels[kernel]){throw\"Unknown dithering kernel: \"+kernel}var ds=kernels[kernel];var index=0,height=this.height,width=this.width,data=this.pixels;var direction=serpentine?-1:1;this.indexedPixels=new Uint8Array(this.pixels.length/3);for(var y=0;y<height;y++){if(serpentine)direction=direction*-1;for(var x=direction==1?0:width-1,xend=direction==1?width:0;x!==xend;x+=direction){index=y*width+x;var idx=index*3;var r1=data[idx];var g1=data[idx+1];var b1=data[idx+2];idx=this.findClosestRGB(r1,g1,b1);this.usedEntry[idx]=true;this.indexedPixels[index]=idx;idx*=3;var r2=this.colorTab[idx];var g2=this.colorTab[idx+1];var b2=this.colorTab[idx+2];var er=r1-r2;var eg=g1-g2;var eb=b1-b2;for(var i=direction==1?0:ds.length-1,end=direction==1?ds.length:0;i!==end;i+=direction){var x1=ds[i][1];var y1=ds[i][2];if(x1+x>=0&&x1+x<width&&y1+y>=0&&y1+y<height){var d=ds[i][0];idx=index+x1+y1*width;idx*=3;data[idx]=Math.max(0,Math.min(255,data[idx]+er*d));data[idx+1]=Math.max(0,Math.min(255,data[idx+1]+eg*d));data[idx+2]=Math.max(0,Math.min(255,data[idx+2]+eb*d))}}}}};GIFEncoder.prototype.findClosest=function(c,used){return this.findClosestRGB((c&16711680)>>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i<len;index++){var dr=r-(this.colorTab[i++]&255);var dg=g-(this.colorTab[i++]&255);var db=b-(this.colorTab[i++]&255);var d=dr*dr+dg*dg+db*db;if((!used||this.usedEntry[index])&&d<dmin){dmin=d;minpos=index}}return minpos};GIFEncoder.prototype.getImagePixels=function(){var w=this.width;var h=this.height;this.pixels=new Uint8Array(w*h*3);var data=this.image;var srcPos=0;var count=0;for(var i=0;i<h;i++){for(var j=0;j<w;j++){this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];srcPos++}}};GIFEncoder.prototype.writeGraphicCtrlExt=function(){this.out.writeByte(33);this.out.writeByte(249);this.out.writeByte(4);var transp,disp;if(this.transparent===null){transp=0;disp=0}else{transp=1;disp=2}if(this.dispose>=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes(\"NETSCAPE2.0\");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i<n;i++)this.out.writeByte(0)};GIFEncoder.prototype.writeShort=function(pValue){this.out.writeByte(pValue&255);this.out.writeByte(pValue>>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{\"./LZWEncoder.js\":2,\"./TypedNeuQuant.js\":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i<hsize;++i)htab[i]=-1}function compress(init_bits,outs){var fcode,c,i,ent,disp,hsize_reg,hshift;g_init_bits=init_bits;clear_flg=false;n_bits=g_init_bits;maxcode=MAXCODE(n_bits);ClearCode=1<<init_bits-1;EOFCode=ClearCode+1;free_ent=ClearCode+2;a_count=0;ent=nextPixel();hshift=0;for(fcode=HSIZE;fcode<65536;fcode*=2)++hshift;hshift=8-hshift;hsize_reg=HSIZE;cl_hash(hsize_reg);output(ClearCode,outs);outer_loop:while((c=nextPixel())!=EOF){fcode=(c<<BITS)+ent;i=c<<hshift^ent;if(htab[i]===fcode){ent=codetab[i];continue}else if(htab[i]>=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<<BITS){codetab[i]=free_ent++;htab[i]=fcode}else{cl_block(outs)}}output(ent,outs);output(EOFCode,outs)}function encode(outs){outs.writeByte(initCodeSize);remaining=width*height;curPixel=0;compress(initCodeSize+1,outs);outs.writeByte(0)}function flush_char(outs){if(a_count>0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<<n_bits)-1}function nextPixel(){if(remaining===0)return EOF;--remaining;var pix=pixels[curPixel++];return pix&255}function output(code,outs){cur_accum&=masks[cur_bits];if(cur_bits>0)cur_accum|=code<<cur_bits;else cur_accum=code;cur_bits+=n_bits;while(cur_bits>=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<<BITS;else maxcode=MAXCODE(n_bits)}}if(code==EOFCode){while(cur_bits>0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<<intbiasshift;var gammashift=10;var gamma=1<<gammashift;var betashift=10;var beta=intbias>>betashift;var betagamma=intbias<<gammashift-betashift;var initrad=netsize>>3;var radiusbiasshift=6;var radiusbias=1<<radiusbiasshift;var initradius=initrad*radiusbias;var radiusdec=30;var alphabiasshift=10;var initalpha=1<<alphabiasshift;var alphadec;var radbiasshift=8;var radbias=1<<radbiasshift;var alpharadbshift=alphabiasshift+radbiasshift;var alpharadbias=1<<alpharadbshift;var prime1=499;var prime2=491;var prime3=487;var prime4=503;var minpicturebytes=3*prime4;function NeuQuant(pixels,samplefac){var network;var netindex;var bias;var freq;var radpower;function init(){network=[];netindex=new Int32Array(256);bias=new Int32Array(netsize);freq=new Int32Array(netsize);radpower=new Int32Array(netsize>>3);var i,v;for(i=0;i<netsize;i++){v=(i<<netbiasshift+8)/netsize;network[i]=new Float64Array([v,v,v,0]);freq[i]=intbias/netsize;bias[i]=0}}function unbiasnet(){for(var i=0;i<netsize;i++){network[i][0]>>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(j<hi||k>lo){a=radpower[m++];if(j<hi){p=network[j++];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}if(k>lo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i<netsize;i++){n=network[i];dist=Math.abs(n[0]-b)+Math.abs(n[1]-g)+Math.abs(n[2]-r);if(dist<bestd){bestd=dist;bestpos=i}biasdist=dist-(bias[i]>>intbiasshift-netbiasshift);if(biasdist<bestbiasd){bestbiasd=biasdist;bestbiaspos=i}betafreq=freq[i]>>betashift;freq[i]-=betafreq;bias[i]+=betafreq<<gammashift}freq[bestpos]+=beta;bias[bestpos]-=betagamma;return bestbiaspos}function inxbuild(){var i,j,p,q,smallpos,smallval,previouscol=0,startpos=0;for(i=0;i<netsize;i++){p=network[i];smallpos=i;smallval=p[1];for(j=i+1;j<netsize;j++){q=network[j];if(q[1]<smallval){smallpos=j;smallval=q[1]}}q=network[smallpos];if(i!=smallpos){j=q[0];q[0]=p[0];p[0]=j;j=q[1];q[1]=p[1];p[1]=j;j=q[2];q[2]=p[2];p[2]=j;j=q[3];q[3]=p[3];p[3]=j}if(smallval!=previouscol){netindex[previouscol]=startpos+i>>1;for(j=previouscol+1;j<smallval;j++)netindex[j]=i;previouscol=smallval;startpos=i}}netindex[previouscol]=startpos+maxnetpos>>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i<netsize||j>=0){if(i<netsize){p=network[i];dist=p[1]-g;if(dist>=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}if(j>=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}}return best}function learn(){var i;var lengthcount=pixels.length;var alphadec=30+(samplefac-1)/3;var samplepixels=lengthcount/(3*samplefac);var delta=~~(samplepixels/ncycles);var alpha=initalpha;var radius=initradius;var rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i<rad;i++)radpower[i]=alpha*((rad*rad-i*i)*radbias/(rad*rad));var step;if(lengthcount<minpicturebytes){samplefac=1;step=3}else if(lengthcount%prime1!==0){step=3*prime1}else if(lengthcount%prime2!==0){step=3*prime2}else if(lengthcount%prime3!==0){step=3*prime3}else{step=3*prime4}var b,g,r,j;var pix=0;i=0;while(i<samplepixels){b=(pixels[pix]&255)<<netbiasshift;g=(pixels[pix+1]&255)<<netbiasshift;r=(pixels[pix+2]&255)<<netbiasshift;j=contest(b,g,r);altersingle(alpha,j,b,g,r);if(rad!==0)alterneigh(rad,j,b,g,r);pix+=step;if(pix>=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j<rad;j++)radpower[j]=alpha*((rad*rad-j*j)*radbias/(rad*rad))}}}function buildColormap(){init();learn();unbiasnet();inxbuild()}this.buildColormap=buildColormap;function getColormap(){var map=[];var index=[];for(var i=0;i<netsize;i++)index[network[i][3]]=i;var k=0;for(var l=0;l<netsize;l++){var j=index[l];map[k++]=network[j][0];map[k++]=network[j][1];map[k++]=network[j][2]}return map}this.getColormap=getColormap;this.lookupRGB=inxsearch}module.exports=NeuQuant},{}],4:[function(require,module,exports){var GIFEncoder,renderFrame;GIFEncoder=require(\"./GIFEncoder.js\");renderFrame=function(frame){var encoder,page,stream,transfer;encoder=new GIFEncoder(frame.width,frame.height);if(frame.index===0){encoder.writeHeader()}else{encoder.firstFrame=false}encoder.setTransparent(frame.transparent);encoder.setRepeat(frame.repeat);encoder.setDelay(frame.delay);encoder.setQuality(frame.quality);encoder.setDither(frame.dither);encoder.setGlobalPalette(frame.globalPalette);encoder.addFrame(frame.data);if(frame.last){encoder.finish()}if(frame.globalPalette===true){frame.globalPalette=encoder.getGlobalPalette()}stream=encoder.stream();frame.data=stream.pages;frame.cursor=stream.cursor;frame.pageSize=stream.constructor.pageSize;if(frame.canTransfer){transfer=function(){var i,len,ref,results;ref=frame.data;results=[];for(i=0,len=ref.length;i<len;i++){page=ref[i];results.push(page.buffer)}return results}();return self.postMessage(frame,transfer)}else{return self.postMessage(frame)}};self.onmessage=function(event){return renderFrame(event.data)}},{\"./GIFEncoder.js\":1}]},{},[4]);\n//# sourceMappingURL=gif.worker.js.map\n"
  },
  {
    "path": "src/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"__MSG_extName__\",\n  \"description\": \"__MSG_extDesc__\",\n  \"default_locale\": \"en\",\n  \"version\": \"4.3.4\",\n  \"background\": {\n    \"service_worker\": \"background.bundle.js\"\n  },\n  \"action\": {\n    \"default_icon\": \"assets/img/icon-34.png\"\n  },\n  \"icons\": {\n    \"128\": \"assets/img/icon-128.png\"\n  },\n  \"host_permissions\": [\"<all_urls>\"],\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"contentScript.bundle.js\"],\n      \"css\": [\"assets/fonts/fonts.css\"]\n    }\n  ],\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\n        \"content.styles.css\",\n        \"blank.mp4\",\n        \"editor.html\",\n        \"assets/*\",\n        \"setup.html\",\n        \"worker.js\",\n        \"vendor/*\",\n        \"cloudrecorder.html\",\n        \"recorder.html\",\n        \"recorderoffscreen.html\",\n        \"sandbox.html\",\n        \"wrapper.html\",\n        \"camera.html\",\n        \"permissions.html\",\n        \"region.html\",\n        \"waveform.html\",\n        \"playground.html\",\n        \"editorwebcodecs.html\",\n        \"editorviewer.html\",\n        \"download.html\",\n        \"*\"\n      ],\n      \"matches\": [\"<all_urls>\"]\n    }\n  ],\n  \"oauth2\": {\n    \"client_id\": \"560517327251-m7n1k3kddknu7s9s4ejvrs1bj91gutd7.apps.googleusercontent.com\",\n    \"scopes\": [\"https://www.googleapis.com/auth/drive.file\"]\n  },\n  \"cross_origin_embedder_policy\": {\n    \"value\": \"require-corp\"\n  },\n  \"cross_origin_opener_policy\": {\n    \"value\": \"same-origin\"\n  },\n  \"content_security_policy\": {\n    \"sandbox\": \"sandbox allow-scripts allow-modals allow-popups; script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; object-src 'self'; worker-src 'self' blob: ;\",\n    \"extension_pages\": \"script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; media-src 'self' data: blob: *; img-src 'self' https: data: blob:\"\n  },\n  \"sandbox\": {\n    \"pages\": [\"editor.html\"]\n  },\n  \"commands\": {\n    \"start-recording\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+Shift+G\"\n      },\n      \"description\": \"Start recording\"\n    },\n    \"cancel-recording\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+Shift+X\"\n      },\n      \"description\": \"Cancel recording\"\n    },\n    \"pause-recording\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+Shift+M\"\n      },\n      \"description\": \"Pause/Resume recording\"\n    },\n    \"stop-recording\": {\n      \"description\": \"Stop recording\"\n    },\n    \"toggle-drawing-mode\": {\n      \"description\": \"Toggle drawing mode\"\n    },\n    \"toggle-blur-mode\": {\n      \"description\": \"Toggle blur mode\"\n    },\n    \"toggle-hide-ui\": {\n      \"description\": \"Toggle hide UI\"\n    },\n    \"toggle-cursor-mode\": {\n      \"description\": \"Toggle cursor options\"\n    }\n  },\n  \"permissions\": [\n    \"identity\",\n    \"activeTab\",\n    \"storage\",\n    \"unlimitedStorage\",\n    \"downloads\",\n    \"tabs\",\n    \"tabCapture\",\n    \"scripting\",\n    \"system.display\"\n  ],\n  \"storage\": {\n    \"managed_schema\": \"schema.json\"\n  },\n  \"externally_connectable\": {\n    \"matches\": [\"https://app.screenity.io/*\"]\n  },\n  \"optional_permissions\": [\n    \"offscreen\",\n    \"desktopCapture\",\n    \"alarms\",\n    \"clipboardWrite\"\n  ]\n}\n"
  },
  {
    "path": "src/media/fastRecorderGate.ts",
    "content": "declare const chrome: any;\n\nexport type FastRecorderProbeResult = {\n  ok: boolean;\n  reasons: string[];\n  details: Record<string, any>;\n  at?: number;\n};\n\nexport type FastRecorderStickyState = {\n  disabled: boolean;\n  reason?: string | null;\n  details?: any;\n};\n\nexport type FastRecorderValidationResult = {\n  ok: boolean;\n  hardFail: boolean;\n  reasons: string[];\n  details: Record<string, any>;\n};\n\nconst STORAGE_KEYS = {\n  userSetting: \"fastRecorderBeta\",\n  stickyDisabled: \"fastRecorderDisabledForDevice\",\n  stickyReason: \"fastRecorderDisabledReason\",\n  stickyDetails: \"fastRecorderDisabledDetails\",\n  lastFailureAt: \"fastRecorderDisabledAt\",\n  probe: \"fastRecorderProbe\",\n  validation: \"fastRecorderValidation\",\n  inUse: \"fastRecorderInUse\",\n};\n\nconst GATE_VERSION = \"ladder-v1\";\n\nconst getFastRecDebug = () => {\n  try {\n    if (typeof window !== \"undefined\") {\n      const params = new URLSearchParams(window.location.search);\n      if (params.get(\"fastRecDebug\") === \"1\") return true;\n    }\n  } catch {}\n  return (globalThis as any)?.SCREENITY_FAST_REC_DEBUG === true;\n};\n\nconst debugLog = (...args: any[]) => {\n  if (!getFastRecDebug()) return;\n  // eslint-disable-next-line no-console\n  console.log(\"[FastRecorderGate]\", ...args);\n};\n\ndebugLog(\"gate version\", GATE_VERSION, Date.now());\n\nconst safeCanPlayType = (mime: string) => {\n  try {\n    if (typeof document === \"undefined\") return \"\";\n    const video = document.createElement(\"video\");\n    return video?.canPlayType?.(mime) || \"\";\n  } catch {\n    return \"\";\n  }\n};\n\nconst safeMseSupport = (mime: string) => {\n  try {\n    if (typeof MediaSource === \"undefined\") return false;\n    return MediaSource.isTypeSupported(mime);\n  } catch {\n    return false;\n  }\n};\n\nexport const getFastRecorderStickyState = async (): Promise<FastRecorderStickyState> => {\n  try {\n    const result = await chrome.storage.local.get([\n      STORAGE_KEYS.stickyDisabled,\n      STORAGE_KEYS.stickyReason,\n      STORAGE_KEYS.stickyDetails,\n    ]);\n    return {\n      disabled: Boolean(result[STORAGE_KEYS.stickyDisabled]),\n      reason: result[STORAGE_KEYS.stickyReason] || null,\n      details: result[STORAGE_KEYS.stickyDetails] || null,\n    };\n  } catch {\n    return { disabled: false };\n  }\n};\n\nexport const markFastRecorderFailure = async (\n  reasonCode: string,\n  details: Record<string, any> = {}\n) => {\n  try {\n    await chrome.storage.local.set({\n      [STORAGE_KEYS.stickyDisabled]: true,\n      [STORAGE_KEYS.stickyReason]: reasonCode,\n      [STORAGE_KEYS.stickyDetails]: details,\n      [STORAGE_KEYS.lastFailureAt]: Date.now(),\n    });\n  } catch {\n    // ignore\n  }\n};\n\nexport const probeFastRecorderSupport = async (): Promise<FastRecorderProbeResult> => {\n  try {\n    debugLog(\"probe start\", GATE_VERSION, Date.now());\n    const reasons: string[] = [];\n    const details: Record<string, any> = {};\n\n    const hasVideoEncoder = typeof VideoEncoder !== \"undefined\";\n    const hasAudioEncoder = typeof AudioEncoder !== \"undefined\";\n    const hasTrackProcessor = typeof MediaStreamTrackProcessor !== \"undefined\";\n\n    details.hasVideoEncoder = hasVideoEncoder;\n    details.hasAudioEncoder = hasAudioEncoder;\n    details.hasTrackProcessor = hasTrackProcessor;\n\n    if (!hasVideoEncoder) reasons.push(\"no-video-encoder\");\n    if (!hasAudioEncoder) reasons.push(\"no-audio-encoder\");\n    if (!hasTrackProcessor) reasons.push(\"no-track-processor\");\n\n    const baseVideoConfig = {\n      width: 1280,\n      height: 720,\n      bitrate: 4_000_000,\n      framerate: 30,\n      bitrateMode: \"constant\",\n      latencyMode: \"realtime\",\n      hardwareAcceleration: \"no-preference\",\n    } as VideoEncoderConfig;\n\n  const audioConfig = {\n    codec: \"mp4a.40.2\",\n    sampleRate: 48000,\n    numberOfChannels: 2,\n    bitrate: 128000,\n  } as AudioEncoderConfig;\n\n    const attemptSummaries: Array<{\n      codec: string;\n      size: string;\n      knobs: string[];\n      supported: boolean;\n    }> = [];\n\n    const normalizeEven = (value: number) => (value % 2 === 0 ? value : value - 1);\n    const sizes = [\n      { width: 1280, height: 720 },\n      { width: 1920, height: 1080 },\n    ];\n    const codecCandidates = [\"avc1.64002A\", \"avc1.4D401F\", \"avc1.42E01E\"];\n    const hwOptions: Array<VideoEncoderConfig[\"hardwareAcceleration\"] | null> = [\n      \"prefer-hardware\",\n      \"prefer-software\",\n      \"no-preference\",\n      null,\n    ];\n    const ladderSteps = [\n      { label: \"full\", omit: [] as string[] },\n      { label: \"no-framerate\", omit: [\"framerate\"] },\n      { label: \"no-bitrateMode\", omit: [\"bitrateMode\"] },\n      { label: \"no-latencyMode\", omit: [\"latencyMode\"] },\n      { label: \"no-alpha\", omit: [\"alpha\"] },\n      { label: \"no-bitrate\", omit: [\"bitrate\"] },\n    ];\n\n    let selectedVideoConfig: VideoEncoderConfig | null = null;\n\n    if (hasVideoEncoder && typeof VideoEncoder.isConfigSupported === \"function\") {\n      for (const size of sizes) {\n        const width = normalizeEven(size.width);\n        const height = normalizeEven(size.height);\n        for (const codec of codecCandidates) {\n          for (const hw of hwOptions) {\n            for (const step of ladderSteps) {\n              const config: any = {\n                ...baseVideoConfig,\n                codec,\n                width,\n                height,\n              };\n              if (hw) {\n                config.hardwareAcceleration = hw;\n              } else {\n                delete config.hardwareAcceleration;\n              }\n              for (const key of step.omit) {\n                delete config[key];\n              }\n\n              let supported = false;\n              try {\n                const support = await VideoEncoder.isConfigSupported(config);\n                supported = Boolean(support?.supported);\n                if (supported) {\n                  selectedVideoConfig = support?.config || config;\n                }\n              } catch {\n                supported = false;\n              }\n\n              attemptSummaries.push({\n                codec,\n                size: `${width}x${height}`,\n                knobs: [\n                  step.label,\n                  hw ? `hw:${hw}` : \"hw:omit\",\n                  config.bitrate ? \"bitrate:on\" : \"bitrate:omit\",\n                  config.framerate ? \"framerate:on\" : \"framerate:omit\",\n                  config.bitrateMode ? \"bitrateMode:on\" : \"bitrateMode:omit\",\n                  config.latencyMode ? \"latencyMode:on\" : \"latencyMode:omit\",\n                ],\n                supported,\n              });\n\n              if (supported && selectedVideoConfig) {\n                details.selectedVideoConfig = selectedVideoConfig;\n                details.videoConfigSupported = true;\n                details.attemptedConfigCount = attemptSummaries.length;\n                details.attemptSummary = attemptSummaries;\n                break;\n              }\n            }\n            if (selectedVideoConfig) break;\n          }\n          if (selectedVideoConfig) break;\n        }\n        if (selectedVideoConfig) break;\n      }\n    }\n\n    if (!selectedVideoConfig) {\n      details.videoConfigSupported = false;\n      details.attemptedConfigCount = attemptSummaries.length;\n      details.attemptSummary = attemptSummaries;\n      reasons.push(\"video-config-unsupported\");\n    }\n\n  if (hasAudioEncoder && typeof AudioEncoder.isConfigSupported === \"function\") {\n    try {\n      const support = await AudioEncoder.isConfigSupported(audioConfig);\n      details.audioConfigSupported = Boolean(support?.supported);\n      details.audioConfig = support?.config || audioConfig;\n      if (!support?.supported) reasons.push(\"audio-config-unsupported\");\n    } catch (err) {\n      details.audioConfigError = String(err);\n      reasons.push(\"audio-config-error\");\n    }\n  }\n\n    const videoCodecCandidates = [\"avc1.64002A\", \"avc1.4D401F\", \"avc1.42E01E\"];\n  const audioCodec = \"mp4a.40.2\";\n  details.videoCodecCandidates = videoCodecCandidates;\n  details.audioCodec = audioCodec;\n\n  const playableCodecs: string[] = [];\n  for (const codec of videoCodecCandidates) {\n    const mp4Mime = `video/mp4; codecs=\"${codec}, ${audioCodec}\"`;\n    const canPlay = safeCanPlayType(mp4Mime);\n    if (canPlay) playableCodecs.push(codec);\n  }\n\n  details.playableVideoCodecs = playableCodecs;\n  details.canPlayType = playableCodecs.length > 0 ? \"maybe\" : \"\";\n\n  const mseSupported = safeMseSupport(\n    `video/mp4; codecs=\"${videoCodecCandidates[0]}, ${audioCodec}\"`\n  );\n  details.mediaSourceSupported = mseSupported;\n\n  if (playableCodecs.length === 0) {\n    reasons.push(\"mp4-playback-unsupported\");\n  }\n\n  const ua = typeof navigator !== \"undefined\" ? navigator.userAgent : \"\";\n  const isLinux = /Linux/i.test(ua);\n  details.userAgent = ua;\n  details.isLinux = isLinux;\n\n    if (isLinux && playableCodecs.length === 0) {\n      reasons.push(\"linux-missing-codecs\");\n    }\n\n    const ok = reasons.length === 0;\n    const at = Date.now();\n    debugLog(\"probe result\", { ok, reasons, details, at });\n\n    try {\n      await chrome.storage.local.set({\n        [STORAGE_KEYS.probe]: { ok, reasons, details, at },\n      });\n    } catch {\n      // ignore\n    }\n\n    return { ok, reasons, details, at };\n  } catch (err: any) {\n    const details = {\n      message: String(err?.message || err),\n      stack: err?.stack ? String(err.stack) : undefined,\n    };\n    const at = Date.now();\n    const result = {\n      ok: false,\n      reasons: [\"probe_exception\"],\n      details,\n      at,\n    };\n    try {\n      await chrome.storage.local.set({\n        [STORAGE_KEYS.probe]: result,\n      });\n    } catch {\n      // ignore\n    }\n    return result;\n  }\n};\n\nexport const shouldUseFastRecorder = (\n  userSetting: boolean | null | undefined,\n  probeResult: FastRecorderProbeResult,\n  stickyDisableState: FastRecorderStickyState\n) => {\n  if (userSetting === false) return false;\n  if (stickyDisableState?.disabled && userSetting !== true) return false;\n  return probeResult?.ok === true && Boolean(probeResult?.details?.selectedVideoConfig);\n};\n\nexport const validateFastRecorderOutputBlob = async (\n  blob: Blob | null,\n  opts: {\n    minBytes?: number;\n    timeoutMs?: number;\n    videoCodec?: string;\n    audioCodec?: string | null;\n    recordingId?: string | null;\n  } = {}\n): Promise<FastRecorderValidationResult> => {\n  const reasons: string[] = [];\n  const details: Record<string, any> = {};\n\n  if (!blob) {\n    reasons.push(\"no-blob\");\n    return { ok: false, hardFail: true, reasons, details };\n  }\n\n  const minBytes = opts.minBytes ?? 64 * 1024;\n  details.size = blob.size;\n  details.type = blob.type;\n\n  if (blob.size < minBytes) {\n    reasons.push(\"blob-too-small\");\n  }\n\n  if (!blob.type || !blob.type.includes(\"mp4\")) {\n    reasons.push(\"unexpected-mime\");\n  }\n\n  let duration = 0;\n  let width = 0;\n  let height = 0;\n  let seekable: { start: number; end: number; length: number } | null = null;\n  let frameChecked = false;\n  let framePresented = false;\n  let blackFrameSuspected = false;\n\n  if (opts.recordingId) {\n    details.recordingId = opts.recordingId;\n  }\n\n  if (typeof document !== \"undefined\") {\n    const timeoutMs = opts.timeoutMs ?? 4000;\n    const url = URL.createObjectURL(blob);\n    const video = document.createElement(\"video\");\n    video.preload = \"metadata\";\n    video.muted = true;\n    video.playsInline = true;\n\n    try {\n      const meta = await new Promise<{ duration: number; width: number; height: number }>(\n        (resolve, reject) => {\n          const timer = setTimeout(() => {\n            reject(new Error(\"metadata-timeout\"));\n          }, timeoutMs);\n\n          video.onloadedmetadata = () => {\n            clearTimeout(timer);\n            resolve({\n              duration: Number.isFinite(video.duration) ? video.duration : video.duration || 0,\n              width: video.videoWidth || 0,\n              height: video.videoHeight || 0,\n            });\n          };\n\n          video.onerror = () => {\n            clearTimeout(timer);\n            reject(new Error(\"metadata-error\"));\n          };\n\n          video.src = url;\n        }\n      );\n\n      duration = meta.duration;\n      width = meta.width;\n      height = meta.height;\n      details.duration = duration;\n      details.width = width;\n      details.height = height;\n      seekable = {\n        length: video.seekable?.length || 0,\n        start: video.seekable?.length ? video.seekable.start(0) : 0,\n        end: video.seekable?.length ? video.seekable.end(0) : 0,\n      };\n      details.seekable = seekable;\n\n      const durationFinite = Number.isFinite(duration) && duration > 0;\n      const seekableOk = seekable && seekable.length > 0 && seekable.end > 0;\n\n      if (!durationFinite && !seekableOk) {\n        reasons.push(\"duration-unavailable\");\n      }\n\n      // First frame validation (best-effort)\n      let seekTarget = 0.1;\n      if (durationFinite) {\n        seekTarget = Math.min(0.1, Math.max(0.01, duration / 10));\n      } else if (seekableOk) {\n        seekTarget = Math.min(seekable!.end - seekable!.start, 0.1) + seekable!.start;\n      }\n\n      const seekResult = await new Promise<void>((resolve, reject) => {\n        const timer = setTimeout(() => reject(new Error(\"seek-timeout\")), timeoutMs);\n        const onSeeked = () => {\n          clearTimeout(timer);\n          resolve();\n        };\n        const onError = () => {\n          clearTimeout(timer);\n          reject(new Error(\"seek-error\"));\n        };\n        video.onseeked = onSeeked;\n        video.onerror = onError;\n        try {\n          video.currentTime = seekTarget;\n        } catch (err) {\n          clearTimeout(timer);\n          reject(err);\n        }\n      });\n\n      void seekResult;\n\n      frameChecked = true;\n      if (typeof video.requestVideoFrameCallback === \"function\") {\n        await new Promise<void>((resolve) => {\n          const timer = setTimeout(() => resolve(), timeoutMs);\n          video.requestVideoFrameCallback(() => {\n            clearTimeout(timer);\n            resolve();\n          });\n        });\n        framePresented = true;\n      } else {\n        await new Promise((resolve) => setTimeout(resolve, 50));\n        framePresented = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA;\n      }\n\n      if (framePresented && width > 0 && height > 0) {\n        try {\n          const canvas = document.createElement(\"canvas\");\n          canvas.width = Math.min(64, width);\n          canvas.height = Math.min(64, height);\n          const ctx = canvas.getContext(\"2d\");\n          if (ctx) {\n            ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n            const sample = ctx.getImageData(0, 0, canvas.width, canvas.height).data;\n            let bright = 0;\n            for (let i = 0; i < sample.length; i += 16) {\n              bright += sample[i] + sample[i + 1] + sample[i + 2];\n            }\n            const avg = bright / (sample.length / 16);\n            blackFrameSuspected = avg < 5;\n          }\n        } catch {\n          // ignore pixel sampling errors\n        }\n      }\n\n      details.frameChecked = frameChecked;\n      details.framePresented = framePresented;\n      details.blackFrameSuspected = blackFrameSuspected;\n\n      if (frameChecked && !framePresented) {\n        reasons.push(\"frame-not-presented\");\n      } else if (blackFrameSuspected) {\n        reasons.push(\"frame-black-suspected\");\n      }\n    } catch (err) {\n      details.metadataError = String(err);\n      reasons.push(\"metadata-unavailable\");\n    } finally {\n      URL.revokeObjectURL(url);\n    }\n  }\n\n  if (Number.isFinite(duration) && duration <= 0) {\n    reasons.push(\"duration-zero\");\n  }\n\n  const videoCodec = opts.videoCodec || \"avc1.42E01E\";\n  const audioCodec =\n    opts.audioCodec === undefined ? \"mp4a.40.2\" : opts.audioCodec;\n  const codecSuffix = audioCodec ? `, ${audioCodec}` : \"\";\n  const mp4Mime = `video/mp4; codecs=\"${videoCodec}${codecSuffix}\"`;\n  const canPlay = safeCanPlayType(mp4Mime);\n  const mseSupported = safeMseSupport(mp4Mime);\n  details.canPlayType = canPlay;\n  details.mediaSourceSupported = mseSupported;\n  details.expectedVideoCodec = videoCodec;\n  details.expectedAudioCodec = audioCodec;\n\n  const metadataFailed =\n    reasons.includes(\"metadata-unavailable\") ||\n    reasons.includes(\"duration-unavailable\");\n\n  const hardFail =\n    reasons.includes(\"no-blob\") ||\n    reasons.includes(\"blob-too-small\") ||\n    reasons.includes(\"unexpected-mime\") ||\n    // When fastStart is false, metadata can be flaky in some contexts.\n    // Only hard-fail metadata issues if the browser also can't play the codec.\n    (metadataFailed && !canPlay);\n\n  const ok = reasons.length === 0 || (!hardFail && Boolean(canPlay));\n\n  debugLog(\"validation result\", { ok, hardFail, reasons, details });\n\n  try {\n    await chrome.storage.local.set({\n      [STORAGE_KEYS.validation]: {\n        ok,\n        hardFail,\n        reasons,\n        details,\n        ts: Date.now(),\n      },\n    });\n  } catch {\n    // ignore\n  }\n\n  return { ok, hardFail, reasons, details };\n};\n\nexport const fastRecorderStorageKeys = STORAGE_KEYS;\n"
  },
  {
    "path": "src/messaging/messageRouter.js",
    "content": "const handlers = {};\n\nexport const registerMessage = (type, handler) => {\n  if (handlers[type]) {\n    console.warn(\n      `⚠️ Handler for ${type} already exists in this context. Skipping.`\n    );\n    return;\n  }\n  handlers[type] = handler;\n};\n\nexport const messageDispatcher = (message, sender, sendResponse) => {\n  const handler = handlers[message.type];\n\n  if (handler) {\n    try {\n      const result = handler(message, sender, sendResponse);\n\n      if (result instanceof Promise) {\n        result\n          .then((response) => {\n            sendResponse(response);\n          })\n          .catch((err) => {\n            sendResponse({ error: err.message });\n          });\n\n        return true;\n      } else {\n        sendResponse(result);\n      }\n    } catch (err) {\n      sendResponse({ error: err.message });\n    }\n  }\n};\n\nexport const messageRouter = () => {\n  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n    const result = messageDispatcher(message, sender, sendResponse);\n\n    if (result === true || result instanceof Promise) {\n      return true;\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/AudioOffscreen/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Screenity Audio Offscreen</title>\n  </head>\n  <body>\n    <noscript>This page requires JavaScript.</noscript>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/AudioOffscreen/index.js",
    "content": "const audioUrl = chrome.runtime.getURL(\"assets/sounds/beep.mp3\");\nlet audioEl = null;\n\nconst getAudio = () => {\n  if (!audioEl) {\n    audioEl = new Audio(audioUrl);\n    audioEl.volume = 0.5;\n  }\n  return audioEl;\n};\n\nconst playBeep = () => {\n  const audio = getAudio();\n  try {\n    audio.currentTime = 0;\n  } catch {}\n  const playPromise = audio.play();\n  if (playPromise?.catch) {\n    playPromise.catch(() => {});\n  }\n};\n\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message?.type === \"play-beep-offscreen\") {\n    playBeep();\n    if (sendResponse) sendResponse({ ok: true });\n    return true;\n  }\n  return false;\n});\n"
  },
  {
    "path": "src/pages/Background/alarms/addAlarmListener.js",
    "content": "import { handleAlarm } from \"./handleAlarm\";\n\nconst alarmListener = (alarm) => {\n  handleAlarm(alarm);\n};\n\nexport const addAlarmListener = () => {\n  if (!chrome.alarms.onAlarm.hasListener(alarmListener)) {\n    chrome.alarms.onAlarm.addListener(alarmListener);\n  }\n};\n\n// Check if the permission is granted\nif (chrome.permissions) {\n  chrome.permissions.contains({ permissions: [\"alarms\"] }, (result) => {\n    if (result) {\n      addAlarmListener();\n    }\n  });\n}\n"
  },
  {
    "path": "src/pages/Background/alarms/handleAlarm.js",
    "content": "import { stopRecording } from \"../recording/stopRecording.js\";\nimport { sendMessageTab } from \"../tabManagement\";\nimport { sendMessageRecord } from \"../recording/sendMessageRecord.js\";\nimport { diagEvent } from \"../../utils/diagnosticLog\";\nimport { chunksStore } from \"../recording/chunkHandler\";\nimport {\n  CLOUD_LOCAL_PLAYBACK_KEY,\n  CLOUD_LOCAL_PLAYBACK_EVENT_KEY,\n  CLOUD_LOCAL_PLAYBACK_ALARM,\n} from \"../recording/cloudLocalPlaybackConstants\";\n\n// Utility to handle tab messaging logic.\n// `tab` is an optional Chrome Tab object used as a fallback when the stored\n// activeTab is no longer valid (e.g. when called from an action-button click).\n// When called from an alarm — where there is no current tab — pass null;\n// the function will skip the fallback branch gracefully.\nconst handleTabMessaging = async (tab) => {\n  const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n\n  try {\n    const targetTab = await chrome.tabs.get(activeTab);\n\n    if (targetTab) {\n      sendMessageTab(activeTab, { type: \"stop-recording-tab\" });\n    } else if (tab?.id) {\n      // Only reachable when a real tab object was provided (not from alarms).\n      sendMessageTab(tab.id, { type: \"stop-recording-tab\" });\n      chrome.storage.local.set({ activeTab: tab.id });\n    }\n  } catch (error) {\n    console.error(\"Error in handleTabMessaging:\", error);\n  }\n};\n\nexport const handleAlarm = async (alarm) => {\n  if (alarm.name === \"recording-alarm\") {\n    const { recording } = await chrome.storage.local.get([\"recording\"]);\n\n    if (recording) {\n      diagEvent(\"alarm-fired\");\n      stopRecording();\n      sendMessageRecord({ type: \"stop-recording-tab\" });\n      // Pass null: alarms fire without a user-initiated tab context.\n      // handleTabMessaging will use the stored activeTab and skip the\n      // tab-object fallback branch when null is provided.\n      await handleTabMessaging(null);\n    }\n\n    await chrome.alarms.clear(\"recording-alarm\");\n    return;\n  }\n\n  if (alarm.name === CLOUD_LOCAL_PLAYBACK_ALARM) {\n    const {\n      [CLOUD_LOCAL_PLAYBACK_KEY]: localPlaybackOffer,\n      recording,\n      pendingRecording,\n      restarting,\n    } = await chrome.storage.local.get([\n      CLOUD_LOCAL_PLAYBACK_KEY,\n      \"recording\",\n      \"pendingRecording\",\n      \"restarting\",\n    ]);\n\n    if (!localPlaybackOffer?.offerId) {\n      await chrome.alarms.clear(CLOUD_LOCAL_PLAYBACK_ALARM);\n      return;\n    }\n\n    const hasActiveRecording = Boolean(recording || pendingRecording || restarting);\n    if (hasActiveRecording) {\n      // Never clear local chunks while a new recording is active; retry soon.\n      const retryAt = Date.now() + 5 * 60 * 1000;\n      await chrome.alarms\n        .create(CLOUD_LOCAL_PLAYBACK_ALARM, { when: retryAt })\n        .catch(() => {});\n      return;\n    }\n\n    const expired = Number(localPlaybackOffer.expiresAt || 0) <= Date.now();\n    if (!expired) {\n      await chrome.alarms\n        .create(CLOUD_LOCAL_PLAYBACK_ALARM, {\n          when: Number(localPlaybackOffer.expiresAt),\n        })\n        .catch(() => {});\n      return;\n    }\n\n    await chunksStore.clear().catch((err) => {\n      console.warn(\n        \"[Screenity][BG] Failed to clear chunksStore for local playback expiry\",\n        err,\n      );\n    });\n    await chrome.storage.local.remove([CLOUD_LOCAL_PLAYBACK_KEY]);\n    await chrome.storage.local.set({\n      [CLOUD_LOCAL_PLAYBACK_EVENT_KEY]: {\n        event: \"offer-expired\",\n        at: Date.now(),\n        offerId: localPlaybackOffer.offerId,\n        projectId: localPlaybackOffer.projectId || null,\n        sceneId: localPlaybackOffer.sceneId || null,\n      },\n    });\n    await chrome.alarms.clear(CLOUD_LOCAL_PLAYBACK_ALARM);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/auth/loginWithWebsite.js",
    "content": "import { getCurrentTab, sendMessageTab } from \"../tabManagement\";\n\nconst API_BASE = process.env.SCREENITY_API_BASE_URL;\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nexport const loginWithWebsite = async () => {\n  if (!CLOUD_FEATURES_ENABLED) {\n    return { authenticated: false, instantMode: false };\n  }\n\n  // User explicitly chose to stay logged out — respect that until they\n  // initiate a login themselves (handle-login clears this flag).\n  const { stayLoggedOut } = await chrome.storage.local.get([\"stayLoggedOut\"]);\n  if (stayLoggedOut) {\n    return { authenticated: false, instantMode: false };\n  }\n\n  const { screenityToken, lastAuthCheck, instantMode } =\n    await chrome.storage.local.get([\n      \"screenityToken\",\n      \"lastAuthCheck\",\n      \"instantMode\",\n    ]);\n\n  if (!screenityToken) {\n    await chrome.storage.local.set({ isLoggedIn: false });\n    return { authenticated: false, instantMode: false };\n  }\n\n  const now = Date.now();\n  const FRESH_FOR = 1000 * 60 * 60 * 4; // 4 hours\n  let isTokenInvalid = false;\n\n  if (lastAuthCheck && now - lastAuthCheck < FRESH_FOR) {\n    const {\n      screenityUser,\n      isSubscribed,\n      proSubscription,\n      hasSubscribedBefore,\n    } = await chrome.storage.local.get([\n      \"screenityUser\",\n      \"isSubscribed\",\n      \"proSubscription\",\n      \"hasSubscribedBefore\",\n    ]);\n\n    if (screenityUser) {\n      // Single storage write to avoid multiple onChanged events\n      await chrome.storage.local.set({\n        onboarding: false,\n        isLoggedIn: true,\n        wasLoggedIn: false,\n        ...(!instantMode\n          ? {\n              backgroundEffectsActive: false,\n              backgroundEffect: \"\",\n              fpsValue: \"30\",\n            }\n          : {}),\n      });\n\n      return {\n        authenticated: true,\n        user: screenityUser,\n        subscribed: isSubscribed || false,\n        proSubscription: proSubscription || null,\n        hasSubscribedBefore: !!hasSubscribedBefore,\n        cached: true,\n        instantMode: instantMode || false,\n      };\n    }\n\n    // Cache is marked fresh but user payload is missing; force a verify call.\n    await chrome.storage.local.set({ lastAuthCheck: 0 });\n  }\n\n  try {\n    const response = await fetch(`${API_BASE}/auth/verify`, {\n      headers: {\n        Authorization: `Bearer ${screenityToken}`,\n      },\n    });\n\n    if (!response.ok) {\n      isTokenInvalid = response.status === 401 || response.status === 403;\n      throw new Error(`Token verify failed (${response.status})`);\n    }\n\n    const responseData = await response.json();\n    const { subscribed, subscription, hasSubscribedBefore, ...user } =\n      responseData;\n\n    await chrome.storage.local.set({\n      screenityUser: user,\n      lastAuthCheck: now,\n      isLoggedIn: true,\n      wasLoggedIn: false,\n      isSubscribed: subscribed,\n      proSubscription: subscription,\n      hasSubscribedBefore: !!hasSubscribedBefore,\n      pushToTalk: false,\n      onboarding: false,\n      showProSplash: false,\n      ...(!instantMode\n        ? {\n            backgroundEffectsActive: false,\n            backgroundEffect: \"\",\n            fpsValue: \"30\",\n          }\n        : {}),\n    });\n\n    const { originalTabId } = await chrome.storage.local.get(\"originalTabId\");\n\n    if (originalTabId) {\n      // Clear immediately so subsequent cached calls don't re-fire.\n      await chrome.storage.local.remove(\"originalTabId\");\n\n      chrome.tabs.update(originalTabId, { active: true });\n\n      sendMessageTab(originalTabId, { type: \"LOGIN_SUCCESS\" });\n\n      sendMessageTab(originalTabId, {\n        type: \"check-auth\",\n      });\n    }\n\n    return {\n      authenticated: true,\n      user,\n      subscribed: !!subscribed,\n      proSubscription: subscription,\n      hasSubscribedBefore: !!hasSubscribedBefore,\n      cached: false,\n      instantMode: instantMode || false,\n    };\n  } catch (err) {\n    if (isTokenInvalid) {\n      try {\n        const refreshRes = await fetch(`${API_BASE}/auth/refresh`, {\n          credentials: \"include\",\n        });\n        if (refreshRes.ok) {\n          const { token: newToken } = await refreshRes.json();\n          await chrome.storage.local.set({ screenityToken: newToken });\n\n          return await loginWithWebsite();\n        }\n      } catch (refreshErr) {\n        console.error(\"❌ Refresh failed:\", refreshErr.message);\n      }\n\n      // Token is invalid and refresh failed: perform full logout.\n      await chrome.storage.local.remove([\n        \"screenityToken\",\n        \"screenityUser\",\n        \"lastAuthCheck\",\n        \"isSubscribed\",\n        \"proSubscription\",\n      ]);\n      await chrome.storage.local.set({ isLoggedIn: false });\n      return { authenticated: false, instantMode: false };\n    }\n\n    // Network/transient error: keep existing auth state instead of forcing logout.\n    const {\n      screenityUser,\n      isSubscribed,\n      proSubscription,\n      hasSubscribedBefore,\n    } = await chrome.storage.local.get([\n      \"screenityUser\",\n      \"isSubscribed\",\n      \"proSubscription\",\n      \"hasSubscribedBefore\",\n    ]);\n\n    if (screenityUser) {\n      await chrome.storage.local.set({\n        isLoggedIn: true,\n        ...(!instantMode\n          ? {\n              backgroundEffectsActive: false,\n              backgroundEffect: \"\",\n              fpsValue: \"30\",\n            }\n          : {}),\n      });\n\n      return {\n        authenticated: true,\n        user: screenityUser,\n        subscribed: !!isSubscribed,\n        proSubscription: proSubscription || null,\n        hasSubscribedBefore: !!hasSubscribedBefore,\n        cached: true,\n        instantMode: instantMode || false,\n      };\n    }\n\n    await chrome.storage.local.set({ isLoggedIn: false });\n    return {\n      authenticated: false,\n      instantMode: instantMode || false,\n      transient: true,\n      error: err?.message || \"auth-check-failed\",\n    };\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/backup/initBackup.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\n\nexport const initBackup = async (request, id) => {\n  const { backupTab } = await chrome.storage.local.get([\"backupTab\"]);\n  const backupURL = chrome.runtime.getURL(\"backup.html\");\n\n  const createBackupTab = () => {\n    chrome.tabs.create(\n      {\n        url: backupURL,\n        active: true,\n        pinned: true,\n        index: 0,\n      },\n      (tab) => {\n        chrome.storage.local.set({ backupTab: tab.id });\n        chrome.tabs.onUpdated.addListener(function listener(tabId, changeInfo) {\n          if (tabId === tab.id && changeInfo.status === \"complete\") {\n            sendMessageTab(tab.id, {\n              type: \"init-backup\",\n              request: request,\n              tabId: id,\n            });\n            chrome.tabs.onUpdated.removeListener(listener);\n          }\n        });\n      }\n    );\n  };\n\n  if (backupTab) {\n    chrome.tabs.get(backupTab, (tab) => {\n      if (chrome.runtime.lastError || !tab) {\n        createBackupTab();\n      } else {\n        sendMessageTab(tab.id, {\n          type: \"init-backup\",\n          request: request,\n          tabId: id,\n        });\n      }\n    });\n  } else {\n    createBackupTab();\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/drive/handleSaveToDrive.js",
    "content": "import { base64ToUint8Array } from \"../utils/base64ToUint8Array\";\nimport { sendMessageTab } from \"../tabManagement\";\nimport { chunksStore } from \"../recording/chunkHandler\";\nimport signIn from \"../modules/signIn\";\nimport { diagEvent } from \"../../utils/diagnosticLog\";\n\nconst findOrCreateScreenityFolder = async (token) => {\n  const headers = new Headers({\n    Authorization: `Bearer ${token}`,\n    \"Content-Type\": \"application/json\",\n  });\n\n  const query = encodeURIComponent(\n    `name='Screenity' and mimeType='application/vnd.google-apps.folder' and trashed=false`\n  );\n\n  const searchRes = await fetch(\n    `https://www.googleapis.com/drive/v3/files?q=${query}&fields=files(id,name)`,\n    { headers }\n  );\n\n  if (!searchRes.ok) {\n    const text = await searchRes.text().catch(() => \"\");\n    throw new Error(\n      `Drive folder search failed: ${searchRes.status} ${text}`\n    );\n  }\n\n  const result = await searchRes.json();\n  if (result.files?.length) return result.files[0].id;\n\n  const createRes = await fetch(`https://www.googleapis.com/drive/v3/files`, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify({\n      name: \"Screenity\",\n      mimeType: \"application/vnd.google-apps.folder\",\n    }),\n  });\n\n  if (!createRes.ok) {\n    const text = await createRes.text().catch(() => \"\");\n    throw new Error(\n      `Drive folder creation failed: ${createRes.status} ${text}`\n    );\n  }\n\n  const newFolder = await createRes.json();\n  return newFolder.id;\n};\n\nconst getAuthTokenFromStorage = async () => {\n  return new Promise((resolve, reject) => {\n    chrome.storage.local.get([\"token\"], async (result) => {\n      if (chrome.runtime.lastError) {\n        console.error(\"[Drive] storage error:\", chrome.runtime.lastError);\n        reject(new Error(chrome.runtime.lastError));\n        return;\n      }\n\n      const token = result.token;\n      if (!token) {\n        try {\n          const newToken = await signIn();\n          if (!newToken) {\n            console.error(\"[Drive] drive_auth_failed: signIn returned null\");\n            diagEvent(\"drive-auth-fail\", { reason: \"signIn returned null\" });\n            reject(new Error(\"Sign-in failed\"));\n          } else {\n            resolve(newToken);\n          }\n        } catch (err) {\n          console.error(\"[Drive] drive_auth_failed:\", err.message);\n          diagEvent(\"drive-auth-fail\", { reason: String(err.message).slice(0, 80) });\n          reject(err);\n        }\n        return;\n      }\n\n      // Check JWT expiry if possible; opaque tokens skip this check.\n      let isExpired = false;\n      try {\n        const parts = token.split(\".\");\n        if (parts.length === 3) {\n          const payload = JSON.parse(atob(parts[1]));\n          const exp = payload.exp * 1000;\n          isExpired = Date.now() >= exp;\n        }\n        // Not a JWT — skip expiry check\n      } catch {\n        // Not parseable — use as-is\n      }\n\n      if (isExpired) {\n        try {\n          const newToken = await signIn();\n          if (!newToken) {\n            console.error(\"[Drive] drive_auth_failed: re-sign-in returned null\");\n            reject(new Error(\"Sign-in failed\"));\n          } else {\n            resolve(newToken);\n          }\n        } catch (err) {\n          console.error(\"[Drive] drive_auth_failed:\", err.message);\n          reject(err);\n        }\n      } else {\n        resolve(token);\n      }\n    });\n  });\n};\n\nconst saveToDrive = async (videoBlob, fileName) => {\n  try {\n    const token = await getAuthTokenFromStorage();\n    if (!token) throw new Error(\"Sign-in failed\");\n\n    diagEvent(\"drive-upload-start\", {\n      blobSize: videoBlob.size,\n      blobType: videoBlob.type,\n    });\n\n    const folderId = await findOrCreateScreenityFolder(token);\n\n    const metadata = {\n      name: fileName,\n      parents: [folderId],\n    };\n\n    const boundary = \"-------314159265358979323846\";\n    const delimiter = `\\r\\n--${boundary}\\r\\n`;\n    const close_delim = `\\r\\n--${boundary}--`;\n\n    const reader = new FileReader();\n\n    const base64Data = await new Promise((resolve, reject) => {\n      reader.onload = (e) => resolve(e.target.result.split(\",\")[1]);\n      reader.onerror = () =>\n        reject(new Error(\"FileReader failed to read blob\"));\n      reader.readAsDataURL(videoBlob);\n    });\n\n    const multipartBody =\n      delimiter +\n      `Content-Type: application/json; charset=UTF-8\\r\\n\\r\\n` +\n      JSON.stringify(metadata) +\n      delimiter +\n      `Content-Type: ${videoBlob.type}\\r\\n` +\n      `Content-Transfer-Encoding: base64\\r\\n\\r\\n` +\n      base64Data +\n      close_delim;\n\n    const uploadResponse = await fetch(\n      \"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart\",\n      {\n        method: \"POST\",\n        headers: {\n          Authorization: `Bearer ${token}`,\n          \"Content-Type\": `multipart/related; boundary=${boundary}`,\n        },\n        body: multipartBody,\n      }\n    );\n\n    if (!uploadResponse.ok) {\n      const errBody = await uploadResponse.text().catch(() => \"\");\n      console.error(\"[Drive] drive_upload_failed\", {\n        status: uploadResponse.status,\n        body: errBody,\n      });\n      throw new Error(`Upload failed: ${uploadResponse.status} ${errBody}`);\n    }\n\n    const { id: fileId } = await uploadResponse.json();\n    if (!fileId) throw new Error(\"File ID missing after upload\");\n\n    diagEvent(\"drive-upload-ok\", { fileId });\n\n    chrome.tabs.create({\n      url: `https://drive.google.com/file/d/${fileId}/view`,\n    });\n\n    return { status: \"ok\", url: fileId };\n  } catch (error) {\n    console.error(\"[Drive] drive_upload_failed:\", error.message);\n    diagEvent(\"drive-upload-fail\", { error: String(error.message).slice(0, 120) });\n    return { status: \"ew\", url: null, error: error.message };\n  }\n};\n\nconst savedToDrive = async () => {\n  const { sandboxTab } = await chrome.storage.local.get([\"sandboxTab\"]);\n  if (!sandboxTab) {\n    console.warn(\"[Drive] savedToDrive: sandboxTab not set, cannot notify UI\");\n    return;\n  }\n  try {\n    await sendMessageTab(sandboxTab, { type: \"saved-to-drive\" });\n  } catch (err) {\n    console.warn(\"[Drive] savedToDrive: failed to notify sandbox tab:\", err);\n  }\n};\n\nexport const handleSaveToDrive = async (request, fallback = false) => {\n  try {\n    let response;\n\n    if (!fallback) {\n      const blob = base64ToUint8Array(request.base64);\n      const ext = request.isWebm ? \".webm\" : \".mp4\";\n      const fileName = (request.title || \"Screenity Recording\") + ext;\n      response = await saveToDrive(blob, fileName);\n    } else {\n      // Build blob from IndexedDB chunks (viewer/recovery mode)\n      const chunks = [];\n      await chunksStore.iterate((value) => chunks.push(value));\n\n      if (chunks.length === 0) {\n        console.error(\"[Drive] drive_upload_failed: no chunks in IndexedDB\");\n        return { status: \"ew\", url: null, error: \"No recording data found\" };\n      }\n\n      // Sort to ensure correct order\n      chunks.sort((a, b) => {\n        const ta = a.timestamp ?? a.index ?? 0;\n        const tb = b.timestamp ?? b.index ?? 0;\n        return ta - tb;\n      });\n\n      const parts = chunks.map((c) =>\n        c.chunk instanceof Blob ? c.chunk : new Blob([c.chunk])\n      );\n      const blob = new Blob(parts, { type: \"video/webm\" });\n      const fileName = (request.title || \"Screenity Recording\") + \".webm\";\n      response = await saveToDrive(blob, fileName);\n    }\n\n    if (response.status === \"ok\") {\n      await savedToDrive();\n    }\n    return response;\n  } catch (err) {\n    console.error(\"[Drive] handleSaveToDrive failed:\", err);\n    diagEvent(\"drive-save-fail\", { error: String(err.message).slice(0, 120) });\n    return { status: \"ew\", url: null, error: err.message };\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/drive/handleSignOutDrive.js",
    "content": "export const handleSignOutDrive = async () => {\n  const { token } = await chrome.storage.local.get([\"token\"]);\n  var url = \"https://accounts.google.com/o/oauth2/revoke?token=\" + token;\n  fetch(url);\n\n  chrome.identity.removeCachedAuthToken({ token: token });\n  chrome.storage.local.set({ token: false });\n};\n"
  },
  {
    "path": "src/pages/Background/index.js",
    "content": "import { initializeListeners } from \"./listeners\";\nimport { setupHandlers } from \"./messaging/handlers\";\nimport {\n  messageRouter,\n  messageDispatcher,\n} from \"../../messaging/messageRouter\";\nimport { hydrateDiagnosticLog, diagEvent } from \"../utils/diagnosticLog\";\n\n// Clear any storage flags that act as in-progress locks and could have been\n// left in a \"true\" state if the service worker was killed mid-operation.\n// Must run before any message handler or alarm handler has a chance to bail\n// out on a stale lock.  Storage reads/writes are fast enough that they\n// complete well before any queued event is dispatched to the worker.\nconst clearStaleLocks = async () => {\n  try {\n    const { sendingChunks, postStopEditorOpening, postStopEditorOpened } =\n      await chrome.storage.local.get([\n        \"sendingChunks\",\n        \"postStopEditorOpening\",\n        \"postStopEditorOpened\",\n      ]);\n\n    const stale = {};\n    if (sendingChunks) {\n      stale.sendingChunks = false;\n      console.warn(\"[Screenity][BG] Stale lock found on startup: sendingChunks — clearing\");\n    }\n    if (postStopEditorOpening) {\n      stale.postStopEditorOpening = false;\n      console.warn(\"[Screenity][BG] Stale lock found on startup: postStopEditorOpening — clearing\");\n    }\n    if (postStopEditorOpened) {\n      stale.postStopEditorOpened = false;\n      console.warn(\"[Screenity][BG] Stale lock found on startup: postStopEditorOpened — clearing\");\n    }\n\n    if (Object.keys(stale).length > 0) {\n      await chrome.storage.local.set(stale);\n      console.info(\n        \"[Screenity][BG] Startup stale locks cleared:\",\n        Object.keys(stale).join(\", \"),\n      );\n    }\n  } catch (err) {\n    console.error(\"[Screenity][BG] Failed to clear stale startup locks:\", err);\n  }\n};\n\n// Event listeners must be registered synchronously at module evaluation time\n// so Chrome counts them for service worker keep-alive.\nmessageRouter();\ninitializeListeners();\nsetupHandlers();\n\n// Fire-and-forget: clears stale locks from a previous crashed session.\n// Runs after listener registration (required synchronous) but before Chrome\n// can dispatch any queued events to this worker.\nclearStaleLocks();\n\n// Hydrate the diagnostic log from storage and record that the SW (re)started.\nhydrateDiagnosticLog().then(() => {\n  diagEvent(\"sw-init\", { ts: Date.now() });\n});\n"
  },
  {
    "path": "src/pages/Background/listeners/index.js",
    "content": "import { onCommandListener } from \"./onCommandListener\";\nimport { onInstalledListener } from \"./onInstalledListener\";\nimport { onTabRemovedListener } from \"./onTabRemovedListener\";\nimport { onTabActivatedListener } from \"./onTabActivatedListener\";\nimport { onTabUpdatedListener } from \"./onTabUpdatedListener\";\nimport { onWindowFocusChangedListener } from \"./onWindowFocusChangedListener\";\nimport { onActionButtonClickedListener } from \"./onActionButtonClickedListener\";\nimport { onStartupListener } from \"./onStartupListener\";\nimport { onMessageExternalListener } from \"./onMessageExternalListener\";\n\n// Initialize all listeners\nexport const initializeListeners = () => {\n  onCommandListener();\n  onInstalledListener();\n  onTabRemovedListener();\n  onTabActivatedListener();\n  onTabUpdatedListener();\n  onWindowFocusChangedListener();\n  onActionButtonClickedListener();\n  onStartupListener();\n  onMessageExternalListener();\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onActionButtonClickedListener.js",
    "content": "import {\n  sendMessageTab,\n  getCurrentTab,\n  setEditorTabReference,\n  clearEditorTabReference,\n} from \"../tabManagement\";\nimport { sendMessageRecord } from \"../recording/sendMessageRecord.js\";\nimport { loginWithWebsite } from \"../auth/loginWithWebsite.js\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\n// Utility to handle tab messaging logic\nconst handleTabMessaging = async (tab) => {\n  const { activeTab, recordingUiTabId } = await chrome.storage.local.get([\n    \"activeTab\",\n    \"recordingUiTabId\",\n  ]);\n  const preferredTabId = recordingUiTabId || activeTab;\n\n  try {\n    const targetTab = preferredTabId\n      ? await chrome.tabs.get(preferredTabId)\n      : null;\n\n    if (targetTab) {\n      await sendMessageTab(preferredTabId, { type: \"stop-recording-tab\" });\n    } else {\n      await sendMessageTab(tab.id, { type: \"stop-recording-tab\" });\n      chrome.storage.local.set({ activeTab: tab.id });\n    }\n  } catch (error) {\n    console.error(\"[Screenity][ActionClick] handleTabMessaging failed, trying direct recorder stop:\", error);\n    try {\n      await sendMessageRecord({ type: \"stop-recording-tab\" });\n    } catch (recorderErr) {\n      console.error(\"[Screenity][ActionClick] direct recorder stop also failed:\", recorderErr);\n    }\n  }\n};\n\n// Utility to open Playground or inject popup\nconst openPlaygroundOrPopup = async (tab) => {\n  const editorUrlPattern =\n    /https:\\/\\/app\\.screenity\\.io\\/editor\\/([^\\/]+)(\\/edit)?\\/?/;\n\n  if (tab.url && editorUrlPattern.test(tab.url)) {\n    const match = tab.url.match(editorUrlPattern);\n    const projectIdFromUrl = match?.[2] || null;\n    await setEditorTabReference({\n      tabId: tab.id,\n      tabUrl: tab.url,\n      source: \"action-click-editor\",\n      expectedProjectId: projectIdFromUrl,\n    });\n\n    if (CLOUD_FEATURES_ENABLED) {\n      const result = await loginWithWebsite();\n\n      if (result?.authenticated) {\n        await chrome.storage.local.set({\n          projectId: projectIdFromUrl,\n          recordingToScene: true,\n          instantMode: false,\n        });\n\n        sendMessageTab(tab.id, {\n          type: \"get-project-info\",\n        });\n      } else {\n        await chrome.storage.local.set({\n          projectId: null,\n          recordingToScene: false,\n          activeSceneId: null,\n        });\n      }\n    } else {\n      await chrome.storage.local.set({\n        projectId: null,\n        recordingToScene: false,\n        activeSceneId: null,\n      });\n    }\n  } else {\n    await clearEditorTabReference(\"action-click-non-editor-tab\", {\n      tabId: tab.id,\n      tabUrl: tab.url,\n    });\n    await chrome.storage.local.set({\n      projectId: null,\n      recordingToScene: false,\n      activeSceneId: null, // reset scene too if needed\n    });\n  }\n\n  const forbiddenURLs = [\n    \"chrome://\",\n    \"chrome-extension://\",\n    \"chrome.google.com/webstore\",\n    \"chromewebstore.google.com\",\n    \"stackoverflow.com/\",\n  ];\n\n  const isForbidden = forbiddenURLs.some((url) => tab.url.startsWith(url));\n  const isPlaygroundOrSetup =\n    tab.url.includes(\"/playground.html\") || tab.url.includes(\"/setup.html\");\n\n  if ((!isForbidden || isPlaygroundOrSetup) && navigator.onLine) {\n    sendMessageTab(tab.id, { type: \"toggle-popup\" })\n      .then(() => console.log(\"[Screenity][ActionClick] toggle-popup delivered to tab\", tab.id))\n      .catch((err) => console.error(\"[Screenity][ActionClick] toggle-popup FAILED to tab\", tab.id, String(err).slice(0, 120)));\n    chrome.storage.local.set({ activeTab: tab.id });\n  } else {\n    const newTab = await chrome.tabs.create({\n      url: \"playground.html\",\n      active: true,\n    });\n    chrome.storage.local.set({ activeTab: newTab.id });\n\n    const onUpdated = (tabId, changeInfo, updatedTab) => {\n      if (updatedTab.id === newTab.id && changeInfo.status === \"complete\") {\n        chrome.tabs.onUpdated.removeListener(onUpdated);\n        setTimeout(() => {\n          sendMessageTab(newTab.id, { type: \"toggle-popup\" });\n        }, 500);\n      }\n    };\n    chrome.tabs.onUpdated.addListener(onUpdated);\n  }\n};\n\n/** Check whether a tab ID refers to a tab that actually exists. */\nconst doesTabExist = async (tabId) => {\n  if (!Number.isInteger(tabId)) return false;\n  try {\n    await chrome.tabs.get(tabId);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\n/** Check whether an offscreen document is actually running. */\nconst isOffscreenAlive = async () => {\n  try {\n    const contexts = await chrome.runtime.getContexts({});\n    return contexts.some((c) => c.contextType === \"OFFSCREEN_DOCUMENT\");\n  } catch {\n    return false;\n  }\n};\n\n// Main action button listener\nexport const onActionButtonClickedListener = () => {\n  chrome.action.onClicked.addListener(async (tab) => {\n    try {\n      const snap = await chrome.storage.local.get([\n        \"recording\", \"pendingRecording\", \"restarting\", \"recorderSession\",\n        \"recordingTab\", \"offscreen\",\n        \"postStopEditorOpening\", \"postStopEditorOpened\",\n        \"editorRecoveryUrl\",\n      ]);\n      console.log(\"[Screenity][ActionClick] storage:\", snap, \"tab:\", tab.id);\n\n      if (snap.editorRecoveryUrl) {\n        await chrome.storage.local.remove([\"editorRecoveryUrl\", \"editorRecoveryAt\"]);\n      }\n\n      const { recording, pendingRecording, restarting, recorderSession } = snap;\n      const sessionRecording = recorderSession?.status === \"recording\";\n      const isRecordingActive = Boolean(\n        recording || pendingRecording || restarting || sessionRecording,\n      );\n\n      if (isRecordingActive) {\n        const { recordingTab, offscreen } = snap;\n        let hasActiveRecorder = false;\n\n        if (sessionRecording) {\n          const sessionOwnerTabId =\n            recorderSession?.recorderTabId || recorderSession?.tabId || null;\n          if (sessionOwnerTabId && (await doesTabExist(sessionOwnerTabId))) {\n            hasActiveRecorder = true;\n          } else {\n            console.warn(\n              \"[Screenity][ActionClick] recorderSession stale (owner tab\",\n              sessionOwnerTabId,\n              \"is dead) — clearing\",\n            );\n            chrome.storage.local.set({\n              recorderSession: recorderSession\n                ? { ...recorderSession, status: \"stale-cleared\", clearedAt: Date.now() }\n                : null,\n            });\n          }\n        }\n\n        if (recordingTab && !hasActiveRecorder) {\n          if (await doesTabExist(recordingTab)) {\n            hasActiveRecorder = true;\n          } else {\n            console.warn(\"[Screenity][ActionClick] recordingTab\", recordingTab, \"is dead — clearing\");\n            chrome.storage.local.set({ recordingTab: null });\n          }\n        }\n\n        if (offscreen && !hasActiveRecorder) {\n          if (await isOffscreenAlive()) {\n            hasActiveRecorder = true;\n          } else {\n            console.warn(\"[Screenity][ActionClick] offscreen flag stale (no document) — clearing\");\n            chrome.storage.local.set({ offscreen: false });\n          }\n        }\n\n        if (!hasActiveRecorder) {\n          console.warn(\n            \"[Screenity][ActionClick] branch: stale-reset-then-popup.\",\n            { recording, pendingRecording, restarting, sessionRecording, recordingTab, offscreen },\n          );\n          await chrome.storage.local.set({\n            recording: false,\n            pendingRecording: false,\n            restarting: false,\n            offscreen: false,\n            recordingTab: null,\n            postStopEditorOpened: false,\n            postStopEditorOpening: false,\n            recordingToScene: false,\n            projectId: null,\n            activeSceneId: null,\n          });\n          chrome.runtime\n            .sendMessage({ type: \"clear-recording-session-safe\", reason: \"action-button-stale-reset\" })\n            .catch(() => {});\n          await openPlaygroundOrPopup(tab);\n        } else {\n          console.log(\"[Screenity][ActionClick] branch: handle-tab-messaging (active recorder)\");\n          await handleTabMessaging(tab);\n        }\n      } else {\n        console.log(\"[Screenity][ActionClick] branch: normal-popup\");\n        // Reset storage keys before opening the popup\n        await chrome.storage.local.set({\n          recordingToScene: false,\n          projectId: null,\n          activeSceneId: null,\n        });\n        await openPlaygroundOrPopup(tab);\n      }\n\n      const { firstTime } = await chrome.storage.local.get([\"firstTime\"]);\n      if (firstTime && tab.url.includes(chrome.runtime.getURL(\"setup.html\"))) {\n        chrome.storage.local.set({ firstTime: false });\n        const activeTab = await getCurrentTab();\n        sendMessageTab(activeTab.id, { type: \"setup-complete\" });\n      }\n    } catch (error) {\n      console.error(\"Error handling action click:\", error);\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onCommandListener.js",
    "content": "import { sendMessageTab, getCurrentTab } from \"../tabManagement\";\n\n// Main command listener\nexport const onCommandListener = () => {\n  let lastToggleDrawingAt = 0;\n  const TOGGLE_DRAWING_COOLDOWN_MS = 400;\n\n  chrome.commands.onCommand.addListener(async (command) => {\n    const activeTab = await getCurrentTab();\n    if (!activeTab || !activeTab.id) return;\n\n    if (command === \"start-recording\") {\n      // Validate if it's possible to inject into content\n      if (\n        !(\n          (navigator.onLine === false &&\n            !activeTab.url.includes(\"/playground.html\") &&\n            !activeTab.url.includes(\"/setup.html\")) ||\n          activeTab.url.startsWith(\"chrome://\") ||\n          (activeTab.url.startsWith(\"chrome-extension://\") &&\n            !activeTab.url.includes(\"/playground.html\") &&\n            !activeTab.url.includes(\"/setup.html\"))\n        ) &&\n        !activeTab.url.includes(\"stackoverflow.com/\") &&\n        !activeTab.url.includes(\"chrome.google.com/webstore\") &&\n        !activeTab.url.includes(\"chromewebstore.google.com\")\n      ) {\n        sendMessageTab(activeTab.id, { type: \"start-stream\" });\n      } else {\n        chrome.tabs\n          .create({\n            url: \"playground.html\",\n            active: true,\n          })\n          .then((tab) => {\n            chrome.storage.local.set({ activeTab: tab.id });\n\n            // Wait for the tab to load\n            chrome.tabs.onUpdated.addListener(function _(tabId, changeInfo) {\n              if (tabId === tab.id && changeInfo.status === \"complete\") {\n                setTimeout(() => {\n                  sendMessageTab(tab.id, { type: \"start-stream\" });\n                }, 500);\n                chrome.tabs.onUpdated.removeListener(_);\n              }\n            });\n          });\n      }\n    } else if (command === \"cancel-recording\") {\n      sendMessageTab(activeTab.id, { type: \"cancel-recording\" });\n    } else if (command === \"pause-recording\") {\n      sendMessageTab(activeTab.id, { type: \"pause-recording\" });\n    } else if (command === \"stop-recording\") {\n      sendMessageTab(activeTab.id, { type: \"stop-recording-tab\" });\n    } else if (command === \"toggle-drawing-mode\") {\n      const now = Date.now();\n      if (now - lastToggleDrawingAt < TOGGLE_DRAWING_COOLDOWN_MS) {\n        return;\n      }\n      lastToggleDrawingAt = now;\n      sendMessageTab(activeTab.id, { type: \"toggle-drawing-mode\" });\n    } else if (command === \"toggle-blur-mode\") {\n      sendMessageTab(activeTab.id, { type: \"toggle-blur-mode\" });\n    } else if (command === \"toggle-hide-ui\") {\n      sendMessageTab(activeTab.id, { type: \"toggle-hide-ui\" });\n    } else if (command === \"toggle-cursor-mode\") {\n      sendMessageTab(activeTab.id, { type: \"toggle-cursor-mode\" });\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onInstalledListener.js",
    "content": "import { removeTab } from \"../tabManagement\";\nimport { executeScripts } from \"../utils/executeScripts\";\nimport { supportContextQuery } from \"../../utils/buildSupportContext\";\n\nconst cloudFeaturesEnabled =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nexport const onInstalledListener = () => {\n  chrome.runtime.onInstalled.addListener(async (details) => {\n    const version = chrome.runtime.getManifest().version;\n    const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n\n    if (details.reason === \"install\") {\n      // Clear storage on fresh install\n      chrome.storage.local.clear();\n\n      // Set uninstall URL based on locale\n      const installQs = await supportContextQuery({ source: \"uninstall\" });\n      const installUrl = `https://tally.so/r/w8Zro5?${installQs}`;\n      chrome.runtime.setUninstallURL(\n        locale.includes(\"en\")\n          ? installUrl\n          : `http://translate.google.com/translate?js=n&sl=auto&tl=${locale}&u=${encodeURIComponent(installUrl)}`\n      );\n\n      chrome.storage.local.set({\n        firstTime: true,\n        onboarding: cloudFeaturesEnabled,\n        bannerSupport: true,\n        firstTimePro: cloudFeaturesEnabled,\n      });\n\n      chrome.storage.managed.get(\"skipSetup\", (managedConfig) => {\n        const skipSetup = managedConfig.skipSetup ?? false;\n        if (!skipSetup) {\n          chrome.tabs.create({ url: \"setup.html\" });\n        }\n      });\n    } else if (details.reason === \"update\") {\n      if (details.previousVersion === \"2.8.6\") {\n        // Do not clear local storage on update; preserve onboarding/versioned keys.\n        chrome.storage.local.set({ updatingFromOld: true });\n      } else {\n        chrome.storage.local.set({ updatingFromOld: false });\n\n        // Onboarding for new cloud version\n        if (details.previousVersion === \"3.1.16\" && cloudFeaturesEnabled) {\n          chrome.storage.local.set({\n            showProSplash: cloudFeaturesEnabled,\n            bannerSupport: true,\n            onboarding: cloudFeaturesEnabled,\n          });\n        }\n      }\n\n      const updateQs = await supportContextQuery({ source: \"uninstall\" });\n      const updateUrl = `https://tally.so/r/3Ex6kX?${updateQs}`;\n      chrome.runtime.setUninstallURL(\n        locale.includes(\"en\")\n          ? updateUrl\n          : `http://translate.google.com/translate?js=n&sl=auto&tl=${locale}&u=${encodeURIComponent(updateUrl)}`\n      );\n    }\n\n    // Disable backups for older Chrome versions\n    if (navigator.userAgent.includes(\"Chrome/\")) {\n      const chromeVersion = parseInt(\n        navigator.userAgent.match(/Chrome\\/([0-9]+)/)?.[1] ?? \"0\"\n      );\n      if (chromeVersion <= 109) {\n        chrome.storage.local.set({ backup: false });\n      }\n    }\n\n    chrome.storage.local.set({ systemAudio: true });\n\n    const { backupTab } = await chrome.storage.local.get([\"backupTab\"]);\n    if (backupTab) {\n      removeTab(backupTab);\n    }\n\n    executeScripts();\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onMessageExternalListener.js",
    "content": "import {\n  getCurrentTab,\n  sendMessageTab,\n  parseEditorTargetUrl,\n  setEditorTabReference,\n  clearEditorTabReference,\n} from \"../tabManagement\";\nimport { loginWithWebsite } from \"../auth/loginWithWebsite\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nexport const onMessageExternalListener = () => {\n  if (!CLOUD_FEATURES_ENABLED) return;\n\n  chrome.runtime.onMessageExternal.addListener(\n    async (message, sender, sendResponse) => {\n      if (message.type === \"AUTH_SUCCESS\" && message.token) {\n        // User explicitly chose to stay logged out — ignore incoming tokens\n        // until they initiate a login themselves.\n        const { stayLoggedOut } = await chrome.storage.local.get([\n          \"stayLoggedOut\",\n        ]);\n        if (stayLoggedOut) return true;\n\n        await chrome.storage.local.set({\n          screenityToken: message.token,\n          screenityUser: null,\n          proSubscription: null,\n          lastAuthCheck: 0,\n          wasLoggedIn: true,\n          isLoggedIn: false,\n          pushToTalk: false,\n          onboarding: false,\n          showProSplash: false,\n        });\n\n        const auth = await loginWithWebsite();\n\n        if (!auth?.authenticated) {\n          await chrome.storage.local.set({\n            isLoggedIn: false,\n          });\n          return true;\n        }\n\n        await chrome.storage.local.set({\n          isLoggedIn: true,\n          wasLoggedIn: false,\n          stayLoggedOut: false,\n          screenityUser: auth.user,\n          isSubscribed: auth.subscribed,\n          proSubscription: auth.proSubscription,\n          hasSubscribedBefore: auth.hasSubscribedBefore,\n          lastAuthCheck: Date.now(),\n        });\n\n        const { originalTabId } = await chrome.storage.local.get(\n          \"originalTabId\"\n        );\n\n        if (originalTabId) {\n          chrome.tabs.update(originalTabId, { active: true });\n          sendMessageTab(originalTabId, { type: \"LOGIN_SUCCESS\" });\n\n          if (sender.tab?.id) {\n            chrome.tabs.remove(sender.tab.id); // Close login tab\n          }\n\n          await chrome.storage.local.remove(\"originalTabId\");\n\n          // Message tab to update from storage\n          sendMessageTab(originalTabId, {\n            type: \"check-auth\",\n            senderId: sender.tab.id,\n          });\n        }\n\n        return true;\n      } else if (message.type === \"SIGN_OUT\") {\n        // Clear all auth-related storage\n        await chrome.storage.local.remove([\n          \"screenityToken\",\n          \"screenityUser\",\n          \"lastAuthCheck\",\n          \"isSubscribed\",\n          \"isLoggedIn\",\n          \"proSubscription\",\n          \"hasSubscribedBefore\",\n        ]);\n\n        return true;\n      } else if (message.type === \"OPEN_POPUP_PROJECT\") {\n        try {\n          const tab = await getCurrentTab();\n          if (!tab?.id) {\n            console.warn(\"No active tab found for popup reopen\");\n            return;\n          }\n\n          await chrome.storage.local.set({\n            recordingProjectTitle: message.recordingProjectTitle,\n            recordingToScene: true,\n            instantMode: false,\n            projectId: message.projectId,\n            activeSceneId: message.activeSceneId,\n          });\n\n          const parsedTarget = parseEditorTargetUrl(tab.url);\n          if (parsedTarget?.projectId === message.projectId) {\n            await setEditorTabReference({\n              tabId: tab.id,\n              tabUrl: tab.url,\n              source: \"webapp-open-popup-project\",\n              expectedProjectId: message.projectId,\n            });\n          } else {\n            await clearEditorTabReference(\"webapp-open-popup-project-not-editor\", {\n              tabId: tab.id,\n              tabUrl: tab.url,\n              expectedProjectId: message.projectId,\n            });\n          }\n\n          const result = await loginWithWebsite();\n\n          if (!result?.authenticated) {\n            const currentTab = await getCurrentTab();\n            if (currentTab?.id) {\n              await chrome.storage.local.set({ originalTabId: currentTab.id });\n            }\n\n            chrome.tabs.create({\n              url: `${process.env.SCREENITY_APP_BASE}/login?extension=true`,\n              active: true,\n            });\n            return;\n          }\n\n          await sendMessageTab(tab.id, {\n            type: \"open-popup-project\",\n            projectTitle: message.recordingProjectTitle,\n            projectId: message.projectId,\n            activeSceneId: message.activeSceneId,\n            recordingToScene: true,\n          });\n        } catch (err) {\n          console.warn(\"Failed to send popup project message:\", err);\n        }\n      } else if (message.type === \"GET_PROJECT_INFO\") {\n        const tab = await getCurrentTab();\n        if (!tab?.id) {\n          console.warn(\"No active tab found for popup reopen\");\n          return;\n        }\n\n        await chrome.storage.local.set({\n          recordingProjectTitle: message.recordingProjectTitle,\n          recordingToScene: true,\n          instantMode: false,\n          projectId: message.projectId,\n          activeSceneId: message.activeSceneId,\n        });\n\n        await sendMessageTab(tab.id, {\n          type: \"open-popup-project\",\n          projectTitle: message.recordingProjectTitle,\n          projectId: message.projectId,\n          activeSceneId: message.activeSceneId,\n          recordingToScene: true,\n        });\n      } else if (message.type === \"PING_FROM_WEBAPP\") {\n        sendResponse({ success: true, message: \"pong\" });\n\n        return true;\n      }\n    }\n  );\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onStartupListener.js",
    "content": "// For some reason without this the service worker doesn't always work\nexport const onStartupListener = async () => {\n  chrome.runtime.onStartup.addListener(() => {\n    if (globalThis.SCREENITY_VERBOSE_LOGS) {\n      console.log(\"Service worker started up successfully.\");\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onTabActivatedListener.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\n\nexport const handleTabActivation = async (activeInfo) => {\n  try {\n    const {\n      recordingStartTime,\n      recording,\n      paused,\n      pausedAt,\n      totalPausedMs,\n      restarting,\n      pendingRecording,\n      recorderSession,\n    } = await chrome.storage.local.get([\n      \"recordingStartTime\",\n      \"recording\",\n      \"paused\",\n      \"pausedAt\",\n      \"totalPausedMs\",\n      \"restarting\",\n      \"pendingRecording\",\n      \"recorderSession\",\n    ]);\n\n    // Get the activated tab\n    const tab = await chrome.tabs.get(activeInfo.tabId);\n\n    // Check both recording flag AND recorderSession to avoid race conditions\n    // recorderSession persists even if the SW restarts\n    const isActivelyRecording =\n      recording || (recorderSession && recorderSession.status === \"recording\");\n\n    if (isActivelyRecording) {\n      // Check if region recording and if the current tab is the recording tab\n      const { tabRecordedID, region, customRegion, recordingType } =\n        await chrome.storage.local.get([\n          \"tabRecordedID\",\n          \"region\",\n          \"customRegion\",\n          \"recordingType\",\n        ]);\n      if (tabRecordedID && tabRecordedID !== activeInfo.tabId) {\n        sendMessageTab(activeInfo.tabId, { type: \"hide-popup-recording\" });\n      } else if (\n        !(\n          tab.url.includes(\"backup.html\") &&\n          tab.url.includes(\"chrome-extension://\")\n        )\n      ) {\n        // Update the active tab reference\n        chrome.storage.local.set({ activeTab: activeInfo.tabId });\n      }\n\n      // Check if it's region or customRegion recording\n      if (!region && !customRegion && recordingType !== \"region\") {\n        sendMessageTab(activeInfo.tabId, {\n          type: \"recording-check\",\n          recordingStartTime,\n        });\n      }\n    } else if (!isActivelyRecording && !restarting && !pendingRecording) {\n      sendMessageTab(activeInfo.tabId, { type: \"recording-ended\" });\n    }\n\n    // If there's a recording start time, update the UI with time\n    if (recordingStartTime) {\n      const now = Date.now();\n      const basePaused = totalPausedMs || 0;\n      const extraPaused = paused && pausedAt ? Math.max(0, now - pausedAt) : 0;\n\n      const elapsed = Math.max(\n        0,\n        Math.floor((now - recordingStartTime - basePaused - extraPaused) / 1000)\n      );\n\n      const { alarm } = await chrome.storage.local.get([\"alarm\"]);\n      if (alarm) {\n        const { alarmTime } = await chrome.storage.local.get([\"alarmTime\"]);\n        const remaining = Math.max(0, Math.floor(alarmTime - elapsed));\n        sendMessageTab(activeInfo.tabId, { type: \"time\", time: remaining });\n      } else {\n        sendMessageTab(activeInfo.tabId, { type: \"time\", time: elapsed });\n      }\n    }\n  } catch (error) {\n    console.error(\"Error in handleTabActivation:\", error.message);\n  }\n};\n\nexport const onTabActivatedListener = () => {\n  chrome.tabs.onActivated.addListener(handleTabActivation);\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onTabRemovedListener.js",
    "content": "import { sendMessageTab, clearEditorTabReference } from \"../tabManagement\";\nimport { removeTab } from \"../tabManagement/removeTab\";\nimport { sendMessageRecord } from \"../recording/sendMessageRecord\";\nimport { diagEvent, endDiagSession } from \"../../utils/diagnosticLog\";\n\n/**\n * Listener for when a tab is removed.\n * It checks if the removed tab is the active recording tab and handles cleanup if so.\n */\nexport const onTabRemovedListener = () => {\n  chrome.tabs.onRemoved.addListener(async (tabId) => {\n    try {\n      const {\n        recording,\n        pendingRecording,\n        restarting,\n        recordingTab,\n        tabRecordedID,\n        recordingUiTabId,\n        activeTab,\n        recorderSession,\n        editorTab,\n        sandboxTab,\n      } = await chrome.storage.local.get([\n        \"recording\",\n        \"pendingRecording\",\n        \"restarting\",\n        \"recordingTab\",\n        \"tabRecordedID\",\n        \"recordingUiTabId\",\n        \"activeTab\",\n        \"recorderSession\",\n        \"editorTab\",\n        \"sandboxTab\",\n      ]);\n\n      if (tabId === editorTab) {\n        await clearEditorTabReference(\"editor-tab-closed\", { tabId });\n      }\n\n      // Editor/sandbox tab closed — close the orphaned recorder tab (pinned\n      // recorder.html) so it doesn't linger. stopRecording() opens editors as\n      // sandboxTab (not editorTab), so we must check both.\n      const isEditorOrSandbox = tabId === editorTab || tabId === sandboxTab;\n      if (isEditorOrSandbox) {\n        if (tabId === sandboxTab) {\n          chrome.storage.local.set({ sandboxTab: null });\n        }\n        const isStillRecording =\n          recording ||\n          (recorderSession && recorderSession.status === \"recording\");\n        if (!isStillRecording && recordingTab && recordingTab !== tabId) {\n          try {\n            const recTab = await chrome.tabs.get(recordingTab);\n            const recUrl = recTab?.url || \"\";\n            if (\n              recUrl.includes(\"recorder.html\") ||\n              recUrl.includes(\"cloudrecorder.html\")\n            ) {\n              removeTab(recordingTab);\n            }\n          } catch {\n            // Tab already gone\n          }\n          chrome.storage.local.set({ recordingTab: null });\n        }\n      }\n\n      const recordedTabId = tabRecordedID || recordingTab;\n\n      // Check both recording flag AND recorderSession\n      const isActivelyRecording =\n        recording ||\n        (recorderSession && recorderSession.status === \"recording\");\n      const recorderOwnerTabId =\n        recorderSession?.recorderTabId || recorderSession?.tabId || null;\n\n      if (tabId === recorderOwnerTabId) {\n        chrome.runtime\n          .sendMessage({\n            type: \"clear-recording-session-safe\",\n            reason: \"recorder-owner-tab-removed\",\n          })\n          .catch(() => {});\n\n        if (recorderSession && recorderSession.status === \"recording\") {\n          diagEvent(\"crash\", { reason: \"recorder-owner-tab-removed\", tabId });\n          await chrome.storage.local.set({\n            recorderSession: {\n              ...recorderSession,\n              status: \"crashed\",\n              crashedAt: Date.now(),\n            },\n            recording: false,\n          });\n          endDiagSession(\"crashed\");\n        }\n      }\n\n      // If the removed tab is the one being recorded (for tab capture)\n      if (!restarting && isActivelyRecording && tabId === recordedTabId) {\n        diagEvent(\"recorded-tab-closed\");\n        // Clear reference to the removed tab\n        chrome.storage.local.set({ recordingTab: null, tabRecordedID: null });\n\n        // Send stop directly to the recorder, not through content script\n        // This is more reliable as the content script tab may not exist\n        try {\n          await sendMessageRecord({\n            type: \"stop-recording-tab\",\n            reason: \"recorded-tab-closed\",\n          });\n        } catch (err) {\n          console.warn(\"Could not message recorder to stop:\", err);\n        }\n\n        // Also try to notify the active tab for UI cleanup\n        const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n        if (activeTab && activeTab !== tabId) {\n          sendMessageTab(activeTab, { type: \"stop-pending\" }).catch(() => {});\n        }\n\n        chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n      }\n\n      // If the CloudRecorder tab itself was closed, that's a critical failure\n      if (\n        !restarting &&\n        isActivelyRecording &&\n        tabId === recordingTab &&\n        recordingTab !== recordedTabId\n      ) {\n        diagEvent(\"crash\", { reason: \"cloud-recorder-tab-closed\", tabId });\n        endDiagSession(\"crashed\");\n        console.error(\"CloudRecorder tab was closed during recording!\");\n        chrome.storage.local.set({\n          recording: false,\n          recorderSession: recorderSession\n            ? { ...recorderSession, status: \"crashed\", crashedAt: Date.now() }\n            : null,\n        });\n        chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n      }\n\n      // If the recorder tab (recorder.html) is closed after recording ends,\n      // clear the stale reference so it doesn't break the next recording start.\n      if (tabId === recordingTab && !isActivelyRecording && !pendingRecording) {\n        chrome.storage.local.set({ recordingTab: null });\n      }\n\n      // If recorder tab is closed before recording starts, clear pending UI state.\n      const pendingOnly =\n        pendingRecording && !isActivelyRecording && !restarting;\n      if (tabId === recordingTab && pendingOnly) {\n        await chrome.storage.local.set({\n          pendingRecording: false,\n          recording: false,\n          recordingTab: null,\n          tabRecordedID: null,\n          recordingUiTabId: null,\n        });\n        chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n\n        const candidateTabs = [activeTab, recordingUiTabId, tabRecordedID].filter(\n          (id, idx, arr) => Number.isInteger(id) && arr.indexOf(id) === idx,\n        );\n        candidateTabs.forEach((id) => {\n          sendMessageTab(id, { type: \"stop-pending\" }).catch(() => {});\n        });\n      }\n    } catch (error) {\n      console.error(\"Error handling tab removal:\", error.message);\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onTabUpdatedListener.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\nimport { diagEvent } from \"../../utils/diagnosticLog\";\nimport { videoReady } from \"../recording/recordingHelpers\";\nimport { chunksStore } from \"../recording/chunkHandler\";\n\nexport const handleTabUpdate = async (tabId, changeInfo, tab) => {\n  try {\n    // React to URL changes EARLY (fires at navigation start, before the new\n    // page loads) so we can clear recording state before the new content\n    // script re-injects the region UI.\n    if (changeInfo.url) {\n      const {\n        recording,\n        tabRecordedID,\n        recorderSession,\n        customRegion,\n        recordingType,\n        restarting,\n      } = await chrome.storage.local.get([\n        \"recording\",\n        \"tabRecordedID\",\n        \"recorderSession\",\n        \"customRegion\",\n        \"recordingType\",\n        \"restarting\",\n      ]);\n\n      const isActivelyRecording =\n        recording ||\n        (recorderSession && recorderSession.status === \"recording\");\n      const isRegionRecording = customRegion || recordingType === \"region\";\n\n      if (\n        !restarting &&\n        isActivelyRecording &&\n        isRegionRecording &&\n        tabRecordedID &&\n        tabRecordedID === tabId\n      ) {\n        diagEvent(\"region-nav-stop\", { tabId });\n        // Clear ALL recording state IMMEDIATELY so the new page's content\n        // script won't see an active recording, and so the status=complete\n        // handler won't send a recording-check that re-enables the UI.\n        // Also clear recordingTab so stopRecording() opens the editor\n        // instead of thinking it was already handled.\n        await chrome.storage.local.set({\n          recording: false,\n          customRegion: false,\n          recordingTab: null,\n          postStopEditorOpening: false,\n          postStopEditorOpened: false,\n          recorderSession: recorderSession\n            ? { ...recorderSession, status: \"stopped\" }\n            : null,\n        });\n        const chunkCount = await chunksStore.length().catch(() => 0);\n        if (chunkCount === 0) {\n          diagEvent(\"region-nav-no-chunks\");\n          sendMessageTab(tabId, {\n            type: \"show-toast\",\n            message: chrome.i18n.getMessage(\"recordingTooShortToast\"),\n            timeout: 5000,\n          }).catch(() => {});\n          return;\n        }\n        await videoReady();\n        return; // Skip the status=complete handler for this event\n      }\n    }\n\n    if (changeInfo.status === \"complete\") {\n      const {\n        recording,\n        paused,\n        pausedAt,\n        totalPausedMs,\n        restarting,\n        tabRecordedID,\n        pendingRecording,\n        recordingStartTime,\n        recorderSession,\n        customRegion,\n        recordingType,\n      } = await chrome.storage.local.get([\n        \"recording\",\n        \"paused\",\n        \"pausedAt\",\n        \"totalPausedMs\",\n        \"restarting\",\n        \"tabRecordedID\",\n        \"pendingRecording\",\n        \"recordingStartTime\",\n        \"recorderSession\",\n        \"customRegion\",\n        \"recordingType\",\n      ]);\n\n      // Check both recording flag AND recorderSession to avoid race conditions\n      // recorderSession persists even if the SW restarts\n      const isActivelyRecording =\n        recording ||\n        (recorderSession && recorderSession.status === \"recording\");\n      const isPendingOrRestarting = restarting || pendingRecording;\n\n      if (!isActivelyRecording && !isPendingOrRestarting) {\n        sendMessageTab(tabId, { type: \"recording-ended\" });\n      } else if (isActivelyRecording) {\n        if (tabRecordedID && tabRecordedID === tabId) {\n          diagEvent(\"recorded-tab-navigated\");\n\n          // For non-region recordings, send a check to the content script\n          sendMessageTab(tabId, {\n            type: \"recording-check\",\n            force: true,\n            recordingStartTime,\n          });\n        } else if (tabRecordedID && tabRecordedID !== tabId) {\n          sendMessageTab(tabId, { type: \"hide-popup-recording\" });\n        }\n      }\n\n      if (recordingStartTime) {\n        const now = Date.now();\n        const basePaused = totalPausedMs || 0;\n        const extraPaused =\n          paused && pausedAt ? Math.max(0, now - pausedAt) : 0;\n\n        const elapsed = Math.max(\n          0,\n          Math.floor(\n            (now - recordingStartTime - basePaused - extraPaused) / 1000\n          )\n        );\n\n        const { alarm } = await chrome.storage.local.get([\"alarm\"]);\n        if (alarm) {\n          const { alarmTime } = await chrome.storage.local.get([\"alarmTime\"]);\n          const remaining = Math.max(0, Math.floor(alarmTime - elapsed));\n          sendMessageTab(tabId, { type: \"time\", time: remaining });\n        } else {\n          sendMessageTab(tabId, { type: \"time\", time: elapsed });\n        }\n      }\n\n      const commands = await chrome.commands.getAll();\n\n      sendMessageTab(tabId, {\n        type: \"commands\",\n        commands: commands,\n      });\n\n      // Check if tab is playground.html\n      if (\n        tab.url.includes(chrome.runtime.getURL(\"playground.html\")) &&\n        changeInfo.status === \"complete\"\n      ) {\n        sendMessageTab(tab.id, { type: \"toggle-popup\" });\n      }\n    }\n  } catch (error) {\n    console.error(\"Error in handleTabUpdate:\", error.message);\n  }\n};\n\nexport const onTabUpdatedListener = () => {\n  chrome.tabs.onUpdated.addListener(handleTabUpdate);\n};\n"
  },
  {
    "path": "src/pages/Background/listeners/onWindowFocusChangedListener.js",
    "content": "import { handleTabActivation } from \"./onTabActivatedListener.js\";\n\nexport const onWindowFocusChangedListener = async (windowId) => {\n  if (windowId === chrome.windows.WINDOW_ID_NONE) return;\n\n  try {\n    const tabs = await chrome.tabs.query({ active: true, windowId });\n    if (tabs && tabs[0]) {\n      handleTabActivation({ tabId: tabs[0].id });\n    }\n  } catch (error) {\n    console.error(\"Failed to query active tab:\", error);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/messaging/handlers.js",
    "content": "import { registerMessage } from \"../../../messaging/messageRouter\";\nimport {\n  focusTab,\n  createTab,\n  resetActiveTab,\n  resetActiveTabRestart,\n  setSurface,\n} from \"../tabManagement\";\n\nimport { startAfterCountdown, startRecording } from \"../recording/startRecording\";\nimport {\n  handleStopRecordingTab,\n  handleStopRecordingTabBackup,\n} from \"../recording/stopRecording\";\nimport { sendChunks } from \"../recording/sendChunks\";\nimport { chunksStore } from \"../recording/chunkHandler\";\nimport { handleSaveToDrive } from \"../drive/handleSaveToDrive\";\nimport { addAlarmListener } from \"../alarms/addAlarmListener\";\nimport { cancelRecording, handleDismiss } from \"../recording/cancelRecording\";\nimport { handleDismissRecordingTab } from \"../recording/discardRecording\";\nimport { sendMessageRecord } from \"../recording/sendMessageRecord\";\nimport { offscreenDocument } from \"../offscreen/offscreenDocument\";\nimport { forceProcessing } from \"../recording/forceProcessing\";\nimport {\n  restartActiveTab,\n  getCurrentTab,\n  sendMessageTab,\n  parseEditorTargetUrl,\n  resolveEditorTabForTarget,\n} from \"../tabManagement\";\nimport {\n  handleRestart,\n} from \"../recording/restartRecording\";\nimport { checkRecording } from \"../recording/checkRecording\";\nimport {\n  isPinned,\n  getPlatformInfo,\n  resizeWindow,\n  checkAvailableMemory,\n} from \"../utils/browserHelpers\";\nimport { requestDownload, downloadIndexedDB } from \"../utils/downloadHelpers\";\nimport { restoreRecording, checkRestore } from \"../recording/restoreRecording\";\nimport {\n  checkCloudRestore,\n  restoreCloudRecording,\n} from \"../recording/restoreCloudRecording\";\nimport {\n  CLOUD_LOCAL_PLAYBACK_KEY,\n  CLOUD_LOCAL_PLAYBACK_EVENT_KEY,\n  CLOUD_LOCAL_PLAYBACK_ALARM,\n} from \"../recording/cloudLocalPlaybackConstants\";\nimport { desktopCapture } from \"../recording/desktopCapture\";\nimport {\n  writeFile,\n  videoReady,\n  handleGetStreamingData,\n  handleRecordingError,\n  handleRecordingComplete,\n  handleOnGetPermissions,\n  handlePip,\n  checkCapturePermissions,\n} from \"../recording/recordingHelpers\";\nimport { newChunk, clearAllRecordings } from \"../recording/chunkHandler\";\nimport { setMicActiveTab } from \"../tabManagement/tabHelpers\";\nimport { handleSignOutDrive } from \"../drive/handleSignOutDrive\";\nimport { loginWithWebsite } from \"../auth/loginWithWebsite\";\nimport {\n  getDiagnosticLog,\n  getErrorSnapshot,\n  getStorageFlags,\n  diagEvent,\n} from \"../../utils/diagnosticLog\";\nimport { supportContextQuery } from \"../../utils/buildSupportContext\";\n\nconst API_BASE = process.env.SCREENITY_API_BASE_URL;\nconst APP_BASE = process.env.SCREENITY_APP_BASE;\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n// Debug toggle for post-stop/chunk flow\nconst DEBUG_POSTSTOP = false;\nconst STOP_RECORDING_TAB_DEBOUNCE_MS = 1200;\nconst CLOUD_LOCAL_PLAYBACK_MAX_BYTES = 250 * 1024 * 1024;\nconst CLOUD_LOCAL_PLAYBACK_MAX_CHUNKS = 4000;\nconst CLOUD_LOCAL_PLAYBACK_MIN_TTL_MS = 60 * 1000;\nconst CLOUD_LOCAL_PLAYBACK_MAX_TTL_MS = 24 * 60 * 60 * 1000;\nlet stopRecordingTabInFlight = false;\nlet stopRecordingTabLastAt = 0;\n\nconst getEditorTargetUrl = ({ projectId, instantMode = false } = {}) => {\n  if (!projectId) return null;\n  if (instantMode) {\n    return `${APP_BASE}/view/${projectId}?load=true`;\n  }\n  return `${APP_BASE}/editor/${projectId}/edit?load=true`;\n};\n\nconst clamp = (value, min, max) => Math.max(min, Math.min(max, value));\n\nconst normalizeLocalPlaybackOffer = (offer = {}) => {\n  const now = Date.now();\n  const expiresAtRaw = Number(offer.expiresAt) || 0;\n  const ttl =\n    expiresAtRaw > now\n      ? clamp(\n          expiresAtRaw - now,\n          CLOUD_LOCAL_PLAYBACK_MIN_TTL_MS,\n          CLOUD_LOCAL_PLAYBACK_MAX_TTL_MS,\n        )\n      : CLOUD_LOCAL_PLAYBACK_MIN_TTL_MS;\n  const expiresAt = now + ttl;\n  const chunkCount = Math.max(\n    0,\n    Math.min(CLOUD_LOCAL_PLAYBACK_MAX_CHUNKS, Number(offer.chunkCount) || 0),\n  );\n  const estimatedBytes = Math.max(0, Number(offer.estimatedBytes) || 0);\n  const createdAt = Number(offer.createdAt) || now;\n\n  return {\n    offerId: offer.offerId || crypto.randomUUID(),\n    projectId: offer.projectId || null,\n    sceneId: offer.sceneId || null,\n    recordingSessionId: offer.recordingSessionId || null,\n    trackType: \"screen\",\n    source: offer.source || \"indexeddb-screen-chunks\",\n    status: offer.status || \"available\",\n    chunkCount,\n    estimatedBytes,\n    mediaId: offer.mediaId || null,\n    bunnyVideoId: offer.bunnyVideoId || null,\n    createdAt,\n    expiresAt,\n    updatedAt: now,\n  };\n};\n\nconst isLocalPlaybackOfferExpired = (offer) =>\n  !offer || Number(offer.expiresAt || 0) <= Date.now();\n\nconst scheduleLocalPlaybackAlarm = async (offer) => {\n  if (!offer?.expiresAt || !chrome.alarms?.create) return;\n  try {\n    await chrome.alarms.clear(CLOUD_LOCAL_PLAYBACK_ALARM);\n    await chrome.alarms.create(CLOUD_LOCAL_PLAYBACK_ALARM, {\n      when: Number(offer.expiresAt),\n    });\n  } catch (err) {\n    console.warn(\"[Screenity][BG] Failed to schedule local playback alarm\", err);\n  }\n};\n\nconst getStoredLocalPlaybackOffer = async () => {\n  const result = await chrome.storage.local.get([CLOUD_LOCAL_PLAYBACK_KEY]);\n  return result?.[CLOUD_LOCAL_PLAYBACK_KEY] || null;\n};\n\nconst clearStoredLocalPlaybackOffer = async ({\n  reason = \"unknown\",\n  clearChunks = true,\n  onlyIfOfferId = null,\n} = {}) => {\n  const existing = await getStoredLocalPlaybackOffer();\n  if (onlyIfOfferId && existing?.offerId && existing.offerId !== onlyIfOfferId) {\n    return { ok: true, skipped: true, reason: \"offer-id-mismatch\" };\n  }\n\n  await chrome.storage.local.remove([CLOUD_LOCAL_PLAYBACK_KEY]);\n  if (chrome.alarms?.clear) {\n    await chrome.alarms.clear(CLOUD_LOCAL_PLAYBACK_ALARM).catch(() => {});\n  }\n\n  if (clearChunks) {\n    await chunksStore.clear().catch((err) => {\n      console.warn(\n        \"[Screenity][BG] Failed to clear chunksStore while clearing local playback offer\",\n        err,\n      );\n    });\n  }\n\n  await chrome.storage.local.set({\n    [CLOUD_LOCAL_PLAYBACK_EVENT_KEY]: {\n      event: \"offer-cleared\",\n      reason,\n      clearedAt: Date.now(),\n      clearedOfferId: existing?.offerId || null,\n      clearChunks: Boolean(clearChunks),\n    },\n  });\n\n  if (existing?.offerId) {\n    console.info(\"[Screenity][BG] Cleared local screen playback offer\", {\n      reason,\n      offerId: existing.offerId,\n      clearChunks: Boolean(clearChunks),\n    });\n  }\n\n  return { ok: true, clearedOfferId: existing?.offerId || null };\n};\n\nconst getValidLocalPlaybackOffer = async ({\n  offerId = null,\n  projectId = null,\n  sceneId = null,\n} = {}) => {\n  const offer = await getStoredLocalPlaybackOffer();\n  if (!offer) return null;\n\n  if (isLocalPlaybackOfferExpired(offer)) {\n    await clearStoredLocalPlaybackOffer({\n      reason: \"offer-expired\",\n      clearChunks: true,\n      onlyIfOfferId: offer.offerId || null,\n    });\n    return null;\n  }\n\n  if (offerId && offer.offerId !== offerId) return null;\n  if (projectId && offer.projectId !== projectId) return null;\n  if (sceneId && offer.sceneId && offer.sceneId !== sceneId) return null;\n  if (offer.trackType !== \"screen\") return null;\n  if (!offer.chunkCount || !offer.estimatedBytes) return null;\n\n  return offer;\n};\n\nconst ensureAudioOffscreen = async () => {\n  if (!chrome.offscreen) return false;\n  try {\n    const contexts = await chrome.runtime.getContexts({});\n    const hasAnyOffscreen = contexts.some(\n      (context) => context.contextType === \"OFFSCREEN_DOCUMENT\",\n    );\n    // If an offscreen document already exists (e.g. the recorder), reuse it\n    // — Chrome only allows one offscreen document per extension.\n    if (hasAnyOffscreen) return true;\n    await chrome.offscreen.createDocument({\n      url: \"audiooffscreen.html\",\n      reasons: [\"AUDIO_PLAYBACK\"],\n      justification: \"Play short UI beep sounds.\",\n    });\n    return true;\n  } catch (error) {\n    console.warn(\"Failed to ensure audio offscreen document\", error);\n    return false;\n  }\n};\n\nconst logStopRecordingTabEvent = (message, sender) => {\n  try {\n    const reason = message?.reason || \"unknown\";\n    const senderTabId = message?.tabId || sender?.tab?.id || null;\n    const senderUrl = sender?.url || null;\n    const stack = new Error().stack;\n    console.warn(\"[Screenity][BG] stop-recording-tab received\", {\n      reason,\n      senderTabId,\n      senderUrl,\n    });\n    chrome.storage.local.set({\n      lastStopRecordingEvent: {\n        reason,\n        senderTabId,\n        senderUrl,\n        stack,\n        ts: Date.now(),\n      },\n    });\n  } catch (err) {\n    console.warn(\"[Screenity][BG] stop-recording-tab logging failed\", err);\n  }\n};\n\nconst setTabAutoDiscardableSafe = async (message, sender) => {\n  try {\n    const tabId = sender?.tab?.id;\n    const discardable = message?.discardable;\n\n    if (!tabId || typeof discardable !== \"boolean\") return;\n\n    await chrome.tabs.update(tabId, { autoDiscardable: discardable });\n  } catch (err) {\n    console.warn(\"Failed to set tab autoDiscardable:\", err);\n  }\n};\n\nconst handleCreateVideoProject = async (message) => {\n  try {\n    const res = await fetch(`${API_BASE}/videos/create`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: `Bearer ${await chrome.storage.local\n          .get(\"screenityToken\")\n          .then((r) => r.screenityToken)}`,\n      },\n      body: JSON.stringify({\n        title: message.title || \"Untitled Recording\",\n        data: message.data || {},\n        instantMode: message.instantMode || false,\n        recording: true,\n        isPublic: message.instantMode ? true : false,\n      }),\n    });\n\n    const result = await res.json();\n\n    if (!res.ok || !result?.videoId) {\n      return {\n        success: false,\n        error: result?.error || \"Server error\",\n      };\n    }\n\n    return { success: true, videoId: result.videoId };\n  } catch (err) {\n    console.error(\"❌ Failed to create video:\", err.message);\n    return { success: false, error: err.message };\n  }\n};\n\nconst handleFetchVideos = async (message) => {\n  try {\n    const page = message.page || 0;\n    const pageSize = message.pageSize || 12;\n    const sort = message.sort || \"newest\";\n    const filter = message.filter || \"all\";\n\n    const token = await chrome.storage.local\n      .get(\"screenityToken\")\n      .then((r) => r.screenityToken);\n\n    const res = await fetch(\n      `${API_BASE}/videos?page=${page}&pageSize=${pageSize}&sort=${sort}&filter=${filter}`,\n      {\n        method: \"GET\",\n        headers: {\n          Authorization: `Bearer ${token}`,\n        },\n        credentials: \"include\",\n      },\n    );\n\n    const result = await res.json();\n\n    if (!res.ok || !result?.videos) {\n      return {\n        success: false,\n        error: result?.error || \"Failed to fetch videos\",\n      };\n    }\n\n    return { success: true, videos: result.videos };\n  } catch (err) {\n    console.error(\"❌ Failed to fetch videos:\", err.message);\n    return { success: false, error: err.message };\n  }\n};\n\nconst handleReopenPopupMulti = async () => {\n  try {\n    const tab = await getCurrentTab();\n    if (!tab?.id) {\n      console.warn(\"No active tab found for popup reopen\");\n      return;\n    }\n\n    await sendMessageTab(tab.id, {\n      type: \"reopen-popup-multi\",\n    });\n  } catch (err) {\n    console.warn(\"Failed to send popup reopen message:\", err);\n  }\n};\n\nconst handleCheckStorageQuota = async (retried = false) => {\n  try {\n    const { screenityToken } = await chrome.storage.local.get(\"screenityToken\");\n\n    const res = await fetch(`${API_BASE}/storage/quota`, {\n      method: \"GET\",\n      headers: {\n        Authorization: `Bearer ${screenityToken}`,\n      },\n      credentials: \"include\",\n    });\n\n    // On 401, invalidate auth cache and retry once so loginWithWebsite()\n    // in the outer handler can refresh the token on the next attempt.\n    if (res.status === 401 && !retried) {\n      await chrome.storage.local.set({ lastAuthCheck: 0 });\n      const refresh = await loginWithWebsite();\n      if (refresh.authenticated) {\n        return handleCheckStorageQuota(true);\n      }\n      return { success: false, error: \"Not authenticated\" };\n    }\n\n    const result = await res.json();\n\n    if (!res.ok) {\n      return {\n        success: false,\n        error: result?.error || \"Fetch failed\",\n      };\n    }\n\n    return { success: true, ...result };\n  } catch (err) {\n    console.error(\"❌ Error checking storage quota:\", err);\n    return { success: false, error: err.message };\n  }\n};\n\nconst handleFinishMultiRecording = async () => {\n  try {\n    const { recordingToScene } = await chrome.storage.local.get([\n      \"recordingToScene\",\n    ]);\n\n    if (!recordingToScene) {\n      const { multiProjectId } = await chrome.storage.local.get([\n        \"multiProjectId\",\n      ]);\n\n      if (!multiProjectId) {\n        console.warn(\"No project ID found for finishing multi recording.\");\n        return;\n      }\n\n      const res = await fetch(\n        `${API_BASE}/videos/${multiProjectId}/auto-publish`,\n        {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            Authorization: `Bearer ${await chrome.storage.local\n              .get(\"screenityToken\")\n              .then((r) => r.screenityToken)}`,\n          },\n        },\n      );\n\n      const url = `${process.env.SCREENITY_APP_BASE}/editor/${multiProjectId}/edit?share=true`;\n      const publicUrl = `${process.env.SCREENITY_APP_BASE}/view/${multiProjectId}/`;\n\n      if (!res.ok) {\n        console.warn(\"Failed to auto-publish multi recording\", res.status);\n        return;\n      }\n\n      // Open the editor directly\n      createTab(url, true, true).then(() => {\n        if (publicUrl) {\n          copyToClipboard(publicUrl);\n          chrome.runtime.sendMessage({\n            type: \"show-toast\",\n            message: \"Public video link copied to clipboard!\",\n          });\n        }\n      });\n    } else {\n      // Multi recording on existing project: only reuse editorTab if it still\n      // matches this project and expected editor/view URL.\n      const { projectId, instantMode } = await chrome.storage.local.get([\n        \"projectId\",\n        \"instantMode\",\n      ]);\n      const targetUrl = getEditorTargetUrl({\n        projectId,\n        instantMode: Boolean(instantMode),\n      });\n      const expectedKind = instantMode ? \"view\" : \"editor\";\n      const resolved = await resolveEditorTabForTarget({\n        targetUrl,\n        expectedProjectId: projectId || null,\n        expectedKind,\n        reason: \"finish-multi-recording\",\n      });\n      const messageTab = resolved.tabId || (await getCurrentTab())?.id || null;\n\n      if (messageTab) {\n        await focusTab(messageTab, { reason: \"finish-multi-recording:notify\" });\n        await sendMessageTab(messageTab, {\n          type: \"update-project-ready\",\n          share: false,\n          newProject: false,\n          projectId: projectId || null,\n        }).catch((err) =>\n          console.warn(\n            \"[Screenity][BG] Failed to send update-project-ready (finish-multi-recording)\",\n            err,\n          ),\n        );\n      } else {\n        console.warn(\n          \"[Screenity][BG] No tab available for update-project-ready (finish-multi-recording)\",\n          { projectId, instantMode: Boolean(instantMode) },\n        );\n      }\n\n      chrome.storage.local.set({\n        recordingProjectTitle: \"\",\n        projectId: null,\n        activeSceneId: null,\n        recordingToScene: false,\n        multiMode: false,\n        multiProjectId: null,\n        editorTab: null,\n        editorTabMeta: null,\n      });\n\n      const tab = await getCurrentTab();\n      if (tab?.id) {\n        sendMessageTab(tab.id, {\n          type: \"clear-recordings\",\n        });\n      }\n    }\n\n    // Reset multi-mode state\n    await chrome.storage.local.set({\n      multiMode: false,\n      multiSceneCount: 0,\n      multiProjectId: null,\n    });\n  } catch (err) {\n    console.warn(\"Failed to finish multi recording\", err);\n  }\n};\n\nlet activeRecordingSession = null;\nlet recordingTabListener = null;\nlet desktopCaptureInFlight = false;\nlet lastDesktopCaptureAt = 0;\n\nconst clearRecordingSession = () => {\n  activeRecordingSession = null;\n  if (recordingTabListener) {\n    chrome.tabs.onRemoved.removeListener(recordingTabListener);\n    recordingTabListener = null;\n  }\n};\n\nconst clearRecordingSessionSafe = async (reason = \"unknown\", details = {}) => {\n  const prev = activeRecordingSession;\n  clearRecordingSession();\n  try {\n    await chrome.storage.local.set({\n      lastRecordingSessionClear: {\n        ts: Date.now(),\n        reason,\n        previousSessionId: prev?.id || null,\n        previousRecorderTabId: prev?.recorderTabId || prev?.tabId || null,\n        ...details,\n      },\n    });\n  } catch {}\n};\n\nconst registerRecordingTabListener = (ownerTabId) => {\n  if (!ownerTabId) return;\n  if (recordingTabListener) {\n    chrome.tabs.onRemoved.removeListener(recordingTabListener);\n    recordingTabListener = null;\n  }\n  recordingTabListener = (closedTabId) => {\n    if (closedTabId === ownerTabId) {\n      chrome.runtime.sendMessage({\n        type: \"stop-recording-tab\",\n        reason: \"recorder-owner-tab-closed\",\n        tabId: closedTabId,\n      });\n      clearRecordingSessionSafe(\"owner-tab-removed\", { closedTabId });\n    }\n  };\n  chrome.tabs.onRemoved.addListener(recordingTabListener);\n};\n\nconst isSessionRecording = (session) => session?.status === \"recording\";\n\nconst doesTabExist = async (tabId) => {\n  if (!Number.isInteger(tabId)) return false;\n  try {\n    await chrome.tabs.get(tabId);\n    return true;\n  } catch {\n    return false;\n  }\n};\n\nconst normalizeIncomingSession = (incoming = {}, sender) => {\n  const ownerTabId = incoming.recorderTabId || sender?.tab?.id || null;\n  const capturedTabId = incoming.capturedTabId || incoming.tabId || null;\n  return {\n    ...incoming,\n    recorderTabId: ownerTabId,\n    capturedTabId,\n    // Keep tabId for backward compatibility.\n    tabId: capturedTabId,\n  };\n};\n\nconst isActiveSessionAlive = async (session) => {\n  if (!session?.id) return false;\n  const ownerTabId = session.recorderTabId || session.tabId || null;\n  const ownerTabAlive = await doesTabExist(ownerTabId);\n  const {\n    recording,\n    pendingRecording,\n    restarting,\n    recorderSession: storedSession,\n  } = await chrome.storage.local.get([\n    \"recording\",\n    \"pendingRecording\",\n    \"restarting\",\n    \"recorderSession\",\n  ]);\n  const flagsActive = Boolean(recording || pendingRecording || restarting);\n  const storedMatches =\n    storedSession?.id === session.id && isSessionRecording(storedSession);\n  return ownerTabAlive && (storedMatches || flagsActive);\n};\n\nconst resolveActiveSessionConflict = async (incomingSession) => {\n  if (!incomingSession?.id) {\n    return { allow: true, staleRecovered: false };\n  }\n\n  if (!activeRecordingSession?.id) {\n    const { recorderSession: storedSession } = await chrome.storage.local.get([\n      \"recorderSession\",\n    ]);\n    if (storedSession?.id && isSessionRecording(storedSession)) {\n      activeRecordingSession = {\n        ...storedSession,\n        recorderTabId: storedSession.recorderTabId || storedSession.tabId || null,\n        capturedTabId:\n          storedSession.capturedTabId || storedSession.tabId || null,\n        tabId: storedSession.capturedTabId || storedSession.tabId || null,\n      };\n    }\n  }\n\n  if (!activeRecordingSession?.id) return { allow: true, staleRecovered: false };\n  if (activeRecordingSession.id === incomingSession.id) {\n    return { allow: true, staleRecovered: false };\n  }\n\n  if (!isSessionRecording(activeRecordingSession)) {\n    await clearRecordingSessionSafe(\"non-recording-session-conflict\");\n    return { allow: true, staleRecovered: true };\n  }\n\n  const alive = await isActiveSessionAlive(activeRecordingSession);\n  if (alive) {\n    console.warn(\"[Screenity][BG] session_conflict_rejected\", {\n      activeId: activeRecordingSession.id,\n      incomingId: incomingSession.id,\n      activeRecorderTabId:\n        activeRecordingSession.recorderTabId || activeRecordingSession.tabId,\n    });\n    return { allow: false, staleRecovered: false };\n  }\n\n  await clearRecordingSessionSafe(\"stale-conflict-recovered\", {\n    incomingId: incomingSession.id,\n  });\n  console.warn(\"[Screenity][BG] session_conflict_stale_recovered\", {\n    incomingId: incomingSession.id,\n  });\n  return { allow: true, staleRecovered: true };\n};\n\nexport const copyToClipboard = (text) => {\n  if (!text) return;\n  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {\n    if (!tabs.length) return;\n    const tabId = tabs[0].id;\n    chrome.scripting.executeScript({\n      target: { tabId },\n      func: (content) => {\n        navigator.clipboard.writeText(content).catch((err) => {\n          console.warn(\n            \"❌ Failed to copy to clipboard in content script:\",\n            err,\n          );\n        });\n      },\n      args: [text],\n    });\n  });\n};\n\n// Initialize message router and register all handlers\nexport const setupHandlers = () => {\n  registerMessage(\"desktop-capture\", async (message) => {\n    const now = Date.now();\n    // Some pages (notably playground) can trigger duplicate start messages.\n    // Gate starts briefly so Chrome doesn't show capture permission twice.\n    if (desktopCaptureInFlight || now - lastDesktopCaptureAt < 1200) {\n      return { ok: true, deduped: true };\n    }\n\n    desktopCaptureInFlight = true;\n    lastDesktopCaptureAt = now;\n    try {\n      await desktopCapture(message);\n      return { ok: true };\n    } finally {\n      // Keep the lock briefly to absorb closely-following duplicate dispatches.\n      setTimeout(() => {\n        desktopCaptureInFlight = false;\n      }, 1000);\n    }\n  });\n  registerMessage(\"backup-created\", (message) =>\n    offscreenDocument(message.request, message.tabId),\n  );\n  registerMessage(\"write-file\", (message) => writeFile(message));\n  registerMessage(\"handle-restart\", (message, sender) =>\n    handleRestart(message, sender),\n  );\n  registerMessage(\"handle-dismiss\", (message) => handleDismiss(message));\n  registerMessage(\"reset-active-tab\", () => resetActiveTab(false));\n  registerMessage(\"reset-active-tab-restart\", (message) =>\n    resetActiveTabRestart(message),\n  );\n  registerMessage(\"video-ready\", async (message) => {\n    await videoReady(message);\n    await clearRecordingSessionSafe(\"video-ready\");\n  });\n\n  // Fired by Region/Recorder.jsx pagehide — the iframe is being torn down\n  // because the user navigated away from the recorded tab.\n  registerMessage(\"region-iframe-destroyed\", async () => {\n    const { recording, recorderSession, customRegion, recordingType } =\n      await chrome.storage.local.get([\n        \"recording\",\n        \"recorderSession\",\n        \"customRegion\",\n        \"recordingType\",\n      ]);\n    const isActivelyRecording =\n      recording ||\n      (recorderSession && recorderSession.status === \"recording\");\n    const isRegionRecording = customRegion || recordingType === \"region\";\n    if (!isActivelyRecording || !isRegionRecording) return;\n\n    diagEvent(\"region-iframe-destroyed\");\n    await chrome.storage.local.set({\n      recording: false,\n      customRegion: false,\n      // Clear recordingTab so stopRecording() doesn't think the editor was\n      // already opened by the stop-recording-tab flow (recordingTab points\n      // to the pinned recorder.html tab which didn't open any editor).\n      recordingTab: null,\n      postStopEditorOpening: false,\n      postStopEditorOpened: false,\n      recorderSession: recorderSession\n        ? { ...recorderSession, status: \"stopped\" }\n        : null,\n    });\n\n    // If no chunks were persisted (user navigated almost immediately),\n    // show a toast instead of opening an empty editor.\n    const chunkCount = await chunksStore.length().catch(() => 0);\n    if (chunkCount === 0) {\n      diagEvent(\"region-nav-no-chunks\");\n      const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n      if (activeTab) {\n        sendMessageTab(activeTab, {\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"recordingTooShortToast\"),\n          timeout: 5000,\n        }).catch(() => {});\n      }\n      return;\n    }\n\n    await videoReady();\n  });\n\n  registerMessage(\"start-recording\", (message) => startRecording(message));\n  registerMessage(\"countdown-finished\", async (message) => {\n    const { recording, restarting, pendingRecording } =\n      await chrome.storage.local.get([\n      \"recording\",\n      \"restarting\",\n      \"pendingRecording\",\n    ]);\n    // pendingRecording/restarting are expected transitional flags while countdown\n    // runs. During restart, `recording` may still be true briefly from the\n    // previous session, so only block when recording is active AND not restarting.\n    if (recording && !restarting) {\n      diagEvent(\"countdown-finished\", { skipped: true, reason: \"already-recording\" });\n      const decisionAt = Date.now();\n      await chrome.storage.local.set({\n        lastCountdownFinishedDecision: {\n          ts: decisionAt,\n          startedAt: null,\n          endedAt: message?.endedAt || null,\n          acceptedCountdownFinishedAt: false,\n          recording: Boolean(recording),\n          restarting: Boolean(restarting),\n          pendingRecording: Boolean(pendingRecording),\n          started: false,\n          reason: \"already-recording\",\n        },\n      });\n      return { ok: true, skipped: true };\n    }\n    diagEvent(\"countdown-finished\", { skipped: false });\n    const decisionAt = Date.now();\n    await chrome.storage.local.set({\n      countdownFinishedAt: message?.endedAt || decisionAt,\n      lastCountdownFinishedDecision: {\n        ts: decisionAt,\n        startedAt: decisionAt,\n        endedAt: message?.endedAt || null,\n        acceptedCountdownFinishedAt: true,\n        recording: Boolean(recording),\n        restarting: Boolean(restarting),\n        pendingRecording: Boolean(pendingRecording),\n        started: true,\n      },\n    });\n    startAfterCountdown();\n    return { ok: true };\n  });\n  registerMessage(\"restarted\", (message) => restartActiveTab(message));\n  const sendChunksToSandbox = async (sender) => {\n    if (DEBUG_POSTSTOP)\n      console.debug(\"[Screenity][BG] sendChunksToSandbox invoked\", {\n        senderTab: sender?.tab?.id,\n      });\n\n    const { sandboxTab } = await chrome.storage.local.get([\"sandboxTab\"]);\n    // Prefer stored sandboxTab but fall back to the caller tab if available\n    const targetTab = sandboxTab || sender?.tab?.id || null;\n    if (!targetTab) {\n      if (DEBUG_POSTSTOP)\n        console.warn(\"[Screenity][BG] no targetTab for sendChunksToSandbox\");\n      throw new Error(\"no-sandbox-tab\");\n    }\n\n    const pingReady = async () => {\n      return new Promise((resolve) => {\n        chrome.runtime.sendMessage(\n          { type: \"ping\", _targetTabId: targetTab },\n          (response) => {\n            resolve(response?.status === \"ready\");\n          },\n        );\n      });\n    };\n\n    const maxPingAttempts = 10;\n    let pingOk = false;\n    for (let attempt = 1; attempt <= maxPingAttempts; attempt += 1) {\n      // eslint-disable-next-line no-await-in-loop\n      pingOk = await pingReady();\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][BG] ping attempt\", { attempt, pingOk });\n      if (pingOk) break;\n      // eslint-disable-next-line no-await-in-loop\n      await new Promise((r) => setTimeout(r, 200));\n    }\n\n    if (!pingOk) {\n      if (DEBUG_POSTSTOP)\n        console.warn(\n          \"[Screenity][BG] sandbox not ready after pings, proceeding anyway\",\n          { targetTab },\n        );\n      // Proceed even if ping failed — runtime message ports can be unreliable\n      // during page load; we'll attempt to send chunks regardless.\n    }\n\n    const maxAttempts = 6;\n    const delayMs = 250;\n    let chunkCount = 0;\n    for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n      chunkCount = 0;\n      await chunksStore.iterate(() => {\n        chunkCount += 1;\n      });\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][BG] checking chunks in IndexedDB\", {\n          attempt,\n          chunkCount,\n        });\n      if (chunkCount > 0) break;\n      await new Promise((r) => setTimeout(r, delayMs));\n    }\n\n    let result = null;\n    const maxDeliveryAttempts = 6;\n    for (\n      let deliveryAttempt = 1;\n      deliveryAttempt <= maxDeliveryAttempts;\n      deliveryAttempt += 1\n    ) {\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][BG] calling sendChunks() to deliver\", {\n          targetTab,\n          chunkCount,\n          deliveryAttempt,\n        });\n      // eslint-disable-next-line no-await-in-loop\n      result = await sendChunks(false, {\n        tabId: targetTab,\n        frameId: sender?.frameId,\n      });\n      if (result?.status === \"ok\") {\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][BG] sendChunks() completed\", result);\n        return { status: \"ok\", chunkCount: result.chunkCount };\n      }\n      // eslint-disable-next-line no-await-in-loop\n      await new Promise((r) => setTimeout(r, 1000));\n    }\n\n    if (DEBUG_POSTSTOP)\n      console.warn(\"[Screenity][BG] sendChunks() did not find chunks\", {\n        targetTab,\n        result,\n      });\n    return { status: \"empty\", chunkCount: 0 };\n  };\n\n  registerMessage(\"send-chunks-to-sandbox\", (message, sender) =>\n    sendChunksToSandbox(sender),\n  );\n\n  registerMessage(\"new-chunk\", (message, sender, sendResponse) => {\n    newChunk(message, sendResponse);\n    return true;\n  });\n\n  registerMessage(\n    \"get-streaming-data\",\n    async (message, sender) => await handleGetStreamingData(message, sender),\n  );\n  registerMessage(\"cancel-recording\", (message) => cancelRecording(message));\n  registerMessage(\"stop-recording-tab\", (message, sender, sendResponse) => {\n    logStopRecordingTabEvent(message, sender);\n    const now = Date.now();\n    if (\n      stopRecordingTabInFlight ||\n      now - stopRecordingTabLastAt < STOP_RECORDING_TAB_DEBOUNCE_MS\n    ) {\n      if (DEBUG_POSTSTOP) {\n        console.warn(\n          \"[Screenity][BG] Suppressed duplicate stop-recording-tab message\",\n          {\n            inFlight: stopRecordingTabInFlight,\n            deltaMs: now - stopRecordingTabLastAt,\n            reason: message?.reason || null,\n          },\n        );\n      }\n      sendResponse({ ok: true, deduped: true });\n      return true;\n    }\n\n    stopRecordingTabInFlight = true;\n    stopRecordingTabLastAt = now;\n    Promise.resolve(handleStopRecordingTab(message))\n      .catch((err) => {\n        console.error(\"Failed to handle stop-recording-tab\", err);\n      })\n      .finally(() => {\n        stopRecordingTabInFlight = false;\n        stopRecordingTabLastAt = Date.now();\n      });\n    sendResponse({ ok: true });\n    return true;\n  });\n  registerMessage(\"dismiss-recording-tab\", (message) =>\n    handleDismissRecordingTab(message),\n  );\n  registerMessage(\"pause-recording-tab\", () => {\n    diagEvent(\"pause\");\n    return sendMessageRecord({ type: \"pause-recording-tab\" });\n  });\n  registerMessage(\"resume-recording-tab\", () => {\n    diagEvent(\"resume\");\n    return sendMessageRecord({ type: \"resume-recording-tab\" });\n  });\n  registerMessage(\"set-mic-active-tab\", (message) => setMicActiveTab(message));\n\n  // Diagnostic events routed from content scripts / sandbox\n  registerMessage(\"diag-countdown-started\", () => diagEvent(\"countdown-started\"));\n  registerMessage(\"diag-countdown-cancelled\", () => diagEvent(\"countdown-cancelled\"));\n  registerMessage(\"diag-editor-ready\", (message) =>\n    diagEvent(\"editor-load-ready\", { path: message?.path || null }),\n  );\n  registerMessage(\"open-editor-recovery\", async () => {\n    const { editorRecoveryUrl } = await chrome.storage.local.get([\"editorRecoveryUrl\"]);\n    if (!editorRecoveryUrl) return;\n    chrome.storage.local.remove([\"editorRecoveryUrl\", \"editorRecoveryAt\"]);\n    chrome.tabs.create({ url: editorRecoveryUrl, active: true });\n  });\n  registerMessage(\"recording-error\", async (message) => {\n    await handleRecordingError(message);\n    await clearRecordingSessionSafe(\"recording-error\", {\n      error: message?.error || null,\n    });\n  });\n  registerMessage(\"on-get-permissions\", (message) =>\n    handleOnGetPermissions(message),\n  );\n  registerMessage(\n    \"recording-complete\",\n    async (message, sender) => await handleRecordingComplete(message, sender),\n  );\n  registerMessage(\"check-recording\", (message) => checkRecording(message));\n  registerMessage(\"open-download-mp4\", async () => {\n    // If cloud features are enabled and the user is signed in, block the\n    // local \"fast MP4\" recovery flow to avoid diverging from the pro/server\n    // workflow. Show a toast instead.\n    if (CLOUD_FEATURES_ENABLED) {\n      try {\n        const { authenticated } = await loginWithWebsite();\n        if (authenticated) {\n          const tab = await getCurrentTab();\n          if (tab?.id) {\n            await sendMessageTab(tab.id, {\n              type: \"show-toast\",\n              message:\n                \"Fast MP4 download is unavailable while signed in. Use the editor or download WEBM instead.\",\n            }).catch(() => {});\n          }\n          return;\n        }\n      } catch (err) {\n        console.warn(\"Failed to check auth for open-download-mp4\", err);\n      }\n    }\n\n    const tab = await createTab(\"download.html\", true, true);\n    if (!tab?.id) return;\n    chrome.tabs.onUpdated.addListener(function listener(tabId, info) {\n      if (info.status === \"complete\" && tabId === tab.id) {\n        chrome.tabs.onUpdated.removeListener(listener);\n        sendMessageTab(tab.id, { type: \"recover-indexed-db-mp4\" });\n      }\n    });\n  });\n\n  registerMessage(\"review-screenity\", () =>\n    createTab(\n      \"https://chrome.google.com/webstore/detail/screenity-screen-recorder/kbbdabhdfibnancpjfhlkhafgdilcnji/reviews\",\n      false,\n      true,\n    ),\n  );\n  registerMessage(\"follow-twitter\", () =>\n    createTab(\"https://alyssax.substack.com/\", false, true),\n  );\n  registerMessage(\"pricing\", () =>\n    createTab(\"https://screenity.io/pro\", false, true),\n  );\n  registerMessage(\"open-processing-info\", () =>\n    createTab(\n      \"https://help.screenity.io/editing-and-exporting/dJRFpGq56JFKC7k8zEvsqb/why-is-there-a-5-minute-limit-for-editing/ddy4e4TpbnrFJ8VoRT37tQ\",\n      true,\n      true,\n    ),\n  );\n  registerMessage(\"upgrade-info\", () =>\n    createTab(\n      \"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-the-technical-requirements-for-using-screenity/6kdB6qru6naVD8ZLFvX3m9\",\n      true,\n      true,\n    ),\n  );\n  registerMessage(\"trim-info\", () =>\n    createTab(\n      \"https://help.screenity.io/editing-and-exporting/dJRFpGq56JFKC7k8zEvsqb/how-to-cut-trim-or-mute-parts-of-your-video/svNbM7YHYY717MuSWXrKXH\",\n      true,\n      true,\n    ),\n  );\n  registerMessage(\"join-waitlist\", () =>\n    createTab(\"https://tally.so/r/npojNV\", true, true),\n  );\n  registerMessage(\"chrome-update-info\", () =>\n    createTab(\n      \"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-the-technical-requirements-for-using-screenity/6kdB6qru6naVD8ZLFvX3m9\",\n      true,\n      true,\n    ),\n  );\n  registerMessage(\"set-surface\", (message) => setSurface(message));\n  registerMessage(\"pip-ended\", () => handlePip(false));\n  registerMessage(\"pip-started\", () => handlePip(true));\n  registerMessage(\"sign-out-drive\", (message) => handleSignOutDrive(message));\n  registerMessage(\"open-help\", () =>\n    createTab(\"https://help.screenity.io/\", true, true),\n  );\n  registerMessage(\"memory-limit-help\", () =>\n    createTab(\n      \"https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/what-does-%E2%80%9Cmemory-limit-reached%E2%80%9D-mean-when-recording/8WkwHbt3puuXunYqQnyPcb\",\n      true,\n      true,\n    ),\n  );\n  registerMessage(\"open-home\", () =>\n    createTab(\"https://screenity.io/\", false, true),\n  );\n  registerMessage(\"report-bug\", async () => {\n    const qs = await supportContextQuery({\n      includeRecordingState: true,\n      source: \"settings\",\n    });\n    createTab(`https://tally.so/r/3ElpXq?${qs}`, false, true);\n  });\n  registerMessage(\"report-error\", async (message) => {\n    const errorCode = message?.errorCode || null;\n    const errorWhy = message?.errorWhy || null;\n    const source = message?.source || \"error-modal\";\n\n    // Check auth for form routing\n    let user = null;\n    let isLoggedIn = false;\n    if (CLOUD_FEATURES_ENABLED) {\n      try {\n        const auth = await loginWithWebsite();\n        if (auth.authenticated && auth.user) {\n          user = auth.user;\n          isLoggedIn = true;\n        }\n      } catch {}\n    }\n\n    const qs = await supportContextQuery({\n      includeRecordingState: true,\n      source,\n      errorCode,\n      errorWhy,\n      user: isLoggedIn ? { name: user.name, email: user.email } : undefined,\n    });\n\n    if (isLoggedIn) {\n      createTab(`https://tally.so/r/310MNg?extension=true&${qs}`, false, true);\n    } else {\n      createTab(`https://tally.so/r/3ElpXq?feedbackType=Bug&${qs}`, false, true);\n    }\n  });\n  registerMessage(\"clear-recordings\", () => clearAllRecordings());\n  registerMessage(\"force-processing\", (message) => forceProcessing(message));\n  registerMessage(\"focus-this-tab\", (message, sender) =>\n    focusTab(sender.tab.id),\n  );\n  registerMessage(\"stop-recording-tab-backup\", (message) =>\n    handleStopRecordingTabBackup(message),\n  );\n  registerMessage(\"indexed-db-download\", (message) =>\n    downloadIndexedDB(message),\n  );\n  registerMessage(\"get-platform-info\", async () => await getPlatformInfo());\n  registerMessage(\n    \"get-diagnostic-log\",\n    async (_message, _sender, sendResponse) => {\n      const log = await getDiagnosticLog();\n      const errors = await getErrorSnapshot();\n      const flags = await getStorageFlags();\n      sendResponse({ log, errors, flags });\n      return true;\n    },\n  );\n  registerMessage(\"submit-diagnostic-report\", async (message) => {\n    try {\n      const appBase = process.env.SCREENITY_APP_BASE;\n      if (!appBase) return;\n      const { startFlowTrace, screenityToken, projectId, recorderSession } =\n        await chrome.storage.local.get([\n          \"startFlowTrace\",\n          \"screenityToken\",\n          \"projectId\",\n          \"recorderSession\",\n        ]);\n      if (!startFlowTrace || !screenityToken) return;\n      const trigger = message?.trigger || \"manual\";\n      const isSuccess = trigger === \"success-summary\";\n      const trace = startFlowTrace;\n      const ua = navigator.userAgent || \"\";\n      const payload = {\n        attemptId: trace.attemptId,\n        projectId: projectId || null,\n        recordingSessionId: recorderSession?.id || null,\n        extVersion: chrome.runtime.getManifest().version,\n        trigger,\n        env: {\n          os: (await chrome.runtime.getPlatformInfo()).os || null,\n          browser: (ua.match(/Chrome\\/\\d+/) || [\"\"])[0] || null,\n        },\n        trace: isSuccess\n          ? {\n              recordingType: trace.recordingType,\n              surface: trace.surface,\n              isPro: trace.isPro,\n              countdown: trace.countdown,\n              outcome: trace.outcome,\n              t: {\n                startStreaming: trace.t?.startStreaming || null,\n                recordingStarted: trace.t?.recordingStarted || null,\n              },\n              routing: null,\n              error: null,\n              errorCode: null,\n              stuck: null,\n            }\n          : trace,\n      };\n      fetch(`${appBase}/api/log/diagnostic-report`, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${screenityToken}`,\n        },\n        body: JSON.stringify(payload),\n        keepalive: true,\n      }).catch(() => {});\n    } catch {\n      // best effort\n    }\n  });\n  registerMessage(\"restore-recording\", (message) => restoreRecording(message));\n  registerMessage(\"check-restore\", async (message, sender, sendResponse) => {\n    const response = await checkRestore();\n    sendResponse(response);\n    return true;\n  });\n  registerMessage(\n    \"check-cloud-restore\",\n    async (_message, _sender, sendResponse) => {\n      const response = await checkCloudRestore();\n      sendResponse(response);\n      return true;\n    },\n  );\n  registerMessage(\"restore-cloud-recording\", () => restoreCloudRecording());\n  registerMessage(\n    \"check-capture-permissions\",\n    async (message, sender, sendResponse) => {\n      const { isLoggedIn, isSubscribed } = message;\n\n      const response = await checkCapturePermissions({\n        isLoggedIn,\n        isSubscribed,\n      });\n\n      sendResponse(response);\n      return true;\n    },\n  );\n  registerMessage(\"is-pinned\", async () => await isPinned());\n\n  // Prevent Chrome from discarding the CloudRecorder tab during recording\n  registerMessage(\"set-tab-auto-discardable\", (message, sender) =>\n    setTabAutoDiscardableSafe(message, sender),\n  );\n\n  registerMessage(\n    \"save-to-drive\",\n    async (message) => await handleSaveToDrive(message, false),\n  );\n  registerMessage(\n    \"save-to-drive-fallback\",\n    async (message) => await handleSaveToDrive(message, true),\n  );\n  registerMessage(\"request-download\", (message) =>\n    requestDownload(message.base64, message.title),\n  );\n  registerMessage(\"resize-window\", (message) =>\n    resizeWindow(message.width, message.height),\n  );\n  registerMessage(\"available-memory\", async () => {\n    return await checkAvailableMemory();\n  });\n  registerMessage(\"extension-media-permissions\", () =>\n    createTab(\n      `chrome://settings/content/siteDetails?site=chrome-extension://${chrome.runtime.id}`,\n      false,\n      true,\n    ),\n  );\n  registerMessage(\"add-alarm-listener\", (payload) => addAlarmListener(payload));\n  registerMessage(\n    \"check-auth-status\",\n    async () => {\n      if (!CLOUD_FEATURES_ENABLED) {\n        return {\n          authenticated: false,\n          message: \"Cloud features disabled\",\n        };\n      }\n      return await loginWithWebsite();\n    },\n  );\n  registerMessage(\n    \"create-video-project\",\n    async (message, sender, sendResponse) => {\n      if (!CLOUD_FEATURES_ENABLED) {\n        sendResponse({ success: false, message: \"Cloud features disabled\" });\n        return true;\n      }\n      const { authenticated, subscribed, user } = await loginWithWebsite();\n\n      if (!authenticated) {\n        sendResponse({ success: false, message: \"User not authenticated\" });\n        return true;\n      }\n\n      if (!subscribed) {\n        sendResponse({ success: false, message: \"Subscription inactive\" });\n        return true;\n      }\n\n      const response = await handleCreateVideoProject(message);\n      sendResponse(response);\n\n      return true;\n    },\n  );\n  registerMessage(\"handle-login\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled, cannot handle login\");\n      return;\n    }\n    // User is explicitly initiating login — clear the stay-logged-out flag.\n    await chrome.storage.local.set({ stayLoggedOut: false });\n\n    const currentTab = await getCurrentTab();\n\n    if (currentTab?.id) {\n      await chrome.storage.local.set({ originalTabId: currentTab.id });\n    }\n    chrome.tabs.create({\n      url: `${process.env.SCREENITY_APP_BASE}/login?extension=true`,\n      active: true,\n    });\n  });\n  registerMessage(\"handle-logout\", async (message, sender, sendResponse) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      sendResponse({ success: false, message: \"Cloud features disabled\" });\n      return true;\n    }\n    await chrome.storage.local.remove([\n      \"screenityToken\",\n      \"screenityUser\",\n      \"lastAuthCheck\",\n      \"isSubscribed\",\n      \"isLoggedIn\",\n      \"proSubscription\",\n    ]);\n\n    // Preserve a post-logout marker so popup can render the LoggedOut state.\n    // stayLoggedOut blocks auto-login until the user explicitly clicks \"Log in\".\n    await chrome.storage.local.set({\n      isLoggedIn: false,\n      wasLoggedIn: true,\n      stayLoggedOut: true,\n      isSubscribed: false,\n      proSubscription: null,\n      screenityUser: null,\n    });\n\n    sendResponse({ success: true });\n    return true;\n  });\n\n  registerMessage(\"click-event\", async ({ payload }, sender) => {\n    if (!CLOUD_FEATURES_ENABLED) return;\n    const { x, y, surface, region, isTab } = payload;\n    const senderWindowId = sender.tab?.windowId;\n\n    // Ask Recorder for current video time\n    sendMessageRecord({ type: \"get-video-time\" }, (response) => {\n      const videoTime = response?.videoTime ?? null;\n\n      const baseClick = { x, y, surface, region, timestamp: videoTime };\n\n      if (region || isTab) {\n        storeClick(baseClick);\n        return;\n      }\n\n      if (surface === \"monitor\" && typeof senderWindowId === \"number\") {\n        chrome.windows.get(senderWindowId, (win) => {\n          if (!win || chrome.runtime.lastError) {\n            console.warn(\"Failed to get window for click\");\n            return;\n          }\n\n          chrome.system.display.getInfo((displays) => {\n            const monitor = displays.find(\n              (d) =>\n                win.left >= d.bounds.left &&\n                win.left < d.bounds.left + d.bounds.width &&\n                win.top >= d.bounds.top &&\n                win.top < d.bounds.top + d.bounds.height,\n            );\n\n            if (!monitor) {\n              console.warn(\"[click-event] No matching monitor found\");\n              return;\n            }\n\n            const screenX = win.left + x;\n            const screenY = win.top + y;\n            const adjX = screenX - monitor.bounds.left;\n            const adjY = screenY - monitor.bounds.top;\n\n            storeClick({ ...baseClick, x: adjX, y: adjY });\n          });\n        });\n        return;\n      }\n\n      if (surface === \"window\" && typeof senderWindowId === \"number\") {\n        chrome.windows.get(senderWindowId, (win) => {\n          if (!win || chrome.runtime.lastError) {\n            console.warn(\"Failed to get window for window click\");\n            return;\n          }\n\n          const screenX = win.left + x;\n          const screenY = win.top + y;\n\n          storeClick({ ...baseClick, x: screenX, y: screenY });\n        });\n        return;\n      }\n\n      storeClick(baseClick);\n    });\n  });\n\n  function storeClick(click) {\n    chrome.storage.local.get({ clickEvents: [] }, (data) => {\n      chrome.storage.local.set({ clickEvents: [...data.clickEvents, click] });\n    });\n  }\n\n  function getMonitorForWindow(message, sender, sendResponse) {\n    chrome.system.display.getInfo((displays) => {\n      chrome.windows.getCurrent((win) => {\n        if (!win || chrome.runtime.lastError) {\n          console.warn(\n            \"[get-monitor-for-window] No window found\",\n            chrome.runtime.lastError,\n          );\n          sendResponse({ error: \"No window found\" });\n          return;\n        }\n\n        const monitor = displays.find(\n          (d) =>\n            win.left >= d.bounds.left &&\n            win.left < d.bounds.left + d.bounds.width &&\n            win.top >= d.bounds.top &&\n            win.top < d.bounds.top + d.bounds.height,\n        );\n\n        if (!monitor) {\n          console.warn(\"[get-monitor-for-window] No matching monitor\");\n          sendResponse({ error: \"No matching monitor\" });\n        } else {\n          // Save monitor info directly into chrome.storage.local\n          chrome.storage.local.set(\n            {\n              displays,\n              recordedMonitorId: monitor.id,\n              monitorBounds: monitor.bounds,\n            },\n            () => {\n              sendResponse({\n                monitorId: monitor.id,\n                monitorBounds: monitor.bounds,\n                displays,\n              });\n            },\n          );\n        }\n      });\n    });\n\n    return true;\n  }\n\n  registerMessage(\"get-monitor-for-window\", getMonitorForWindow);\n\n  registerMessage(\"fetch-videos\", async (message, sender, sendResponse) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      sendResponse({ success: false, message: \"Cloud features disabled\" });\n      return true;\n    }\n    const { authenticated, subscribed, user } = await loginWithWebsite();\n\n    if (!authenticated) {\n      sendResponse({ success: false, message: \"User not authenticated\" });\n      return true;\n    }\n\n    if (!subscribed) {\n      sendResponse({ success: false, message: \"Subscription inactive\" });\n      return true;\n    }\n\n    const response = await handleFetchVideos(message);\n    sendResponse(response);\n\n    return true;\n  });\n  registerMessage(\"reopen-popup-multi\", async (message) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    await handleReopenPopupMulti();\n  });\n  registerMessage(\n    \"check-storage-quota\",\n    async (message, sender, sendResponse) => {\n      if (!CLOUD_FEATURES_ENABLED) {\n        sendResponse({ success: false, error: \"Cloud features disabled\" });\n        return true;\n      }\n      const authResult = await loginWithWebsite();\n      const { authenticated, subscribed } = authResult;\n\n      if (!authenticated) {\n        sendResponse({ success: false, error: \"Not authenticated\" });\n        return true;\n      }\n\n      if (!subscribed) {\n        sendResponse({ success: false, error: \"Subscription inactive\" });\n        return true;\n      }\n\n      const response = await handleCheckStorageQuota();\n      sendResponse(response);\n\n      return true;\n    },\n  );\n  registerMessage(\"time-warning\", async (message) => {\n    const tab = await getCurrentTab();\n    if (tab?.id) {\n      await sendMessageTab(tab.id, {\n        type: \"time-warning\",\n      }).catch((e) => console.warn(\"Failed to send time-warning to tab:\", e));\n    }\n  });\n  registerMessage(\"time-stopped\", async (message) => {\n    const tab = await getCurrentTab();\n    if (tab?.id) {\n      await sendMessageTab(tab.id, {\n        type: \"time-stopped\",\n      }).catch((e) => console.warn(\"Failed to send time-stopped to tab:\", e));\n    }\n  });\n  registerMessage(\"prepare-open-editor\", async (message) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    const targetUrl = message.url || null;\n    const parsedTarget = parseEditorTargetUrl(targetUrl);\n    const expectedProjectId = message.projectId || parsedTarget?.projectId || null;\n\n    await chrome.storage.local.set({\n      pendingEditorOpen: {\n        url: targetUrl,\n        publicUrl: message.publicUrl || null,\n        projectId: expectedProjectId,\n        instantMode: Boolean(message.instantMode),\n        ts: Date.now(),\n      },\n    });\n\n    console.info(\"[Screenity][BG] prepare-open-editor\", {\n      projectId: expectedProjectId,\n      targetUrl,\n      instantMode: Boolean(message.instantMode),\n      hasPublicUrl: Boolean(message.publicUrl),\n    });\n\n    const expectedKind = message.instantMode ? \"view\" : \"editor\";\n    const resolved = await resolveEditorTabForTarget({\n      targetUrl,\n      expectedProjectId: expectedProjectId,\n      expectedKind,\n      reason: \"prepare-open-editor\",\n    });\n    console.info(\"[Screenity][BG] prepare-open-editor resolved\", {\n      tabId: resolved.tabId || null,\n      reused: Boolean(resolved.reused),\n      opened: Boolean(resolved.opened),\n      projectId: expectedProjectId,\n    });\n  });\n  registerMessage(\"prepare-editor-existing\", async (message) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    let messageTab = null;\n\n    if (message.multiMode) {\n      messageTab = (await getCurrentTab())?.id || null;\n    } else {\n      const { projectId, instantMode } = await chrome.storage.local.get([\n        \"projectId\",\n        \"instantMode\",\n      ]);\n      const targetUrl = getEditorTargetUrl({\n        projectId,\n        instantMode: Boolean(instantMode),\n      });\n      const resolved = await resolveEditorTabForTarget({\n        targetUrl,\n        expectedProjectId: projectId || null,\n        expectedKind: instantMode ? \"view\" : \"editor\",\n        reason: \"prepare-editor-existing\",\n      });\n      messageTab = resolved.tabId;\n    }\n\n    if (messageTab) {\n      await sendMessageTab(messageTab, {\n        type: \"update-project-loading\",\n        multiMode: message.multiMode,\n      }).catch((err) =>\n        console.warn(\n          \"[Screenity][BG] Failed to send update-project-loading\",\n          err,\n        ),\n      );\n    } else {\n      console.warn(\"❗ No valid messageTab found in prepare-editor-existing\");\n    }\n  });\n  registerMessage(\"preparing-recording\", async () => {\n    // Prefer stored activeTab over getCurrentTab() which can return\n    // the pinned recorder tab instead of the user's page.\n    const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n    const tabId = activeTab || (await getCurrentTab())?.id;\n    if (tabId) {\n      await sendMessageTab(tabId, {\n        type: \"preparing-recording\",\n      }).catch((e) =>\n        console.warn(\"Failed to send preparing-recording to tab:\", e),\n      );\n    }\n  });\n  registerMessage(\"editor-ready\", async (message) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    const { pendingEditorOpen } = await chrome.storage.local.get([\n      \"pendingEditorOpen\",\n    ]);\n\n    let messageTab = null;\n    const projectId = message.projectId || pendingEditorOpen?.projectId || null;\n    const instantMode = Boolean(\n      message.instantMode ?? pendingEditorOpen?.instantMode,\n    );\n    const targetUrl = getEditorTargetUrl({\n      projectId,\n      instantMode,\n    });\n    const editorUrl = message.editorUrl || pendingEditorOpen?.url || targetUrl;\n    const expectedKind = instantMode ? \"view\" : \"editor\";\n    const publicUrl = message.publicUrl || pendingEditorOpen?.publicUrl || null;\n    const sceneId = message.sceneId || null;\n    const localPlaybackOffer =\n      (await getValidLocalPlaybackOffer({\n        offerId: message?.localPlayback?.offerId || null,\n        projectId: projectId || null,\n        sceneId: sceneId || null,\n      })) ||\n      null;\n\n    console.info(\"[Screenity][BG] editor-ready received\", {\n      newProject: Boolean(message.newProject),\n      multiMode: Boolean(message.multiMode),\n      projectId,\n      hasSceneId: Boolean(sceneId),\n      editorUrl,\n      hasPendingOpen: Boolean(pendingEditorOpen),\n      localPlaybackAvailable: Boolean(localPlaybackOffer?.offerId),\n      localPlaybackOfferId: localPlaybackOffer?.offerId || null,\n    });\n\n    if (message.newProject) {\n      const resolved = await resolveEditorTabForTarget({\n        targetUrl: editorUrl,\n        expectedProjectId: projectId,\n        expectedKind,\n        reason: \"editor-ready:new-project\",\n      });\n      messageTab = resolved.tabId;\n\n      chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n\n      // Copy to clipboard immediately after focusTab, before the auto-publish\n      // network request.  The auto-publish await can take hundreds of ms, during\n      // which the renderer may lose document focus — causing navigator.clipboard\n      // to throw \"Document is not focused\".  Copying here gives the best chance\n      // the tab is still the topmost focused document.\n      if (publicUrl) {\n        copyToClipboard(publicUrl);\n      }\n\n      if (projectId) {\n        await fetch(\n          `${API_BASE}/videos/${projectId}/auto-publish`,\n          {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n              Authorization: `Bearer ${await chrome.storage.local\n                .get(\"screenityToken\")\n                .then((r) => r.screenityToken)}`,\n            },\n          },\n        ).catch((err) =>\n          console.warn(\"[Screenity][BG] Failed to auto-publish project\", err),\n        );\n      }\n    } else if (message.multiMode) {\n      messageTab = (await getCurrentTab())?.id || null;\n    } else {\n      const resolved = await resolveEditorTabForTarget({\n        targetUrl: editorUrl,\n        expectedProjectId: projectId,\n        expectedKind,\n        reason: \"editor-ready:existing-project\",\n      });\n      messageTab = resolved.tabId;\n\n      chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n    }\n\n    // Copy for the non-newProject paths (scene additions are excluded because\n    // publicUrl is null there; multiMode new-project is excluded because it\n    // goes through handleFinishMultiRecording which has its own clipboard call).\n    if (publicUrl && !message.newProject) {\n      copyToClipboard(publicUrl);\n    }\n\n    if (messageTab) {\n      await sendMessageTab(messageTab, {\n        type: \"update-project-ready\",\n        share: Boolean(publicUrl),\n        newProject: Boolean(message.newProject),\n        sceneId: sceneId,\n        projectId,\n        localPlayback: localPlaybackOffer\n          ? {\n              available: true,\n              offerId: localPlaybackOffer.offerId,\n              trackType: \"screen\",\n              chunkCount: localPlaybackOffer.chunkCount,\n              estimatedBytes: localPlaybackOffer.estimatedBytes,\n              expiresAt: localPlaybackOffer.expiresAt,\n              source: localPlaybackOffer.source || \"indexeddb-screen-chunks\",\n              mediaId: localPlaybackOffer.mediaId || null,\n              bunnyVideoId: localPlaybackOffer.bunnyVideoId || null,\n            }\n          : {\n              available: false,\n              trackType: \"screen\",\n            },\n      }).catch((err) =>\n        console.warn(\"[Screenity][BG] Failed to send update-project-ready\", err),\n      );\n    } else {\n      console.warn(\"❗ No valid messageTab found in editor-ready\");\n    }\n\n    if (pendingEditorOpen) {\n      await chrome.storage.local.remove([\"pendingEditorOpen\"]);\n    }\n  });\n  registerMessage(\"cloud-local-playback-register\", async (message) => {\n    const normalizedOffer = normalizeLocalPlaybackOffer(message?.offer || {});\n    if (!normalizedOffer.projectId || !normalizedOffer.sceneId) {\n      return { ok: false, error: \"missing-project-or-scene\" };\n    }\n    if (!normalizedOffer.chunkCount || !normalizedOffer.estimatedBytes) {\n      return { ok: false, error: \"missing-local-screen-bytes\" };\n    }\n    if (normalizedOffer.estimatedBytes > CLOUD_LOCAL_PLAYBACK_MAX_BYTES) {\n      return {\n        ok: false,\n        error: \"offer-too-large\",\n        maxBytes: CLOUD_LOCAL_PLAYBACK_MAX_BYTES,\n      };\n    }\n\n    await chrome.storage.local.set({\n      [CLOUD_LOCAL_PLAYBACK_KEY]: normalizedOffer,\n      [CLOUD_LOCAL_PLAYBACK_EVENT_KEY]: {\n        event: \"offer-registered\",\n        at: Date.now(),\n        offerId: normalizedOffer.offerId,\n        projectId: normalizedOffer.projectId,\n        sceneId: normalizedOffer.sceneId,\n        chunkCount: normalizedOffer.chunkCount,\n        estimatedBytes: normalizedOffer.estimatedBytes,\n        expiresAt: normalizedOffer.expiresAt,\n      },\n    });\n    await scheduleLocalPlaybackAlarm(normalizedOffer);\n\n    console.info(\"[Screenity][BG] Registered local screen playback offer\", {\n      offerId: normalizedOffer.offerId,\n      projectId: normalizedOffer.projectId,\n      sceneId: normalizedOffer.sceneId,\n      chunkCount: normalizedOffer.chunkCount,\n      estimatedBytes: normalizedOffer.estimatedBytes,\n      expiresAt: normalizedOffer.expiresAt,\n    });\n\n    return { ok: true, offer: normalizedOffer };\n  });\n  registerMessage(\"cloud-local-playback-clear\", async (message) => {\n    const result = await clearStoredLocalPlaybackOffer({\n      reason: message?.reason || \"explicit-clear\",\n      clearChunks: message?.clearChunks !== false,\n      onlyIfOfferId: message?.offerId || null,\n    });\n    return result;\n  });\n  registerMessage(\"cloud-local-playback-get-offer\", async (message) => {\n    const offer = await getValidLocalPlaybackOffer({\n      offerId: message?.offerId || null,\n      projectId: message?.projectId || null,\n      sceneId: message?.sceneId || null,\n    });\n    if (!offer) {\n      return { ok: false, error: \"offer-unavailable\" };\n    }\n    return { ok: true, offer };\n  });\n  registerMessage(\"cloud-local-playback-read-chunk\", async (message) => {\n    const offer = await getValidLocalPlaybackOffer({\n      offerId: message?.offerId || null,\n      projectId: message?.projectId || null,\n      sceneId: message?.sceneId || null,\n    });\n    if (!offer) {\n      return { ok: false, error: \"offer-unavailable\" };\n    }\n\n    const index = Number(message?.index);\n    if (!Number.isInteger(index) || index < 0 || index >= offer.chunkCount) {\n      return { ok: false, error: \"chunk-index-out-of-range\", index };\n    }\n\n    const item = await chunksStore.getItem(`chunk_${index}`).catch(() => null);\n    if (!item?.chunk) {\n      return { ok: false, error: \"chunk-missing\", index };\n    }\n\n    const blob =\n      item.chunk instanceof Blob\n        ? item.chunk\n        : new Blob([item.chunk], { type: \"video/webm\" });\n    const arrayBuffer = await blob.arrayBuffer();\n    const base64 = btoa(\n      new Uint8Array(arrayBuffer).reduce(\n        (data, byte) => data + String.fromCharCode(byte),\n        \"\",\n      ),\n    );\n\n    return {\n      ok: true,\n      chunk: {\n        index,\n        size: blob.size,\n        mimeType: blob.type || \"video/webm\",\n        base64,\n      },\n      offer: {\n        offerId: offer.offerId,\n        expiresAt: offer.expiresAt,\n      },\n    };\n  });\n  registerMessage(\"cloud-local-playback-mark-used\", async (message) => {\n    const offer = await getValidLocalPlaybackOffer({\n      offerId: message?.offerId || null,\n      projectId: message?.projectId || null,\n      sceneId: message?.sceneId || null,\n    });\n    if (!offer) {\n      return { ok: false, error: \"offer-unavailable\" };\n    }\n    const updated = {\n      ...offer,\n      status: \"used\",\n      usedAt: Date.now(),\n      usedBy: message?.usedBy || \"editor\",\n      updatedAt: Date.now(),\n    };\n    await chrome.storage.local.set({\n      [CLOUD_LOCAL_PLAYBACK_KEY]: updated,\n      [CLOUD_LOCAL_PLAYBACK_EVENT_KEY]: {\n        event: \"offer-used\",\n        at: Date.now(),\n        offerId: updated.offerId,\n        projectId: updated.projectId,\n        sceneId: updated.sceneId,\n      },\n    });\n    console.info(\"[Screenity][BG] Local screen playback offer marked used\", {\n      offerId: updated.offerId,\n      projectId: updated.projectId,\n      sceneId: updated.sceneId,\n    });\n    return { ok: true, offer: updated };\n  });\n  registerMessage(\"cloud-local-playback-mark-fallback\", async (message) => {\n    const offer = await getValidLocalPlaybackOffer({\n      offerId: message?.offerId || null,\n      projectId: message?.projectId || null,\n      sceneId: message?.sceneId || null,\n    });\n    if (!offer) {\n      return { ok: false, error: \"offer-unavailable\" };\n    }\n    const updated = {\n      ...offer,\n      status: \"fallback\",\n      fallbackReason: message?.reason || \"unknown\",\n      fallbackAt: Date.now(),\n      updatedAt: Date.now(),\n    };\n    await chrome.storage.local.set({\n      [CLOUD_LOCAL_PLAYBACK_KEY]: updated,\n      [CLOUD_LOCAL_PLAYBACK_EVENT_KEY]: {\n        event: \"offer-fallback\",\n        at: Date.now(),\n        offerId: updated.offerId,\n        reason: updated.fallbackReason,\n      },\n    });\n    console.info(\"[Screenity][BG] Local screen playback offer fallback\", {\n      offerId: updated.offerId,\n      reason: updated.fallbackReason,\n    });\n    return { ok: true, offer: updated };\n  });\n  registerMessage(\"finish-multi-recording\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    await handleFinishMultiRecording();\n  });\n  registerMessage(\"handle-reactivate\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n\n    chrome.tabs.create({\n      url: `${process.env.SCREENITY_APP_BASE}/reactivate`,\n      active: true,\n    });\n  });\n  registerMessage(\"handle-upgrade\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n\n    chrome.tabs.create({\n      url: `${process.env.SCREENITY_APP_BASE}/upgrade`,\n      active: true,\n    });\n  });\n  registerMessage(\"open-account-settings\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    const { authenticated } = await loginWithWebsite();\n    if (!authenticated) {\n      console.warn(\"User not authenticated, cannot open account settings\");\n      return;\n    }\n\n    const url = `${process.env.SCREENITY_APP_BASE}/?settings=open`;\n    createTab(url, true, true);\n  });\n  registerMessage(\"open-support\", async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      console.warn(\"Cloud features disabled\");\n      return;\n    }\n    const { authenticated, user } = await loginWithWebsite();\n    if (!authenticated || !user) {\n      console.warn(\"User not authenticated, cannot open support\");\n      return;\n    }\n\n    const { name, email } = user;\n    const qs = await supportContextQuery({\n      includeRecordingState: true,\n      source: \"settings\",\n      user: { name, email },\n    });\n    const url = `https://tally.so/r/310MNg?extension=true&${qs}`;\n    createTab(url, true, true);\n  });\n  registerMessage(\"check-banner-support\", async (message, sendResponse) => {\n    const { bannerSupport } = await chrome.storage.local.get([\"bannerSupport\"]);\n    sendResponse({ bannerSupport: Boolean(bannerSupport) });\n    return true;\n  });\n  registerMessage(\"hide-banner\", async () => {\n    await chrome.storage.local.set({ bannerSupport: false });\n    chrome.runtime.sendMessage({ type: \"hide-banner\" });\n  });\n  registerMessage(\"clear-recording-alarm\", async () => {\n    await chrome.alarms.clear(\"recording-alarm\");\n  });\n  // Relay toasts to the active content script (extension pages can't reach it directly).\n  registerMessage(\"show-toast\", async (message) => {\n    try {\n      const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n      if (activeTab) {\n        sendMessageTab(activeTab, {\n          type: \"show-toast\",\n          message: message.message,\n          timeout: message.timeout,\n        }).catch(() => {});\n      }\n    } catch {}\n  });\n  registerMessage(\"get-tab-id\", (message, sender, sendResponse) => {\n    sendResponse({ tabId: sender?.tab?.id ?? null });\n    return true;\n  });\n  registerMessage(\"play-beep\", async (message, sender, sendResponse) => {\n    const ok = await ensureAudioOffscreen();\n    if (ok) {\n      chrome.runtime.sendMessage({ type: \"play-beep-offscreen\" });\n    }\n    if (sendResponse) sendResponse({ ok });\n    return true;\n  });\n  registerMessage(\"refresh-auth\", async () => {\n    if (!CLOUD_FEATURES_ENABLED)\n      return { success: false, message: \"Cloud features disabled\" };\n    return await loginWithWebsite();\n  });\n  registerMessage(\"sync-recording-state\", async (message, sendResponse) => {\n    const {\n      recording,\n      paused,\n      recordingStartTime,\n      pausedAt,\n      totalPausedMs,\n      pendingRecording,\n    } = await chrome.storage.local.get([\n      \"recording\",\n      \"paused\",\n      \"recordingStartTime\",\n      \"pausedAt\",\n      \"totalPausedMs\",\n      \"pendingRecording\",\n    ]);\n    sendResponse({\n      recording: Boolean(recording),\n      paused: Boolean(paused),\n      recordingStartTime: recordingStartTime || null,\n      pausedAt: pausedAt || null,\n      totalPausedMs: totalPausedMs || 0,\n      pendingRecording: Boolean(pendingRecording),\n    });\n    return true;\n  });\n  registerMessage(\n    \"register-recording-session\",\n    async (message, sender, sendResponse) => {\n      const incoming = normalizeIncomingSession(message.session || {}, sender);\n      const resolution = await resolveActiveSessionConflict(incoming);\n      if (!resolution.allow) {\n        sendResponse({\n          ok: false,\n          error: \"Another recording session is already active\",\n          activeRecordingSession,\n        });\n        return true;\n      }\n\n      activeRecordingSession = incoming;\n      registerRecordingTabListener(incoming.recorderTabId);\n      sendResponse({\n        ok: true,\n        session: activeRecordingSession,\n        staleRecovered: resolution.staleRecovered,\n      });\n      return true;\n    },\n  );\n\n  registerMessage(\n    \"clear-recording-session\",\n    async (message, sender, sendResponse) => {\n      await clearRecordingSessionSafe(\n        message?.reason || \"clear-recording-session\",\n      );\n      sendResponse({ ok: true });\n      return true;\n    },\n  );\n\n  registerMessage(\n    \"clear-recording-session-safe\",\n    async (message, sender, sendResponse) => {\n      await clearRecordingSessionSafe(\n        message?.reason || \"clear-recording-session-safe\",\n        {\n          sourceTabId: sender?.tab?.id || null,\n        },\n      );\n      sendResponse({ ok: true });\n      return true;\n    },\n  );\n\n  registerMessage(\n    \"restore-recording-session\",\n    async (message, sender, sendResponse) => {\n      const { recorderSession } = await chrome.storage.local.get([\n        \"recorderSession\",\n      ]);\n      sendResponse({ recorderSession: recorderSession || null });\n      return true;\n    },\n  );\n};\n"
  },
  {
    "path": "src/pages/Background/modules/signIn.js",
    "content": "const isEdge = navigator.userAgent.includes(\"Edg\");\n\nconst EDGE_CLIENT_ID =\n  \"560517327251-856rbcshgori6mft9slnsaq34p21td3n.apps.googleusercontent.com\";\nconst REDIRECT_URI = isEdge ? chrome.identity.getRedirectURL() : \"\";\n\nconst signInEdge = async () => {\n  const authUrl = new URL(\"https://accounts.google.com/o/oauth2/v2/auth\");\n  authUrl.searchParams.set(\"client_id\", EDGE_CLIENT_ID);\n  authUrl.searchParams.set(\"response_type\", \"token\");\n  authUrl.searchParams.set(\"redirect_uri\", REDIRECT_URI);\n  authUrl.searchParams.set(\n    \"scope\",\n    \"https://www.googleapis.com/auth/drive.file\"\n  );\n\n  return new Promise((resolve, reject) => {\n    chrome.identity.launchWebAuthFlow(\n      {\n        url: authUrl.href,\n        interactive: true,\n      },\n      async (redirectUrl) => {\n        if (chrome.runtime.lastError) {\n          console.error(\"[Drive] drive_auth_failed: Edge auth error:\", chrome.runtime.lastError.message);\n          reject(new Error(chrome.runtime.lastError.message));\n          return;\n        }\n\n        if (!redirectUrl) {\n          console.error(\"[Drive] drive_auth_failed: no redirect URL (user cancelled?)\");\n          reject(new Error(\"User cancelled sign-in or failed to get token\"));\n          return;\n        }\n\n        // Extract token from redirect URL\n        const url = new URL(redirectUrl);\n        const params = new URLSearchParams(url.hash.substring(1));\n        const token = params.get(\"access_token\");\n\n        if (!token) {\n          console.error(\"[Drive] drive_auth_failed: no access_token in redirect\");\n          reject(new Error(\"Failed to extract token from redirect\"));\n          return;\n        }\n\n        // Save token to storage\n        await new Promise((res) =>\n          chrome.storage.local.set({ token }, () => res())\n        );\n\n        resolve(token);\n      }\n    );\n  });\n};\n\nconst signInChrome = async () => {\n  const token = await chrome.identity.getAuthToken({ interactive: true });\n\n  if (!token) {\n    console.error(\"[Drive] drive_auth_failed: getAuthToken returned null\");\n    throw new Error(\"User cancelled sign-in or failed to get token\");\n  }\n\n  // Save token to storage\n  await new Promise((resolve) =>\n    chrome.storage.local.set({ token: token.token }, () => resolve())\n  );\n\n  return token.token;\n};\n\nconst signIn = async () => {\n  try {\n    if (isEdge) {\n      return await signInEdge();\n    } else {\n      return await signInChrome();\n    }\n  } catch (error) {\n    console.error(\"[Drive] drive_auth_failed:\", error.message);\n    return null;\n  }\n};\n\nexport default signIn;\n"
  },
  {
    "path": "src/pages/Background/offscreen/closeOffscreenDocument.js",
    "content": "export const closeOffscreenDocument = async () => {\n  try {\n    const existingContexts = await chrome.runtime.getContexts({});\n    const offscreenDocument = existingContexts.find(\n      (c) => c.contextType === \"OFFSCREEN_DOCUMENT\"\n    );\n\n    if (offscreenDocument) {\n      await chrome.offscreen.closeDocument();\n    }\n  } catch (error) {\n    console.error(\"Failed to close offscreen document:\", error);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/offscreen/discardOffscreenDocuments.js",
    "content": "/**\n * Closes any active offscreen document if it exists.\n * Also clears the offscreen storage flag to prevent stale state.\n */\nexport const discardOffscreenDocuments = async () => {\n  try {\n    const existingContexts = await chrome.runtime.getContexts({});\n    const offscreenDocument = existingContexts.find(\n      (c) => c.contextType === \"OFFSCREEN_DOCUMENT\"\n    );\n\n    if (offscreenDocument) {\n      await chrome.offscreen.closeDocument();\n    }\n  } catch (error) {\n    console.error(\"Failed to discard offscreen documents:\", error.message);\n  }\n  chrome.storage.local.set({ offscreen: false });\n};\n"
  },
  {
    "path": "src/pages/Background/offscreen/offscreenDocument.js",
    "content": "import { getCurrentTab } from \"../tabManagement\";\nimport { removeTab } from \"../tabManagement/removeTab\";\nimport { sendMessageRecord } from \"../recording/sendMessageRecord.js\";\nimport { closeOffscreenDocument } from \"./closeOffscreenDocument.js\";\nimport { loginWithWebsite } from \"../auth/loginWithWebsite.js\";\nimport { traceStep } from \"../../utils/startFlowTrace.js\";\n\nconst openRecorderTab = async (\n  activeTab,\n  backup,\n  isRegion,\n  camera = false,\n  request\n) => {\n  let switchTab = true;\n\n  // Check subscription status\n  const { authenticated, subscribed, cached, transient, error: authError } = await loginWithWebsite();\n  const recorderUrl =\n    authenticated && subscribed\n      ? chrome.runtime.getURL(\"cloudrecorder.html\")\n      : chrome.runtime.getURL(\"recorder.html\");\n  if (!isRegion) {\n    if (camera) {\n      switchTab = false;\n    }\n  } else {\n    switchTab = activeTab.url.includes(\n      chrome.runtime.getURL(\"playground.html\")\n    );\n  }\n\n  // Close any leftover recorder tab from a previous recording so we don't\n  // end up with two pinned tabs. Only close actual extension recorder pages —\n  // for region recordings, recordingTab can temporarily point to the user's\n  // active page, which we must NOT close.\n  const { recordingTab: prevRecTab } = await chrome.storage.local.get([\n    \"recordingTab\",\n  ]);\n  if (prevRecTab != null) {\n    try {\n      const prevTab = await chrome.tabs.get(prevRecTab);\n      const prevUrl = prevTab?.url || \"\";\n      if (\n        prevUrl.includes(\"recorder.html\") ||\n        prevUrl.includes(\"cloudrecorder.html\")\n      ) {\n        await removeTab(prevRecTab);\n      }\n    } catch {\n      // Tab doesn't exist — just clear the reference\n    }\n    chrome.storage.local.set({ recordingTab: null });\n  }\n\n  chrome.tabs\n    .create({\n      url: recorderUrl,\n      pinned: true,\n      index: 0,\n      // FLAG: Check this is ok?\n      active: switchTab,\n    })\n    .then((tab) => {\n      // Prevent Chrome from discarding this tab during recording\n      chrome.tabs.update(tab.id, { autoDiscardable: false }).catch((err) => {\n        console.warn(\n          \"[Screenity] autoDiscardable failed for recorder tab\",\n          tab.id,\n          String(err),\n        );\n        chrome.storage.local.set({\n          lastAutoDiscardableError: {\n            ts: Date.now(),\n            tabId: tab.id,\n            error: String(err),\n          },\n        });\n      });\n\n      chrome.storage.local.set({\n        recordingTab: tab.id,\n        offscreen: false,\n        region: false,\n        wasRegion: true,\n        clickEvents: [],\n        recordingUiTabId: activeTab.id,\n        ...(isRegion ? { tabRecordedID: activeTab.id } : {}),\n      });\n\n      chrome.tabs.onUpdated.addListener(function _(tabId, changeInfo) {\n        if (tabId === tab.id && changeInfo.status === \"complete\") {\n          chrome.tabs.onUpdated.removeListener(_);\n          traceStep(\"recorderTabCreated\");\n          // Include tabPreferred in the message so CloudRecorder can use it\n          // synchronously without racing against its own storage read.\n          const isPlayground = activeTab.url.includes(\n            chrome.runtime.getURL(\"playground.html\")\n          );\n          sendMessageRecord({\n            type: \"loaded\",\n            request: request,\n            backup: backup,\n            tabPreferred: isPlayground,\n            ...(isRegion\n              ? {\n                  isTab: true,\n                  tabID: activeTab.id,\n                }\n              : {}),\n          });\n        }\n      });\n    });\n};\n\nexport const offscreenDocument = async (request, tabId = null) => {\n  const { backup } = await chrome.storage.local.get([\"backup\"]);\n  let activeTab = await getCurrentTab();\n\n  if (tabId !== null) {\n    activeTab = await chrome.tabs.get(tabId);\n  }\n\n  chrome.storage.local.set({\n    activeTab: activeTab.id,\n    tabRecordedID: null,\n    memoryError: false,\n    recordingUiTabId: activeTab.id,\n  });\n\n  if (activeTab.url.includes(chrome.runtime.getURL(\"playground.html\"))) {\n    chrome.storage.local.set({ tabPreferred: true });\n  } else {\n    chrome.storage.local.set({ tabPreferred: false });\n  }\n\n  await closeOffscreenDocument();\n\n  if (request.region) {\n    if (tabId !== null) chrome.tabs.update(tabId, { active: true });\n\n    chrome.storage.local.set({\n      recordingTab: activeTab.id,\n      offscreen: false,\n      region: true,\n      recordingUiTabId: activeTab.id,\n    });\n\n    if (request.customRegion) {\n      sendMessageRecord({\n        type: \"loaded\",\n        request: request,\n        backup: backup,\n        region: true,\n      });\n    } else {\n      await openRecorderTab(activeTab, backup, true, false, request);\n    }\n  } else {\n    if (!request.offscreenRecording || request.camera) {\n      // Skip offscreen recording if conditions aren't met\n      await openRecorderTab(activeTab, backup, false, request.camera, request);\n      return;\n    }\n\n    try {\n      if (tabId !== null) chrome.tabs.update(tabId, { active: true });\n\n      const { qualityValue, fpsValue } = await chrome.storage.local.get([\n        \"qualityValue\",\n        \"fpsValue\",\n      ]);\n\n      await closeOffscreenDocument();\n\n      await chrome.offscreen.createDocument({\n        url: \"recorderoffscreen.html\",\n        reasons: [\"USER_MEDIA\", \"AUDIO_PLAYBACK\", \"DISPLAY_MEDIA\"],\n        justification: \"Recording from getDisplayMedia API\",\n      });\n\n      chrome.storage.local.set({\n        recordingTab: null,\n        offscreen: true,\n        region: false,\n        wasRegion: false,\n        recordingUiTabId: activeTab.id,\n      });\n\n      sendMessageRecord({\n        type: \"loaded\",\n        request: request,\n        isTab: false,\n        quality: qualityValue,\n        fps: fpsValue,\n        backup: backup,\n      });\n    } catch (error) {\n      console.error(\"Error creating offscreen document:\", error);\n      await openRecorderTab(activeTab, backup, false, request.camera, request);\n    }\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/cancelRecording.js",
    "content": "import { sendMessageTab, focusTab } from \"../tabManagement\";\nimport { discardOffscreenDocuments } from \"../offscreen/discardOffscreenDocuments\";\n\nexport const handleDismiss = async () => {\n  try {\n    await chrome.storage.local.set({ restarting: true });\n\n    const { region, wasRegion } = await chrome.storage.local.get([\n      \"region\",\n      \"wasRegion\",\n    ]);\n\n    if (wasRegion) {\n      await chrome.storage.local.set({ wasRegion: false, region: true });\n    }\n\n    chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n    chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n    chrome.storage.local.set({ pipForceClose: Date.now() });\n    chrome.storage.local.set({ recordingUiTabId: null });\n    chrome.storage.local.remove([\"recordingMeta\"]);\n  } catch (error) {\n    console.error(\"Failed to handle dismiss:\", error);\n  }\n};\n\nexport const cancelRecording = async () => {\n  try {\n    // Reset the icon to its default state\n    chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n\n    // Get the active tab from storage\n    const { activeTab, recordingUiTabId, tabRecordedID } =\n      await chrome.storage.local.get([\n        \"activeTab\",\n        \"recordingUiTabId\",\n        \"tabRecordedID\",\n      ]);\n\n    await chrome.storage.local.set({\n      pendingRecording: false,\n      recordingUiTabId: null,\n      tabRecordedID: null,\n    });\n\n    // Send stop message, focus the tab, and clean up\n    const candidateTabs = [activeTab, recordingUiTabId, tabRecordedID].filter(\n      (id, idx, arr) => Number.isInteger(id) && arr.indexOf(id) === idx,\n    );\n    candidateTabs.forEach((id) => {\n      sendMessageTab(id, { type: \"stop-pending\" }).catch(() => {});\n    });\n    focusTab(activeTab);\n    discardOffscreenDocuments();\n    chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n    chrome.storage.local.set({ pipForceClose: Date.now() });\n    chrome.storage.local.set({ recordingUiTabId: null });\n    chrome.storage.local.remove([\"recordingMeta\"]);\n  } catch (error) {\n    console.error(\"Failed to cancel recording:\", error.message);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/checkRecording.js",
    "content": "import { discardRecording } from \"./discardRecording\";\nimport { discardOffscreenDocuments } from \"../offscreen/discardOffscreenDocuments\";\n\nexport const checkRecording = async () => {\n  const { recordingTab, offscreen } = await chrome.storage.local.get([\n    \"recordingTab\",\n    \"offscreen\",\n  ]);\n\n  if (recordingTab && !offscreen) {\n    try {\n      chrome.tabs.get(recordingTab, (tab) => {\n        if (chrome.runtime.lastError || !tab) {\n          discardRecording();\n        }\n      });\n    } catch (error) {\n      discardRecording();\n    }\n  } else if (offscreen) {\n    try {\n      const existingContexts = await chrome.runtime.getContexts({});\n      const offDocument = existingContexts.find(\n        (c) => c.contextType === \"OFFSCREEN_DOCUMENT\"\n      );\n\n      if (!offDocument) {\n        discardOffscreenDocuments();\n        discardRecording();\n      }\n    } catch (error) {\n      console.error(\"Error checking offscreen document: \", error);\n      discardRecording();\n    }\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/chunkHandler.js",
    "content": "import localforage from \"localforage\";\nimport { blobToBase64 } from \"../utils/blobToBase64\";\nimport { sendMessageTab } from \"../tabManagement\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\nexport const chunksStore = localforage.createInstance({ name: \"chunks\" });\nexport const localDirectoryStore = localforage.createInstance({\n  name: \"localDirectory\",\n});\nconst DEBUG_POSTSTOP = false;\n\nexport const clearAllRecordings = async () => {\n  try {\n    await chunksStore.clear();\n  } catch (err) {\n    console.error(\"Failed to clear chunksStore\", err);\n  }\n};\n\nexport const handleChunks = async (chunks, override = false, target = null) => {\n  const { sendingChunks, sandboxTab, bannerSupport } =\n    await chrome.storage.local.get([\n      \"sendingChunks\",\n      \"sandboxTab\",\n      \"bannerSupport\",\n    ]);\n\n  if (DEBUG_POSTSTOP)\n    console.debug(\"[Screenity][BG] handleChunks called\", {\n      chunksLength: chunks?.length,\n      sandboxTab,\n      override,\n    });\n\n  if (sendingChunks) return;\n\n  await chrome.storage.local.set({ sendingChunks: true });\n\n  try {\n    if (!Array.isArray(chunks) || chunks.length === 0) {\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][BG] no chunks to send; deferring delivery\");\n      return;\n    }\n\n    chunks.sort((a, b) => {\n      if (a.index == null) return -1;\n      if (b.index == null) return 1;\n      return a.index - b.index;\n    });\n\n    const batchSize = 10;\n    const maxRetries = 3;\n    const retryDelay = 1000;\n\n    const targetTab = target?.tabId || sandboxTab;\n    const targetFrame = target?.frameId ?? null;\n\n    if (DEBUG_POSTSTOP)\n      console.debug(\"[Screenity][BG] sending chunk-count\", {\n        count: chunks.length,\n        targetTab,\n        targetFrame,\n        override,\n      });\n\n    const sendToTarget = (msg) =>\n      new Promise((resolve, reject) => {\n        try {\n          if (targetTab == null) return reject(new Error(\"no-target-tab\"));\n          if (typeof targetFrame === \"number\") {\n            chrome.tabs.sendMessage(\n              targetTab,\n              msg,\n              { frameId: targetFrame },\n              (resp) => {\n                if (chrome.runtime.lastError)\n                  return reject(chrome.runtime.lastError.message);\n                resolve(resp);\n              },\n            );\n          } else {\n            sendMessageTab(targetTab, msg, null).then(resolve).catch(reject);\n          }\n        } catch (err) {\n          reject(err);\n        }\n      });\n\n    try {\n      await sendToTarget({\n        type: \"chunk-count\",\n        count: chunks.length,\n        override,\n      });\n    } catch (err) {\n      if (DEBUG_POSTSTOP)\n        console.warn(\"[Screenity][BG] chunk-count message failed\", err);\n    }\n\n    if (bannerSupport) {\n      try {\n        await sendToTarget({ type: \"banner-support\" });\n      } catch (err) {\n        if (DEBUG_POSTSTOP)\n          console.warn(\"[Screenity][BG] banner-support message failed\", err);\n      }\n    }\n\n    const delay = (ms) => new Promise((res) => setTimeout(res, ms));\n\n    const sendBatch = async (batch) => {\n      let attempt = 0;\n      while (attempt < maxRetries) {\n        attempt += 1;\n        try {\n          if (DEBUG_POSTSTOP)\n            console.debug(\"[Screenity][BG] sending new-chunk-tab batch\", {\n              batchLen: batch.length,\n              attempt,\n            });\n          await sendToTarget({ type: \"new-chunk-tab\", chunks: batch });\n          return true;\n        } catch (err) {\n          if (DEBUG_POSTSTOP)\n            console.warn(\"[Screenity][BG] sendBatch attempt failed, retrying\", {\n              attempt,\n              err,\n            });\n          if (attempt < maxRetries) await delay(retryDelay);\n        }\n      }\n      if (DEBUG_POSTSTOP)\n        console.warn(\"[Screenity][BG] sendBatch failed after retries\");\n      return false;\n    };\n\n    let currentIndex = 0;\n\n    while (currentIndex < chunks.length) {\n      const end = Math.min(currentIndex + batchSize, chunks.length);\n      const rawBatch = chunks.slice(currentIndex, end);\n\n      const batch = await Promise.all(\n        rawBatch.map(async (chunk) => {\n          try {\n            const base64 = await blobToBase64(chunk.chunk);\n            return { chunk: base64, index: chunk.index };\n          } catch {\n            return null;\n          }\n        }),\n      );\n\n      const filtered = batch.filter(Boolean);\n      if (filtered.length > 0) {\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][BG] sending filtered batch\", {\n            filteredLen: filtered.length,\n            currentIndex,\n          });\n        const ok = await sendBatch(filtered);\n        if (!ok) {\n          if (DEBUG_POSTSTOP)\n            console.warn(\"[Screenity][BG] failed to send batch, aborting\");\n          return;\n        }\n      }\n\n      currentIndex += batchSize;\n    }\n\n    if (DEBUG_POSTSTOP)\n      console.debug(\n        \"[Screenity][BG] all batches sent, instructing sandbox to make video tab\",\n        { sandboxTab: targetTab },\n      );\n    try {\n      await sendToTarget({ type: \"make-video-tab\", override });\n    } catch (err) {\n      if (DEBUG_POSTSTOP)\n        console.warn(\"[Screenity][BG] make-video-tab message failed\", err);\n    }\n  } finally {\n    await chrome.storage.local.set({ sendingChunks: false });\n  }\n};\n\nexport const newChunk = async (request, sender, sendResponse) => {\n  try {\n    const { sandboxTab } = await chrome.storage.local.get([\"sandboxTab\"]);\n\n    await sendMessageTab(sandboxTab, {\n      type: \"new-chunk-tab\",\n      chunk: request.chunk,\n      index: request.index,\n    });\n\n    sendResponse({ status: \"ok\" });\n  } catch (err) {\n    sendResponse({ status: \"error\", error: err.message });\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "src/pages/Background/recording/cloudLocalPlaybackConstants.js",
    "content": "export const CLOUD_LOCAL_PLAYBACK_KEY = \"cloudLocalScreenPlaybackOffer\";\nexport const CLOUD_LOCAL_PLAYBACK_EVENT_KEY = \"lastCloudLocalPlaybackEvent\";\nexport const CLOUD_LOCAL_PLAYBACK_ALARM = \"cloud-local-playback-expiry\";\n"
  },
  {
    "path": "src/pages/Background/recording/desktopCapture.js",
    "content": "import { getCurrentTab } from \"../tabManagement\";\nimport { initBackup } from \"../backup/initBackup\";\nimport { offscreenDocument } from \"../offscreen/offscreenDocument\";\nimport { localDirectoryStore } from \"./chunkHandler\";\n\nexport const desktopCapture = async (request) => {\n  const { backup } = await chrome.storage.local.get([\"backup\"]);\n  const { backupSetup } = await chrome.storage.local.get([\"backupSetup\"]);\n\n  // Ensure that chunk sending is marked as not in progress\n  chrome.storage.local.set({ sendingChunks: false });\n\n  if (backup) {\n    // Clear the local directory store if backup hasn't been set up\n    if (!backupSetup) {\n      localDirectoryStore.clear();\n    }\n\n    // Get the current active tab and initialize backup\n    const activeTab = await getCurrentTab();\n    initBackup(request, activeTab.id);\n  } else {\n    // Proceed with offscreen document creation\n    offscreenDocument(request);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/discardRecording.js",
    "content": "import { sendMessageRecord } from \"./sendMessageRecord\";\nimport { discardOffscreenDocuments } from \"../offscreen/discardOffscreenDocuments\";\n\nexport const discardRecording = async () => {\n  sendMessageRecord({ type: \"dismiss-recording\" });\n  chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n\n  // Clear offscreen documents if they exist\n  discardOffscreenDocuments();\n\n  chrome.storage.local.set({\n    recordingTab: null,\n    sandboxTab: null,\n    recording: false,\n    restarting: false,\n    pendingRecording: false,\n    offscreen: false,\n    postStopEditorOpened: false,\n  });\n  chrome.storage.local.set({ pipForceClose: Date.now() });\n  chrome.storage.local.set({ recordingUiTabId: null });\n  chrome.storage.local.remove([\"recordingMeta\"]);\n\n  chrome.runtime.sendMessage({ type: \"discard-backup\" });\n  chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n};\n\nexport const handleDismissRecordingTab = async () => {\n  chrome.runtime.sendMessage({ type: \"discard-backup\" });\n  discardRecording();\n};\n"
  },
  {
    "path": "src/pages/Background/recording/forceProcessing.js",
    "content": "import { removeTab } from \"../tabManagement\";\nimport { sendChunks } from \"./sendChunks\";\n\nexport const forceProcessing = async () => {\n  const editorURL = \"editor.html\";\n\n  const { sandboxTab } = await chrome.storage.local.get([\"sandboxTab\"]);\n\n  chrome.tabs.create(\n    {\n      url: editorURL,\n      active: true,\n    },\n    (tab) => {\n      chrome.tabs.onUpdated.addListener(function onTabUpdate(\n        tabId,\n        changeInfo\n      ) {\n        if (tabId === tab.id && changeInfo.status === \"complete\") {\n          chrome.tabs.onUpdated.removeListener(onTabUpdate);\n\n          if (sandboxTab) {\n            removeTab(sandboxTab);\n          }\n\n          chrome.storage.local.set({ sandboxTab: tab.id });\n\n          sendChunks(true);\n        }\n      });\n    }\n  );\n};\n"
  },
  {
    "path": "src/pages/Background/recording/getStreamingData.js",
    "content": "export const getStreamingData = async () => {\n  try {\n    const {\n      micActive,\n      defaultAudioInput,\n      defaultAudioOutput,\n      defaultVideoInput,\n      systemAudio,\n      recordingType,\n    } = await chrome.storage.local.get([\n      \"micActive\",\n      \"defaultAudioInput\",\n      \"defaultAudioOutput\",\n      \"defaultVideoInput\",\n      \"systemAudio\",\n      \"recordingType\",\n    ]);\n\n    return {\n      micActive,\n      defaultAudioInput,\n      defaultAudioOutput,\n      defaultVideoInput,\n      systemAudio,\n      recordingType,\n    };\n  } catch (error) {\n    console.error(\"Failed to retrieve streaming data:\", error);\n    return null;\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/recordingHelpers.js",
    "content": "import {\n  sendMessageTab,\n  focusTab,\n  removeTab,\n  getCurrentTab,\n} from \"../tabManagement\";\nimport { sendMessageRecord } from \"./sendMessageRecord\";\nimport { stopRecording } from \"./stopRecording\";\nimport { addAlarmListener } from \"../alarms/addAlarmListener\";\nimport { getStreamingData } from \"./getStreamingData\";\nimport { discardOffscreenDocuments } from \"../offscreen/discardOffscreenDocuments\";\nimport { diagEvent, endDiagSession } from \"../../utils/diagnosticLog\";\nimport { classifyError } from \"../../utils/errorCodes\";\nexport const checkCapturePermissions = async ({ isLoggedIn, isSubscribed }) => {\n  const permissions = [\"desktopCapture\", \"alarms\", \"offscreen\"];\n\n  // Add clipboardWrite and notifications only for subscribed users\n  if (isLoggedIn && isSubscribed) {\n    permissions.push(\"clipboardWrite\");\n  }\n\n  // Check if required APIs are available in this browser context\n  if (\n    chrome.desktopCapture &&\n    chrome.alarms &&\n    chrome.offscreen &&\n    (!permissions.includes(\"clipboardWrite\") || chrome.clipboard)\n  ) {\n    return { status: \"ok\" };\n  }\n\n  const granted = await new Promise((resolve) => {\n    chrome.permissions.request({ permissions }, resolve);\n  });\n\n  if (granted) {\n    addAlarmListener();\n    return { status: \"ok\" };\n  } else {\n    return { status: \"error\" };\n  }\n};\n\nexport const handlePip = async (started = false) => {\n  const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n  if (started) {\n    sendMessageTab(activeTab, { type: \"pip-started\" });\n  } else {\n    sendMessageTab(activeTab, { type: \"pip-ended\" });\n  }\n};\n\nexport const handleOnGetPermissions = async (request) => {\n  // Send a message to (actual) active tab\n  const activeTab = await getCurrentTab();\n  if (activeTab) {\n    sendMessageTab(activeTab.id, {\n      type: \"on-get-permissions\",\n      data: request,\n    });\n  }\n};\n\nexport const handleRecordingComplete = async () => {\n  const { recordingTab } = await chrome.storage.local.get([\"recordingTab\"]);\n\n  if (recordingTab) {\n    chrome.tabs.get(recordingTab, (tab) => {\n      if (chrome.runtime.lastError || !tab) return;\n      // Check if tab url contains chrome-extension and recorder.html\n      if (\n        tab.url.includes(\"chrome-extension\") &&\n        tab.url.includes(\"recorder.html\")\n      ) {\n        // FLAG: For testing purposes -> comment to debug\n        removeTab(recordingTab);\n      }\n    });\n  }\n  // Clear so subsequent recordings don't conflict\n  chrome.storage.local.set({ recordingTab: null, offscreen: false });\n  console.log(\"[Screenity][BG] handleRecordingComplete fired, recordingTab:\", recordingTab);\n};\n\nexport const handleRecordingError = async (request) => {\n  if (globalThis.SCREENITY_VERBOSE_LOGS) {\n    console.log(\"handleRecordingError called with request:\", request);\n  }\n\n  // Propagate or derive error code\n  const errorCode =\n    request?.errorCode ||\n    classifyError(request?.why || \"\", request?.error || \"\");\n\n  try {\n    const { recordingAttemptId } = await chrome.storage.local.get([\n      \"recordingAttemptId\",\n    ]);\n    await chrome.storage.local.set({\n      lastRecordingError: {\n        ts: Date.now(),\n        error: request?.error || null,\n        why: request?.why || null,\n        errorCode,\n        recordingAttemptId: recordingAttemptId || null,\n      },\n    });\n  } catch {}\n  const { activeTab, recordingUiTabId, tabRecordedID } =\n    await chrome.storage.local.get([\n      \"activeTab\",\n      \"recordingUiTabId\",\n      \"tabRecordedID\",\n    ]);\n\n  await chrome.storage.local.set({\n    pendingRecording: false,\n  });\n\n  const isWarningOnly = request.error === \"stream-ended\";\n  diagEvent(isWarningOnly ? \"warning\" : \"error\", {\n    error: request?.error || null,\n    why: request?.why || null,\n    errorCode,\n  });\n\n  // For stream-ended, we just notify the user but DON'T stop the recording\n  // The user can decide whether to continue or stop\n  if (isWarningOnly) {\n    sendMessageTab(activeTab, {\n      type: \"stream-ended-warning\",\n      message: request.why || chrome.i18n.getMessage(\"streamEndedWarningToast\"),\n    }).catch((err) => {\n      diagEvent(\"warning\", { note: \"stream-ended-warning undelivered\", err: String(err).slice(0, 80) });\n    });\n    return; // Don't continue with the normal error handling\n  }\n\n  endDiagSession(\"error\");\n\n  await chrome.storage.local.set({\n    recording: false,\n    recordingUiTabId: null,\n    tabRecordedID: null,\n    offscreen: false,\n    postStopEditorOpened: false,\n  });\n\n  chrome.runtime\n    .sendMessage({\n      type: \"clear-recording-session-safe\",\n      reason: \"recording-error-terminal\",\n    })\n    .catch((err) => {\n      diagEvent(\"warning\", { note: \"clear-session-safe undelivered\", err: String(err).slice(0, 80) });\n    });\n\n  sendMessageRecord({ type: \"recording-error\" }).then(() => {\n    const candidateTabs = [activeTab, recordingUiTabId, tabRecordedID].filter(\n      (id, idx, arr) => Number.isInteger(id) && arr.indexOf(id) === idx,\n    );\n    candidateTabs.forEach((id) => {\n      sendMessageTab(id, { type: \"stop-pending\" }).catch(() => {});\n    });\n    focusTab(activeTab);\n    if (request.error === \"stream-error\") {\n      sendMessageTab(activeTab, { type: \"stream-error\", errorCode });\n    } else if (request.error === \"backup-error\") {\n      sendMessageTab(activeTab, { type: \"backup-error\", errorCode });\n    }\n  });\n\n  const { recordingTab } = await chrome.storage.local.get([\"recordingTab\"]);\n  const { region } = await chrome.storage.local.get([\"region\"]);\n  if (recordingTab && !region) {\n    // FLAG: For testing purposes -> comment to debug\n    removeTab(recordingTab);\n  }\n  chrome.storage.local.set({ recordingTab: null });\n  discardOffscreenDocuments();\n};\n\nexport const handleGetStreamingData = async () => {\n  const data = await getStreamingData();\n  sendMessageRecord({ type: \"streaming-data\", data: JSON.stringify(data) });\n};\n\nexport const videoReady = async () => {\n  const { backupTab } = await chrome.storage.local.get([\"backupTab\"]);\n  if (backupTab) {\n    sendMessageTab(backupTab, { type: \"close-writable\" });\n  }\n  chrome.runtime\n    .sendMessage({\n      type: \"clear-recording-session-safe\",\n      reason: \"video-ready-terminal\",\n    })\n    .catch((err) => {\n      diagEvent(\"warning\", { note: \"clear-session-safe undelivered (video-ready)\", err: String(err).slice(0, 80) });\n    });\n  await stopRecording();\n};\n\nexport const writeFile = async (request) => {\n  const { backupTab } = await chrome.storage.local.get([\"backupTab\"]);\n\n  if (backupTab) {\n    sendMessageTab(\n      backupTab,\n      {\n        type: \"write-file\",\n        index: request.index,\n      },\n      null,\n      () => {\n        sendMessageRecord({ type: \"stop-recording-tab\" });\n      },\n    );\n  } else {\n    sendMessageRecord({ type: \"stop-recording-tab\" });\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/restartRecording.js",
    "content": "import { sendMessageRecord } from \"./sendMessageRecord\";\nimport { resetActiveTabRestart } from \"../tabManagement/resetActiveTab\";\nimport { diagEvent } from \"../../utils/diagnosticLog\";\n\nconst RESTART_ACK_TIMEOUT_MS = 7000;\n\nconst withTimeout = (promise, timeoutMs) =>\n  Promise.race([\n    promise,\n    new Promise((_, reject) => {\n      setTimeout(() => reject(new Error(\"restart-ack-timeout\")), timeoutMs);\n    }),\n  ]);\n\nconst persistRestartFlow = async (phase, details = {}) => {\n  await chrome.storage.local.set({\n    lastRestartFlow: {\n      phase,\n      ts: Date.now(),\n      ...details,\n    },\n  });\n};\n\nconst resolveSourceTabId = (message, sender) =>\n  message?.sourceTabId || sender?.tab?.id || null;\n\nexport const handleRestart = async (message = {}, sender = null) => {\n  const sourceTabId = resolveSourceTabId(message, sender);\n  await chrome.storage.local.set({\n    restarting: true,\n    ...(sourceTabId != null\n      ? { activeTab: sourceTabId, recordingUiTabId: sourceTabId }\n      : {}),\n  });\n  await persistRestartFlow(\"requested\", { sourceTabId });\n  diagEvent(\"restart-requested\");\n\n  try {\n    const response = await withTimeout(\n      sendMessageRecord({\n        type: \"restart-recording-tab\",\n        sourceTabId,\n      }),\n      RESTART_ACK_TIMEOUT_MS,\n    );\n\n    if (!response?.ok || response?.restarted !== true) {\n      throw new Error(response?.error || \"restart-ack-failed\");\n    }\n\n    await persistRestartFlow(\"ack\", {\n      sourceTabId,\n      restarted: Boolean(response?.restarted),\n    });\n    await resetActiveTabRestart({ sourceTabId });\n    await persistRestartFlow(\"countdown-dispatched\", { sourceTabId });\n    diagEvent(\"restart-completed\");\n    return { ok: true, restarted: true };\n  } catch (error) {\n    const reason = error?.message || String(error);\n    await chrome.storage.local.set({ restarting: false });\n    await persistRestartFlow(\"failed\", { sourceTabId, reason });\n    diagEvent(\"restart-failed\", { reason });\n    return { ok: false, error: reason };\n  }\n};\n\nexport const handleRestartRecordingTab = async (message, sender) =>\n  handleRestart(message, sender);\n"
  },
  {
    "path": "src/pages/Background/recording/restoreCloudRecording.js",
    "content": "import localforage from \"localforage\";\nimport { createTab, sendMessageTab } from \"../tabManagement\";\n\nconst RECOVERABLE_CLOUD_STATUSES = new Set([\n  \"recording\",\n  \"hidden\",\n  \"unload\",\n  \"stopping\",\n  \"finalize-failed\",\n  \"upload-stalled\",\n]);\n\n// Mirror the store names CloudRecorder.jsx uses. createInstance({ name }) sets\n// the IndexedDB database name directly, so these reach the same databases\n// regardless of any global localforage.config() call.\nconst cloudScreenStore = localforage.createInstance({ name: \"chunks\" });\nconst cloudCameraStore = localforage.createInstance({ name: \"cameraChunks\" });\nconst cloudAudioStore = localforage.createInstance({ name: \"audioChunks\" });\n\nexport const checkCloudRestore = async () => {\n  try {\n    const { recorderSession } = await chrome.storage.local.get([\n      \"recorderSession\",\n    ]);\n    if (\n      !recorderSession ||\n      !RECOVERABLE_CLOUD_STATUSES.has(recorderSession.status)\n    ) {\n      return { cloudRestore: false };\n    }\n\n    const [screenCount, cameraCount, audioCount] = await Promise.all([\n      cloudScreenStore.length().catch(() => 0),\n      cloudCameraStore.length().catch(() => 0),\n      cloudAudioStore.length().catch(() => 0),\n    ]);\n\n    return {\n      cloudRestore: screenCount > 0 || cameraCount > 0 || audioCount > 0,\n    };\n  } catch (err) {\n    console.warn(\"[CloudRestore] checkCloudRestore failed:\", err);\n    return { cloudRestore: false };\n  }\n};\n\nexport const restoreCloudRecording = async () => {\n  const { cloudRestore } = await checkCloudRestore();\n  if (!cloudRestore) return;\n\n  const tab = await createTab(\"download.html\", true, true);\n  if (!tab?.id) return;\n\n  chrome.tabs.onUpdated.addListener(function listener(tabId, info) {\n    if (info.status === \"complete\" && tabId === tab.id) {\n      chrome.tabs.onUpdated.removeListener(listener);\n      sendMessageTab(tab.id, { type: \"recover-cloud-indexed-db\" });\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/recording/restoreRecording.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\nimport { chunksStore } from \"./chunkHandler\";\n\nexport const checkRestore = async () => {\n  const chunks = [];\n  await chunksStore.iterate((value, key) => {\n    chunks.push(value);\n  });\n\n  if (chunks.length === 0) {\n    return { restore: false, chunks: [] };\n  }\n  return { restore: true };\n};\n\nexport const restoreRecording = async () => {\n  const { fastRecorderInUse } = await chrome.storage.local.get([\n    \"fastRecorderInUse\",\n  ]);\n  //const hasWebCodecs = supportsWebCodecs();\n  const hasWebCodecs = Boolean(fastRecorderInUse);\n\n  let editorUrl, messageType;\n\n  if (hasWebCodecs) {\n    editorUrl = \"editorwebcodecs.html?mode=recover\";\n    messageType = \"restore-recording\";\n  } else {\n    editorUrl = \"editorviewer.html?mode=recover\";\n    messageType = \"viewer-recording\";\n  }\n\n  const chunks = [];\n  await chunksStore.iterate((value) => {\n    chunks.push(value);\n  });\n\n  if (chunks.length === 0) {\n    return;\n  }\n\n  // Create the editor tab\n  chrome.tabs.create(\n    {\n      url: editorUrl,\n      active: true,\n    },\n    async (tab) => {\n      // Save the tab ID in local storage\n      chrome.storage.local.set({ sandboxTab: tab.id });\n\n      // Wait for the tab to load before sending messages\n      await new Promise((resolve) => {\n        chrome.tabs.onUpdated.addListener(function listener(tabId, info) {\n          if (info.status === \"complete\" && tabId === tab.id) {\n            chrome.tabs.onUpdated.removeListener(listener);\n\n            setTimeout(() => {\n              sendMessageTab(tab.id, {\n                type: messageType,\n              });\n              resolve();\n            }, 2000);\n          }\n        });\n      });\n    }\n  );\n};\n"
  },
  {
    "path": "src/pages/Background/recording/sendChunks.js",
    "content": "import { chunksStore } from \"./chunkHandler\";\nimport { handleChunks } from \"./chunkHandler\";\nimport { diagEvent } from \"../../utils/diagnosticLog\";\n\nexport const sendChunks = async (override = false, target = null) => {\n  try {\n    // Wait briefly for chunks to be flushed to IndexedDB\n    const maxAttempts = 50;\n    const delayMs = 200;\n    let chunkCount = 0;\n    for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n      chunkCount = 0;\n      await chunksStore.iterate(() => {\n        chunkCount += 1;\n      });\n      console.debug(\"[Screenity][BG] sendChunks: chunkCount check\", {\n        attempt,\n        chunkCount,\n      });\n      if (chunkCount > 0) break;\n      await new Promise((r) => setTimeout(r, delayMs));\n    }\n\n    if (chunkCount === 0) {\n      console.warn(\n        \"[Screenity][BG] sendChunks: no chunks available after waiting\",\n      );\n      try {\n        await chrome.storage.local.set({\n          lastChunkSendFailure: {\n            ts: Date.now(),\n            why: \"no-chunks-after-wait\",\n            override,\n            targetTabId: target?.tabId ?? null,\n          },\n        });\n      } catch {}\n      diagEvent(\"chunks-fail\", { why: \"no-chunks-after-wait\" });\n      return { status: \"empty\", chunkCount: 0 };\n    }\n\n    const chunks = [];\n    await chunksStore.iterate((value) => {\n      chunks.push(value);\n    });\n    console.debug(\"[Screenity][BG] sendChunks: collected chunks\", {\n      count: chunks.length,\n    });\n    // Await handleChunks to ensure messaging completes before returning\n    await handleChunks(chunks, override, target);\n    diagEvent(\"chunks-sent\", { count: chunks.length });\n    return { status: \"ok\", chunkCount: chunks.length };\n  } catch (error) {\n    console.error(\"Failed to send chunks. Reloading extension.\", error);\n    chrome.runtime.reload();\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/recording/sendMessageRecord.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\n\nexport const sendMessageRecord = (message, responseCallback = null) => {\n  return new Promise((resolve, reject) => {\n    chrome.storage.local.get([\"recordingTab\", \"offscreen\"], (result) => {\n      if (chrome.runtime.lastError) {\n        console.warn(\n          \"sendMessageRecord: storage error\",\n          chrome.runtime.lastError.message\n        );\n        return reject(chrome.runtime.lastError.message);\n      }\n\n      if (result.offscreen) {\n        chrome.runtime.sendMessage(message, (response) => {\n          if (chrome.runtime.lastError) {\n            reject(chrome.runtime.lastError.message);\n          } else {\n            responseCallback ? responseCallback(response) : resolve(response);\n          }\n        });\n      } else if (result.recordingTab) {\n        sendMessageTab(result.recordingTab, message, responseCallback)\n          .then(resolve)\n          .catch((err) => {\n            const errStr = String(err);\n            const isDeadTab =\n              errStr.includes(\"Receiving end does not exist\") ||\n              errStr.includes(\"No tab with id\");\n            console.warn(\n              `sendMessageRecord: failed to message recordingTab ${result.recordingTab}${isDeadTab ? \" (stale/dead tab)\" : \"\"}`,\n              err\n            );\n            reject(err);\n          });\n      } else {\n        // No recordingTab set - check if there's an active recorderSession\n        // This can happen if the service worker restarted and lost in-memory state\n        chrome.storage.local.get([\"recorderSession\"], (sessionResult) => {\n          const recorderTabId =\n            sessionResult.recorderSession?.recorderTabId ||\n            sessionResult.recorderSession?.tabId ||\n            null;\n          if (sessionResult.recorderSession && recorderTabId) {\n            // Try the tab from the persisted session\n            sendMessageTab(recorderTabId, message, responseCallback)\n              .then(resolve)\n              .catch(reject);\n          } else {\n            console.warn(\n              \"sendMessageRecord: no recordingTab or recorderSession available\"\n            );\n            reject(new Error(\"No recording tab available\"));\n          }\n        });\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/recording/startRecording.js",
    "content": "import { sendMessageRecord } from \"./sendMessageRecord\";\nimport { sendMessageTab } from \"../tabManagement\";\nimport { initDiagSession, diagEvent } from \"../../utils/diagnosticLog\";\nimport { makeRecordingAttemptId } from \"../../utils/errorCodes\";\n\nexport const startRecording = async () => {\n  // Correlation ID for this recording attempt\n  const recordingAttemptId = makeRecordingAttemptId();\n  chrome.storage.local.set({\n    restarting: false,\n    recordingAttemptId,\n  });\n\n  const { activeTab, recordingUiTabId } = await chrome.storage.local.get([\n    \"activeTab\",\n    \"recordingUiTabId\",\n  ]);\n  if (recordingUiTabId != null) {\n    chrome.storage.local.set({ recordingUiTabId });\n  } else if (activeTab != null) {\n    chrome.storage.local.set({ recordingUiTabId: activeTab });\n  }\n\n  // Check if customRegion is set\n  const { customRegion } = await chrome.storage.local.get([\"customRegion\"]);\n\n  const { recordingType } = await chrome.storage.local.get([\"recordingType\"]);\n\n  if (recordingType === \"region\" || recordingType === \"tab\") {\n    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {\n      const tab = tabs[0];\n      if (tab) {\n        const title = tab.title || \"\";\n        const url = tab.url || \"\";\n        chrome.storage.local.set({\n          recordingMeta: {\n            type: \"tab\",\n            title,\n            url,\n            startedAt: Date.now(),\n          },\n        });\n      }\n\n      if (tab && tab.url) {\n        try {\n          const url = new URL(tab.url);\n          let hostname = url.hostname;\n\n          if (hostname.startsWith(\"www.\")) {\n            hostname = hostname.slice(4);\n          }\n\n          chrome.storage.local.set({ recordedTabDomain: hostname });\n        } catch (e) {\n          console.warn(\"Could not parse tab URL for domain:\", e);\n        }\n      }\n    });\n  } else {\n    chrome.storage.local.remove([\"recordingMeta\"]);\n  }\n\n  chrome.storage.local.set({ lastRecordingType: recordingType || \"screen\" });\n\n  // Initialize diagnostic session for this recording\n  const { quality, systemAudio, audioInput, backup, offscreen, alarm, alarmTime, countdown } =\n    await chrome.storage.local.get([\n      \"quality\",\n      \"systemAudio\",\n      \"audioInput\",\n      \"backup\",\n      \"offscreen\",\n      \"alarm\",\n      \"alarmTime\",\n      \"countdown\",\n    ]);\n  await initDiagSession({\n    recordingAttemptId,\n    recordingType: recordingType || \"screen\",\n    quality: quality || null,\n    region: Boolean(customRegion),\n    systemAudio: Boolean(systemAudio),\n    audioInput: Boolean(audioInput),\n    backup: Boolean(backup),\n    offscreen: Boolean(offscreen),\n    alarm: Boolean(alarm),\n    alarmTime: alarm ? (alarmTime || null) : null,\n    countdown: Boolean(countdown),\n  });\n  // Log session-start immediately (not inside the async .then() callback) so\n  // the event is flushed to storage before the SW can be killed.\n  diagEvent(\"session-start\", { region: Boolean(customRegion) });\n\n  // If recordingTab points to a dead tab left over from a previous recording,\n  // clear it so sendMessageRecord doesn't try to message it.\n  const { recordingTab: prevRecTab } = await chrome.storage.local.get([\n    \"recordingTab\",\n  ]);\n  if (prevRecTab != null) {\n    try {\n      await chrome.tabs.get(prevRecTab);\n      // Tab is alive — this is the current recorder tab, leave it alone.\n    } catch {\n      // Tab doesn't exist — stale reference from a previous recording.\n      diagEvent(\"stale-recording-tab-cleared\", { prevRecTab });\n      chrome.storage.local.set({ recordingTab: null });\n    }\n  }\n\n  const startMsg = customRegion\n    ? { type: \"start-recording-tab\", region: true }\n    : { type: \"start-recording-tab\" };\n\n  sendMessageRecord(startMsg).catch((err) => {\n    const errStr = String(err).slice(0, 120);\n    const isStaleTab =\n      errStr.includes(\"Receiving end does not exist\") ||\n      errStr.includes(\"No tab with id\") ||\n      errStr.includes(\"No recording tab available\");\n    diagEvent(\"start-fail\", {\n      region: Boolean(customRegion),\n      error: errStr,\n      staleTab: isStaleTab,\n    });\n    // Notify the user that the recording failed to start\n    chrome.storage.local.get([\"activeTab\"], ({ activeTab }) => {\n      if (activeTab) {\n        sendMessageTab(activeTab, {\n          type: \"recording-error\",\n          error: \"start-failed\",\n          why: isStaleTab\n            ? \"stale-recorder-tab\"\n            : \"recorder-tab-unavailable\",\n        }).catch(() => {});\n      }\n    });\n    chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n  });\n  chrome.action.setIcon({ path: \"assets/recording-logo.png\" });\n  // Set up alarm if set in storage\n  if (alarm) {\n    const seconds = parseFloat(alarmTime);\n    chrome.alarms.create(\"recording-alarm\", { delayInMinutes: seconds / 60 });\n  }\n};\n\nexport const startAfterCountdown = () => {\n  chrome.storage.local.get([\"recordingTab\", \"offscreen\"], (result) => {\n    if (chrome.runtime.lastError) {\n      console.error(\n        \"Failed to start after countdown:\",\n        chrome.runtime.lastError\n      );\n      return;\n    }\n\n    const { recordingTab, offscreen } = result || {};\n    chrome.storage.local.set({\n      lastStartAfterCountdown: {\n        ts: Date.now(),\n        recordingTab: recordingTab ?? null,\n        offscreen: Boolean(offscreen),\n      },\n    });\n\n    // Some flows can start before recordingTab is persisted. Start anyway and\n    // let sendMessageRecord route via recorderSession/offscreen fallback.\n    if (recordingTab === null && !offscreen) {\n      console.warn(\n        \"[Screenity] startAfterCountdown: no recordingTab/offscreen available, starting with fallback routing\"\n      );\n    }\n    startRecording();\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/recording/stopRecording.js",
    "content": "import { focusTab, sendMessageTab } from \"../tabManagement/\";\nimport { discardOffscreenDocuments } from \"../offscreen/discardOffscreenDocuments\";\nimport { sendMessageRecord } from \"./sendMessageRecord\";\nimport { sendChunks } from \"./sendChunks\";\nimport { waitForContentScript } from \"../utils/waitForContentScript\";\nimport { diagEvent, endDiagSession } from \"../../utils/diagnosticLog\";\n\nconst acquirePostStopEditorLock = async (recordingId = null) => {\n  const { postStopEditorOpening } = await chrome.storage.local.get([\n    \"postStopEditorOpening\",\n  ]);\n  if (postStopEditorOpening) return false;\n  await chrome.storage.local.set({\n    postStopEditorOpening: true,\n    postStopEditorOpened: true,\n    postStopRecordingId: recordingId,\n  });\n  return true;\n};\n\nconst releasePostStopEditorLock = async (overrides = {}) => {\n  // Only clear the \"in-progress\" flag. Keep postStopEditorOpened=true so\n  // stopRecording() (which runs later) still knows an editor was opened and\n  // won't open a duplicate. stopRecording() clears postStopEditorOpened itself.\n  await chrome.storage.local.set({\n    postStopEditorOpening: false,\n    ...overrides,\n  });\n};\n\n/** Store a recovery URL and toast the user when the editor tab fails to open. */\nconst handleEditorOpenFailed = async (editorUrl, lastError) => {\n  diagEvent(\"editor-open-failed\", {\n    editorUrl,\n    lastError: lastError || \"tab-create-failed\",\n  });\n  await chrome.storage.local.set({\n    editorRecoveryUrl: editorUrl,\n    editorRecoveryAt: Date.now(),\n  });\n  // Toast the active tab\n  try {\n    const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n    if (activeTab) {\n      sendMessageTab(activeTab, {\n        type: \"show-toast\",\n        message: chrome.i18n.getMessage(\"editorRecoveryToast\"),\n        timeout: 12000,\n      }).catch(() => {});\n    }\n  } catch (_) {\n    // Recovery URL is still in storage either way\n  }\n};\n\nexport const stopRecording = async () => {\n  chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n  chrome.storage.local.set({ restarting: false });\n  const { recordingStartTime, isSubscribed, paused, pausedAt, totalPausedMs, recordingDuration: storedDuration } =\n    await chrome.storage.local.get([\n      \"recordingStartTime\",\n      \"isSubscribed\",\n      \"paused\",\n      \"pausedAt\",\n      \"totalPausedMs\",\n      \"recordingDuration\",\n    ]);\n\n  const startTime = Number(recordingStartTime);\n  const now = Date.now();\n  const basePaused = Number(totalPausedMs) || 0;\n  const pausedAtMs = Number(pausedAt);\n  const extraPaused =\n    paused && Number.isFinite(pausedAtMs) && pausedAtMs > 0\n      ? Math.max(0, now - pausedAtMs)\n      : 0;\n\n  let duration =\n    Number.isFinite(startTime) && startTime > 0\n      ? Math.max(0, now - startTime - basePaused - extraPaused)\n      : 0;\n\n  // Recorder clears recordingStartTime before stopRecording() runs via\n  // videoReady(), so the computed duration can be 0 for valid recordings.\n  // Fall back to the previously stored recordingDuration if available.\n  if (duration === 0 && Number(storedDuration) > 0) {\n    duration = Number(storedDuration);\n  }\n  const maxDuration = 7 * 60 * 1000;\n\n  diagEvent(\"stop\", { duration, isSubscribed: Boolean(isSubscribed) });\n\n  chrome.storage.local.set({\n    recording: false,\n    recordingDuration: duration,\n    tabRecordedID: null,\n    offscreen: false,\n  });\n  chrome.storage.local.set({ recordingUiTabId: null });\n  chrome.storage.local.set({ pipForceClose: Date.now() });\n\n  chrome.storage.local.set({ recordingStartTime: 0 });\n\n  const {\n    fastRecorderInUse,\n    fastRecorderActiveRecordingId,\n    fastRecorderValidation,\n  } = await chrome.storage.local.get([\n    \"fastRecorderInUse\",\n    \"fastRecorderActiveRecordingId\",\n    \"fastRecorderValidation\",\n  ]);\n  const validation =\n    fastRecorderValidation && typeof fastRecorderValidation === \"object\"\n      ? fastRecorderValidation\n      : null;\n  const validationMatches =\n    validation?.details?.recordingId &&\n    validation.details.recordingId === fastRecorderActiveRecordingId;\n  const hardFailForCurrent = Boolean(validationMatches && validation?.hardFail);\n  //const hasWebCodecs = supportsWebCodecs();\n  const hasWebCodecs = Boolean(fastRecorderInUse) && !hardFailForCurrent;\n\n  const {\n    postStopEditorOpened,\n    postStopEditorOpening,\n    sandboxTab,\n    postStopRecordingId,\n    recordingTab,\n  } = await chrome.storage.local.get([\n    \"postStopEditorOpened\",\n    \"postStopEditorOpening\",\n    \"sandboxTab\",\n    \"postStopRecordingId\",\n    \"recordingTab\",\n  ]);\n\n  if (isSubscribed) {\n    chrome.alarms.clear(\"recording-alarm\");\n    discardOffscreenDocuments();\n    chrome.storage.local.remove([\"recordingMeta\"]);\n  } else if (postStopEditorOpening || postStopEditorOpened) {\n    // Editor already opened by stop-recording-tab flow; avoid opening a second tab.\n    // That flow will trigger processing when the tab finishes loading.\n  } else if (hasWebCodecs) {\n    diagEvent(\"editor-open\", { type: \"editorwebcodecs\" });\n    // Use Mediabunny (editorwebcodecs.html) when WebCodecs is supported\n    const query = postStopRecordingId\n      ? `?mode=postStop&recordingId=${encodeURIComponent(postStopRecordingId)}`\n      : \"?mode=postStop\";\n    const wcUrl = `editorwebcodecs.html${query}`;\n    chrome.tabs.create(\n      { url: wcUrl, active: true },\n      (tab) => {\n        if (chrome.runtime.lastError || !tab?.id) {\n          handleEditorOpenFailed(wcUrl, chrome.runtime.lastError?.message);\n          return;\n        }\n        chrome.tabs.onUpdated.addListener(function _(\n          tabId,\n          changeInfo,\n          updatedTab,\n        ) {\n          if (tabId === tab.id && changeInfo.status === \"complete\") {\n            chrome.tabs.onUpdated.removeListener(_);\n            chrome.storage.local.set({ sandboxTab: tab.id });\n            (async () => {\n              try {\n                await waitForContentScript(tab.id);\n                await sendMessageTab(tab.id, { type: \"fallback-recording\" });\n              } catch (err) {\n                console.error(\"❌ Failed to wait/send to content script:\", err);\n              }\n            })();\n          }\n        });\n      },\n    );\n\n    chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n  } else if (duration > maxDuration) {\n    diagEvent(\"editor-open\", { type: \"editorviewer\", duration });\n    // Fallback for large recordings without WebCodecs - use viewer mode\n\n    const query = postStopRecordingId\n      ? `?mode=postStop&recordingId=${encodeURIComponent(postStopRecordingId)}`\n      : \"?mode=postStop\";\n    const viewerUrl = `editorviewer.html${query}`;\n    chrome.tabs.create(\n      { url: viewerUrl, active: true },\n      (tab) => {\n        if (chrome.runtime.lastError || !tab?.id) {\n          handleEditorOpenFailed(viewerUrl, chrome.runtime.lastError?.message);\n          return;\n        }\n        chrome.tabs.onUpdated.addListener(function _(\n          tabId,\n          changeInfo,\n          updatedTab,\n        ) {\n          if (tabId === tab.id && changeInfo.status === \"complete\") {\n            chrome.tabs.onUpdated.removeListener(_);\n            chrome.storage.local.set({ sandboxTab: tab.id });\n            (async () => {\n              try {\n                await waitForContentScript(tab.id);\n                await sendMessageTab(tab.id, { type: \"viewer-recording\" });\n              } catch (err) {\n                console.error(\"❌ Failed to wait/send to content script:\", err);\n              }\n            })();\n          }\n        });\n      },\n    );\n\n    chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n  } else {\n    diagEvent(\"editor-open\", { type: \"editor-ffmpeg\" });\n    // Use FFmpeg (editor.html) for browsers without WebCodecs\n    const query = postStopRecordingId\n      ? `?mode=postStop&recordingId=${encodeURIComponent(postStopRecordingId)}`\n      : \"?mode=postStop\";\n    const ffmpegUrl = `editor.html${query}`;\n    chrome.tabs.create({ url: ffmpegUrl, active: true }, (tab) => {\n      if (chrome.runtime.lastError || !tab?.id) {\n        handleEditorOpenFailed(ffmpegUrl, chrome.runtime.lastError?.message);\n        return;\n      }\n      chrome.tabs.onUpdated.addListener(function _(\n        tabId,\n        changeInfo,\n        updatedTab,\n      ) {\n        if (tabId === tab.id && changeInfo.status === \"complete\") {\n          chrome.tabs.onUpdated.removeListener(_);\n          chrome.storage.local.set({ sandboxTab: tab.id });\n          (async () => {\n            try {\n              await waitForContentScript(tab.id);\n              await sendMessageTab(tab.id, { type: \"fallback-recording\" });\n            } catch (err) {\n              console.error(\"❌ Failed to wait/send to content script:\", err);\n            }\n          })();\n        }\n      });\n    });\n\n    chrome.runtime.sendMessage({ type: \"turn-off-pip\" });\n  }\n\n  // NOTE: Do NOT clear recordingTab here. The pinned recorder tab may still\n  // be alive and is cleaned up by:\n  //  - handleRecordingComplete() when the editor finishes processing\n  //  - onTabRemoved when the user closes the editor (which closes the recorder tab)\n  //  - openRecorderTab() as a safety net before creating a new recorder tab\n  // Clearing the reference here would orphan the tab because later cleanup\n  // relies on the reference to find and close it.\n\n  // End the diagnostic session for all paths (subscriber, free webcodecs,\n  // free viewer, free ffmpeg, and already-opened-editor).  Error/crash paths\n  // end the session separately via their own endDiagSession calls.\n  endDiagSession(\"ok\");\n\n  const { wasRegion } = await chrome.storage.local.get([\"wasRegion\"]);\n  if (wasRegion) {\n    chrome.storage.local.set({ wasRegion: false, region: true });\n  }\n\n  // Clear the \"editor was opened\" flag so it doesn't bleed into the next\n  // recording session. handleStopRecordingTab sets this; releasePostStopEditorLock\n  // intentionally preserves it so we can detect duplicates above.\n  chrome.storage.local.set({ postStopEditorOpened: false });\n\n  chrome.alarms.clear(\"recording-alarm\");\n  discardOffscreenDocuments();\n};\n\nexport const handleStopRecordingTab = async (request) => {\n  chrome.action.setIcon({ path: \"assets/icon-34.png\" });\n  if (request.memoryError) {\n    diagEvent(\"error\", {\n      type: \"memory-error\",\n      reason: request.reason || null,\n      savedChunks: request.savedChunks ?? null,\n    });\n    chrome.storage.local.set({\n      recording: false,\n      restarting: false,\n      tabRecordedID: null,\n      memoryError: true,\n    });\n  }\n\n  const {\n    isSubscribed,\n    recordingStartTime,\n    paused,\n    pausedAt,\n    totalPausedMs,\n    fastRecorderInUse,\n  } = await chrome.storage.local.get([\n    \"isSubscribed\",\n    \"recordingStartTime\",\n    \"paused\",\n    \"pausedAt\",\n    \"totalPausedMs\",\n    \"fastRecorderInUse\",\n  ]);\n  const stopTabNow = Date.now();\n  const stopTabStartTime = Number(recordingStartTime);\n  const stopTabBasePaused = Number(totalPausedMs) || 0;\n  const stopTabPausedAtMs = Number(pausedAt);\n  const stopTabExtraPaused =\n    paused && Number.isFinite(stopTabPausedAtMs) && stopTabPausedAtMs > 0\n      ? Math.max(0, stopTabNow - stopTabPausedAtMs)\n      : 0;\n  const stopTabDuration =\n    Number.isFinite(stopTabStartTime) && stopTabStartTime > 0\n      ? Math.max(0, stopTabNow - stopTabStartTime - stopTabBasePaused - stopTabExtraPaused)\n      : 0;\n  diagEvent(\"stop-tab\", { duration: stopTabDuration, reason: request.reason || null, memoryError: Boolean(request.memoryError), fastRecorderInUse: Boolean(fastRecorderInUse) });\n\n  if (stopTabDuration > 0) {\n    chrome.storage.local.set({ recordingDuration: stopTabDuration });\n  }\n\n  if (!isSubscribed) {\n    const { fastRecorderActiveRecordingId } = await chrome.storage.local.get([\n      \"fastRecorderActiveRecordingId\",\n    ]);\n    const recordingId =\n      fastRecorderActiveRecordingId ||\n      `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;\n    const now = Date.now();\n    const startTime = Number(recordingStartTime);\n    const basePaused = Number(totalPausedMs) || 0;\n    const pausedAtMs = Number(pausedAt);\n    const extraPaused =\n      paused && Number.isFinite(pausedAtMs) && pausedAtMs > 0\n        ? Math.max(0, now - pausedAtMs)\n        : 0;\n    const duration =\n      Number.isFinite(startTime) && startTime > 0\n        ? Math.max(0, now - startTime - basePaused - extraPaused)\n        : 0;\n    const maxDuration = 7 * 60 * 1000;\n    const lockAcquired = await acquirePostStopEditorLock(recordingId);\n    if (!lockAcquired) {\n      console.warn(\n        \"[Screenity][BG] Duplicate stop-recording-tab suppressed (editor opening)\",\n      );\n      sendMessageRecord({ type: \"stop-recording-tab\" });\n      return;\n    }\n\n    if (fastRecorderInUse) {\n      diagEvent(\"editor-open\", { type: \"editorwebcodecs\", via: \"stop-tab\" });\n      const editorUrl = \"editorwebcodecs.html\";\n      // Open editor immediately in postStop mode (WebCodecs only)\n      chrome.tabs.create(\n        {\n          url: `${editorUrl}?mode=postStop&recordingId=${encodeURIComponent(\n            recordingId,\n          )}`,\n          active: true,\n        },\n        (tab) => {\n          if (chrome.runtime.lastError || !tab?.id) {\n            const errMsg = chrome.runtime.lastError?.message || \"tab-create-failed\";\n            console.error(\"❌ Failed to open post-stop editor:\", errMsg);\n            releasePostStopEditorLock({ postStopRecordingId: null });\n            const fullUrl = `${editorUrl}?mode=postStop&recordingId=${encodeURIComponent(recordingId)}`;\n            handleEditorOpenFailed(fullUrl, errMsg);\n            return;\n          }\n          let settled = false;\n          const safetyTimer = setTimeout(() => {\n            if (!settled) {\n              settled = true;\n              console.warn(\"[Screenity][BG] Editor tab load timed out — releasing lock\");\n              diagEvent(\"editor-open-timeout\", { tabId: tab.id, type: \"editorwebcodecs\" });\n              releasePostStopEditorLock();\n            }\n          }, 15000);\n          chrome.tabs.onUpdated.addListener(function _(\n            tabId,\n            changeInfo,\n            updatedTab,\n          ) {\n            if (tabId === tab.id && changeInfo.status === \"complete\") {\n              chrome.tabs.onUpdated.removeListener(_);\n              if (settled) return;\n              settled = true;\n              clearTimeout(safetyTimer);\n              chrome.storage.local.set({ sandboxTab: tab.id });\n              releasePostStopEditorLock();\n              sendMessageTab(tab.id, { type: \"fallback-recording\" }).catch(\n                (err) => {\n                  console.error(\n                    \"❌ Failed to send message:\",\n                    err?.message || err,\n                  );\n                },\n              );\n            }\n          });\n        },\n      );\n    } else {\n      const editorUrl =\n        duration > maxDuration ? \"editorviewer.html\" : \"editor.html\";\n      diagEvent(\"editor-open\", { type: editorUrl.replace(\".html\", \"\"), via: \"stop-tab\", duration });\n      // MediaRecorder/FFmpeg path: open editor normally (no postStop gating)\n      chrome.tabs.create({ url: editorUrl, active: true }, (tab) => {\n        if (chrome.runtime.lastError || !tab?.id) {\n          const errMsg = chrome.runtime.lastError?.message || \"tab-create-failed\";\n          console.error(\"❌ Failed to open post-stop editor:\", errMsg);\n          releasePostStopEditorLock({ postStopRecordingId: null });\n          handleEditorOpenFailed(editorUrl, errMsg);\n          return;\n        }\n        let settled = false;\n        const safetyTimer = setTimeout(() => {\n          if (!settled) {\n            settled = true;\n            console.warn(\"[Screenity][BG] Editor tab load timed out — releasing lock\");\n            diagEvent(\"editor-open-timeout\", { tabId: tab.id, type: editorUrl });\n            releasePostStopEditorLock({ postStopRecordingId: null });\n          }\n        }, 15000);\n        chrome.tabs.onUpdated.addListener(function _(\n          tabId,\n          changeInfo,\n          updatedTab,\n        ) {\n          if (tabId === tab.id && changeInfo.status === \"complete\") {\n            chrome.tabs.onUpdated.removeListener(_);\n            if (settled) return;\n            settled = true;\n            clearTimeout(safetyTimer);\n            chrome.storage.local.set({\n              sandboxTab: tab.id,\n              postStopRecordingId: null,\n            });\n            releasePostStopEditorLock({ postStopRecordingId: null });\n            if (editorUrl === \"editorviewer.html\") {\n              sendMessageTab(tab.id, { type: \"viewer-recording\" }).catch(\n                (err) => {\n                  console.error(\n                    \"❌ Failed to send message:\",\n                    err?.message || err,\n                  );\n                },\n              );\n            } else {\n              (async () => {\n                let sent = false;\n                for (let i = 0; i < 6; i += 1) {\n                  // eslint-disable-next-line no-await-in-loop\n                  const result = await sendChunks();\n                  if (result?.status === \"ok\") {\n                    sent = true;\n                    break;\n                  }\n                  // eslint-disable-next-line no-await-in-loop\n                  await new Promise((resolve) => setTimeout(resolve, 1000));\n                }\n                if (!sent) {\n                  console.warn(\n                    \"[Screenity][BG] editor opened but chunks are still unavailable\",\n                  );\n                  try {\n                    await chrome.storage.local.set({\n                      lastChunkSendFailure: {\n                        ts: Date.now(),\n                        why: \"editor-opened-no-chunks\",\n                        targetTabId: tab.id,\n                      },\n                    });\n                  } catch {}\n                }\n              })();\n            }\n          }\n        });\n      });\n    }\n  }\n\n  // Send stop to recorder; toast if the recorder tab doesn't ack.\n  (async () => {\n    const STOP_ACK_TIMEOUT_MS = 3000;\n    try {\n      const ack = await Promise.race([\n        sendMessageRecord({ type: \"stop-recording-tab\" }),\n        new Promise((_, reject) =>\n          setTimeout(() => reject(new Error(\"stop-ack-timeout\")), STOP_ACK_TIMEOUT_MS)\n        ),\n      ]);\n      if (!ack || ack.ok !== true) throw new Error(\"stop-no-ack\");\n    } catch (err) {\n      diagEvent(\"stop-ack-failed\", { error: String(err).slice(0, 120) });\n      try {\n        const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n        if (activeTab) {\n          sendMessageTab(activeTab, {\n            type: \"show-toast\",\n            message: chrome.i18n.getMessage(\"stopAckTimeoutToast\"),\n            timeout: 8000,\n          }).catch(() => {});\n        }\n      } catch (_) {\n        // Toast failed — non-fatal\n      }\n    }\n  })();\n};\n\nexport const handleStopRecordingTabBackup = async (request) => {\n  chrome.storage.local.set({\n    recording: false,\n    restarting: false,\n    tabRecordedID: null,\n    memoryError: true,\n  });\n  sendMessageRecord({ type: \"stop-recording-tab\" });\n\n  const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n\n  sendMessageTab(activeTab, { type: \"stop-pending\" });\n  focusTab(activeTab);\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/createTab.js",
    "content": "export const createTab = async (url, translate = false, active = false) => {\n  if (!url) return;\n\n  if (translate) {\n    const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n    if (!locale.includes(\"en\")) {\n      url =\n        \"http://translate.google.com/translate?js=n&sl=auto&tl=\" +\n        locale +\n        \"&u=\" +\n        url;\n    }\n  }\n\n  return new Promise((resolve) => {\n    chrome.tabs.create(\n      {\n        url: url,\n        active: active,\n      },\n      (tab) => {\n        resolve(tab);\n      }\n    );\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/editorTab.js",
    "content": "import { createTab } from \"./createTab\";\nimport { focusTab } from \"./focusTab\";\n\nconst APP_BASE_URL = process.env.SCREENITY_APP_BASE;\nconst EDITOR_TAB_META_KEY = \"editorTabMeta\";\n\nconst toUrl = (value) => {\n  if (!value || typeof value !== \"string\") return null;\n  try {\n    return new URL(value);\n  } catch {\n    return null;\n  }\n};\n\nconst getAppOrigin = () => {\n  const appUrl = toUrl(APP_BASE_URL);\n  return appUrl?.origin || null;\n};\n\nexport const parseEditorTargetUrl = (rawUrl) => {\n  const url = toUrl(rawUrl);\n  if (!url) return null;\n\n  const appOrigin = getAppOrigin();\n  if (!appOrigin || url.origin !== appOrigin) return null;\n\n  const editorMatch = url.pathname.match(/^\\/editor\\/([^/]+)\\/edit\\/?$/);\n  if (editorMatch?.[1]) {\n    return {\n      kind: \"editor\",\n      projectId: editorMatch[1],\n      origin: url.origin,\n      href: url.href,\n    };\n  }\n\n  const viewMatch = url.pathname.match(/^\\/view\\/([^/]+)\\/?$/);\n  if (viewMatch?.[1]) {\n    return {\n      kind: \"view\",\n      projectId: viewMatch[1],\n      origin: url.origin,\n      href: url.href,\n    };\n  }\n\n  return null;\n};\n\nconst getTabUrl = (tab) => tab?.pendingUrl || tab?.url || null;\n\nconst getTabById = async (tabId) => {\n  if (!Number.isInteger(tabId)) return null;\n  try {\n    return await chrome.tabs.get(tabId);\n  } catch {\n    return null;\n  }\n};\n\nexport const clearEditorTabReference = async (reason = \"unknown\", extra = {}) => {\n  await chrome.storage.local.set({\n    editorTab: null,\n    [EDITOR_TAB_META_KEY]: null,\n  });\n  console.info(\"[Screenity][BG] Cleared editorTab reference\", {\n    reason,\n    ...extra,\n  });\n};\n\nexport const setEditorTabReference = async ({\n  tabId,\n  tabUrl,\n  source = \"unknown\",\n  expectedProjectId = null,\n}) => {\n  if (!Number.isInteger(tabId)) {\n    await clearEditorTabReference(\"set-editor-tab-invalid-id\", {\n      source,\n      tabId,\n    });\n    return null;\n  }\n\n  const parsed = parseEditorTargetUrl(tabUrl);\n  if (!parsed) {\n    await clearEditorTabReference(\"set-editor-tab-invalid-url\", {\n      source,\n      tabId,\n      tabUrl,\n    });\n    return null;\n  }\n\n  if (expectedProjectId && expectedProjectId !== parsed.projectId) {\n    await clearEditorTabReference(\"set-editor-tab-project-mismatch\", {\n      source,\n      tabId,\n      expectedProjectId,\n      parsedProjectId: parsed.projectId,\n      tabUrl,\n    });\n    return null;\n  }\n\n  const meta = {\n    source,\n    setAt: Date.now(),\n    projectId: parsed.projectId,\n    kind: parsed.kind,\n    origin: parsed.origin,\n    url: parsed.href,\n  };\n\n  await chrome.storage.local.set({\n    editorTab: tabId,\n    [EDITOR_TAB_META_KEY]: meta,\n  });\n\n  console.info(\"[Screenity][BG] Stored editorTab reference\", {\n    tabId,\n    ...meta,\n  });\n\n  return { tabId, meta };\n};\n\nexport const getValidatedEditorTab = async ({\n  expectedProjectId = null,\n  expectedKind = null,\n  reason = \"unknown\",\n}) => {\n  const { editorTab, editorTabMeta } = await chrome.storage.local.get([\n    \"editorTab\",\n    EDITOR_TAB_META_KEY,\n  ]);\n\n  if (!Number.isInteger(editorTab)) {\n    return { ok: false, reason: \"missing-tab-id\" };\n  }\n\n  const tab = await getTabById(editorTab);\n  if (!tab?.id) {\n    await clearEditorTabReference(\"validate-editor-tab-missing-tab\", {\n      reason,\n      editorTab,\n    });\n    return { ok: false, reason: \"tab-not-found\" };\n  }\n\n  const tabUrl = getTabUrl(tab);\n  const parsedUrl = parseEditorTargetUrl(tabUrl);\n  if (!parsedUrl) {\n    await clearEditorTabReference(\"validate-editor-tab-not-editor-url\", {\n      reason,\n      editorTab,\n      tabUrl,\n    });\n    return { ok: false, reason: \"tab-not-editor-url\" };\n  }\n\n  if (expectedProjectId && parsedUrl.projectId !== expectedProjectId) {\n    await clearEditorTabReference(\"validate-editor-tab-project-mismatch\", {\n      reason,\n      editorTab,\n      expectedProjectId,\n      parsedProjectId: parsedUrl.projectId,\n      tabUrl,\n    });\n    return { ok: false, reason: \"project-mismatch\" };\n  }\n\n  if (expectedKind && parsedUrl.kind !== expectedKind) {\n    await clearEditorTabReference(\"validate-editor-tab-kind-mismatch\", {\n      reason,\n      editorTab,\n      expectedKind,\n      parsedKind: parsedUrl.kind,\n      tabUrl,\n    });\n    return { ok: false, reason: \"kind-mismatch\" };\n  }\n\n  if (editorTabMeta?.projectId && editorTabMeta.projectId !== parsedUrl.projectId) {\n    await clearEditorTabReference(\"validate-editor-tab-meta-project-mismatch\", {\n      reason,\n      editorTab,\n      metaProjectId: editorTabMeta.projectId,\n      parsedProjectId: parsedUrl.projectId,\n      tabUrl,\n    });\n    return { ok: false, reason: \"meta-project-mismatch\" };\n  }\n\n  if (editorTabMeta?.kind && editorTabMeta.kind !== parsedUrl.kind) {\n    await clearEditorTabReference(\"validate-editor-tab-meta-kind-mismatch\", {\n      reason,\n      editorTab,\n      metaKind: editorTabMeta.kind,\n      parsedKind: parsedUrl.kind,\n      tabUrl,\n    });\n    return { ok: false, reason: \"meta-kind-mismatch\" };\n  }\n\n  return {\n    ok: true,\n    tab,\n    tabUrl,\n    parsedUrl,\n    meta: editorTabMeta || null,\n  };\n};\n\nexport const resolveEditorTabForTarget = async ({\n  targetUrl,\n  expectedProjectId = null,\n  expectedKind = null,\n  reason = \"unknown\",\n}) => {\n  const parsedTarget = parseEditorTargetUrl(targetUrl);\n  const projectId = expectedProjectId || parsedTarget?.projectId || null;\n  const kind = expectedKind || parsedTarget?.kind || null;\n\n  const existing = await getValidatedEditorTab({\n    expectedProjectId: projectId,\n    expectedKind: kind,\n    reason,\n  });\n\n  if (existing.ok && existing.tab?.id) {\n    const focused = await focusTab(existing.tab.id, {\n      reason: `${reason}:reuse`,\n      projectId,\n      kind,\n    });\n    if (focused) {\n      console.info(\"[Screenity][BG] Reusing validated editor tab\", {\n        reason,\n        tabId: existing.tab.id,\n        projectId,\n        kind,\n      });\n      return { tabId: existing.tab.id, reused: true, opened: false };\n    }\n    console.warn(\"[Screenity][BG] Failed to focus validated editor tab\", {\n      reason,\n      tabId: existing.tab.id,\n      projectId,\n      kind,\n    });\n  } else {\n    console.info(\"[Screenity][BG] Stored editor tab not reusable\", {\n      reason,\n      projectId,\n      kind,\n      validationReason: existing.reason,\n    });\n  }\n\n  if (!targetUrl) {\n    console.warn(\"[Screenity][BG] Cannot open fallback editor tab: missing URL\", {\n      reason,\n      projectId,\n      kind,\n    });\n    return { tabId: null, reused: false, opened: false };\n  }\n\n  const createdTab = await createTab(targetUrl, true, true);\n  if (!createdTab?.id) {\n    console.warn(\"[Screenity][BG] Failed to open fallback editor tab\", {\n      reason,\n      targetUrl,\n      projectId,\n      kind,\n    });\n    return { tabId: null, reused: false, opened: false };\n  }\n\n  await setEditorTabReference({\n    tabId: createdTab.id,\n    tabUrl: getTabUrl(createdTab) || targetUrl,\n    source: `fallback-open:${reason}`,\n    expectedProjectId: projectId,\n  });\n  await focusTab(createdTab.id, {\n    reason: `${reason}:opened`,\n    projectId,\n    kind,\n  });\n\n  console.info(\"[Screenity][BG] Opened fallback editor tab\", {\n    reason,\n    tabId: createdTab.id,\n    targetUrl,\n    projectId,\n    kind,\n  });\n\n  return { tabId: createdTab.id, reused: false, opened: true };\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/focusTab.js",
    "content": "export const focusTab = async (tabId, context = {}) => {\n  if (!Number.isInteger(tabId)) {\n    console.warn(\"[Screenity][BG] focusTab skipped: invalid tabId\", {\n      tabId,\n      context,\n    });\n    return false;\n  }\n\n  try {\n    const tab = await chrome.tabs.get(tabId);\n    if (!tab?.id || typeof tab.windowId !== \"number\") {\n      console.warn(\"[Screenity][BG] focusTab skipped: tab unavailable\", {\n        tabId,\n        context,\n      });\n      return false;\n    }\n\n    await chrome.windows.update(tab.windowId, { focused: true });\n    await chrome.tabs.update(tab.id, { active: true });\n    return true;\n  } catch (error) {\n    console.warn(\"[Screenity][BG] focusTab failed\", {\n      tabId,\n      context,\n      error: error?.message || String(error),\n    });\n    return false;\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/getCurrentTab.js",
    "content": "export const getCurrentTab = async () => {\n  const queryOptions = { active: true, lastFocusedWindow: true };\n  const [tab] = await chrome.tabs.query(queryOptions);\n  return tab;\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/index.js",
    "content": "import { createTab } from \"./createTab\";\nimport { getCurrentTab } from \"./getCurrentTab\";\nimport { sendMessageTab } from \"./sendMessageTab\";\nimport { removeTab } from \"./removeTab\";\nimport { focusTab } from \"./focusTab\";\nimport {\n  resetActiveTab,\n  resetActiveTabRestart,\n  restartActiveTab,\n} from \"./resetActiveTab\";\nimport { setSurface } from \"./setSurface\";\nimport {\n  parseEditorTargetUrl,\n  setEditorTabReference,\n  clearEditorTabReference,\n  getValidatedEditorTab,\n  resolveEditorTabForTarget,\n} from \"./editorTab\";\n\nexport {\n  createTab,\n  getCurrentTab,\n  sendMessageTab,\n  removeTab,\n  focusTab,\n  resetActiveTab,\n  resetActiveTabRestart,\n  restartActiveTab,\n  setSurface,\n  parseEditorTargetUrl,\n  setEditorTabReference,\n  clearEditorTabReference,\n  getValidatedEditorTab,\n  resolveEditorTabForTarget,\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/removeTab.js",
    "content": "export const removeTab = async (tabId) => {\n  if (tabId === null) return;\n\n  try {\n    const tab = await new Promise((resolve) => {\n      chrome.tabs.get(tabId, (tab) => {\n        resolve(tab);\n      });\n    });\n\n    if (tab && tab.id) {\n      chrome.tabs.remove(tab.id);\n    }\n  } catch (error) {\n    // Tab doesn't exist or can't be accessed\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/resetActiveTab.js",
    "content": "import { focusTab } from \"./focusTab\";\nimport { sendMessageTab } from \"./sendMessageTab\";\nimport { startRecording } from \"../recording/startRecording\";\nimport { getCurrentTab } from \"./getCurrentTab\";\nimport { traceStep } from \"../../utils/startFlowTrace\";\n\nexport const restartActiveTab = async (message = {}) => {\n  try {\n    const { recordingUiTabId, activeTab: storedActiveTab } =\n      await chrome.storage.local.get([\"recordingUiTabId\", \"activeTab\"]);\n    const preferredTabId =\n      message?.sourceTabId || recordingUiTabId || storedActiveTab || null;\n    const currentTab = await getCurrentTab();\n    const targetTabId = preferredTabId || currentTab?.id || null;\n    if (targetTabId) {\n      sendMessageTab(targetTabId, { type: \"ready-to-record\" });\n\n      const { countdown } = await chrome.storage.local.get([\"countdown\"]);\n\n      if (!countdown) {\n        startRecording();\n      }\n      // With countdown, content shows the UI and sends \"countdown-finished\".\n    } else {\n      console.error(\"No active tab found.\");\n    }\n  } catch (error) {\n    console.error(\"Failed to restart active tab:\", error);\n  }\n};\n\nexport const resetActiveTab = async (forceRestart = false, message = {}) => {\n  const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n  const { surface } = await chrome.storage.local.get([\"surface\"]);\n\n  if (forceRestart) {\n    return restartActiveTab(message);\n  }\n\n  const [currentTab] = await chrome.tabs.query({\n    active: true,\n    currentWindow: true,\n  });\n\n  const shouldFocusTab =\n    surface !== \"browser\" ||\n    (activeTab && (await isRestrictedDomain(activeTab)));\n\n  if (activeTab) {\n    try {\n      const tab = await chrome.tabs.get(activeTab);\n      if (!tab) {\n        console.error(\"Active tab not found.\");\n        return;\n      }\n\n      // Focus and update only if needed\n      if (shouldFocusTab) {\n        await chrome.windows.update(tab.windowId, { focused: true });\n        await chrome.tabs.update(activeTab, {\n          active: true,\n          selected: true,\n          highlighted: true,\n        });\n        await focusTab(activeTab);\n      }\n\n      // Always prefer the stored activeTab (the user's page, set before the\n      // recorder tab is created). Falling back to currentTab can target the\n      // pinned recorder tab when surface === \"browser\".\n      const targetTabId = activeTab || currentTab?.id;\n\n      // Persist routing decision to the start-flow trace. Awaited so the\n      // write lands before sendMessageTab triggers the content-side write.\n      await traceStep(\"readyToRecordSent\", {\n        routing: {\n          targetTabId,\n          activeTab,\n          currentTabId: currentTab?.id,\n          shouldFocusTab,\n        },\n        surface,\n      });\n\n      if (targetTabId) {\n        sendMessageTab(targetTabId, { type: \"ready-to-record\" }).catch(() => {});\n\n        const { countdown } = await chrome.storage.local.get([\"countdown\"]);\n\n        if (!countdown) {\n          // No countdown — start immediately.  The recorder's readiness gate\n          // will wait for the stream to be live before actually recording.\n          startRecording();\n        }\n        // With countdown: Content shows the countdown UI and sends\n        // \"countdown-finished\" when done, which triggers startAfterCountdown().\n      } else {\n        console.error(\"No valid tab to send message to.\");\n      }\n    } catch (error) {\n      console.error(\"Failed to get tab or send message:\", error);\n    }\n  } else {\n    console.error(\"No active tab ID stored.\");\n  }\n\n  async function isRestrictedDomain(tabId) {\n    try {\n      const tab = await chrome.tabs.get(tabId);\n      const url = new URL(tab.url);\n      return (\n        url.hostname.includes(\"google.com\") ||\n        url.protocol === \"chrome:\" ||\n        url.protocol === \"chrome-extension:\" ||\n        url.protocol === \"about:\"\n      );\n    } catch (e) {\n      return false;\n    }\n  }\n};\n\nexport const resetActiveTabRestart = async (message = {}) => {\n  await resetActiveTab(true, message);\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/sendMessageTab.js",
    "content": "export const sendMessageTab = async (\n  tabId,\n  message,\n  responseCallback = null,\n  noTab = null\n) => {\n  if (tabId === null || message === null)\n    return Promise.reject(\"Tab ID or message is null\");\n\n  try {\n    const tab = await new Promise((resolve, reject) => {\n      chrome.tabs.get(tabId, (tab) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError.message);\n        } else {\n          resolve(tab);\n        }\n      });\n    });\n\n    const extOrigin = chrome.runtime.getURL(\"\").replace(/\\/$/, \"\");\n    const isExtUrl = tab?.url && tab.url.startsWith(extOrigin);\n    const isPendingExtUrl =\n      tab?.pendingUrl && tab.pendingUrl.startsWith(extOrigin);\n\n    if (isExtUrl || isPendingExtUrl) {\n      return new Promise((resolve, reject) => {\n        chrome.runtime.sendMessage(\n          { ...message, _targetTabId: tab.id },\n          (response) => {\n            if (chrome.runtime.lastError) {\n              const msg = chrome.runtime.lastError.message || \"\";\n              if (msg.includes(\"message port closed before a response\")) {\n                responseCallback ? responseCallback(undefined) : resolve();\n                return;\n              }\n              reject(msg);\n              return;\n            }\n            responseCallback ? responseCallback(response) : resolve(response);\n          }\n        );\n      });\n    }\n\n    if (\n      !tab ||\n      !tab.url ||\n      tab.url.startsWith(\"chrome://\") ||\n      tab.url.startsWith(\"chromewebstore.google.com\") ||\n      tab.url.startsWith(\"chrome.google.com/webstore\") ||\n      tab.url === \"\" ||\n      tab.url === \"about:blank\"\n    ) {\n      return Promise.reject(\"Invalid tab URL\");\n    }\n\n    return new Promise((resolve, reject) => {\n      chrome.tabs.sendMessage(tab.id, message, (response) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError.message);\n        } else {\n          responseCallback ? responseCallback(response) : resolve(response);\n        }\n      });\n    });\n  } catch (error) {\n    console.error(\"Error sending message to tab:\", error);\n    if (noTab && typeof noTab === \"function\") {\n      noTab();\n    }\n    return Promise.reject(error);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/setSurface.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\nimport { loginWithWebsite } from \"../auth/loginWithWebsite\";\n\nexport const setSurface = async (request) => {\n  await chrome.storage.local.set({ surface: request.surface });\n\n  const result = await loginWithWebsite();\n  const { activeTab } = await chrome.storage.local.get([\"activeTab\"]);\n\n  sendMessageTab(activeTab, {\n    type: \"set-surface\",\n    surface: request.surface,\n    subscribed: result?.subscribed || false,\n    instantMode: result?.instantMode || false,\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/tabManagement/tabHelpers.js",
    "content": "export const setMicActiveTab = async (request) => {\n  chrome.storage.local.get([\"region\"], (result) => {\n    if (result.region) {\n      sendMessageRecord({\n        type: \"set-mic-active-tab\",\n        active: request.active,\n        defaultAudioInput: request.defaultAudioInput,\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/utils/base64ToUint8Array.js",
    "content": "export const base64ToUint8Array = (base64) => {\n  const dataUrlRegex = /^data:(.*?);base64,/;\n  const matches = base64.match(dataUrlRegex);\n\n  const decodeBase64 = (base64String) => {\n    const binaryString = atob(base64String);\n    const bytes = new Uint8Array(binaryString.length);\n    for (let i = 0; i < binaryString.length; i++) {\n      bytes[i] = binaryString.charCodeAt(i);\n    }\n    return bytes;\n  };\n\n  if (matches !== null) {\n    // Base64 is a Data URL\n    const mimeType = matches[1];\n    return new Blob([decodeBase64(base64.slice(matches[0].length))], {\n      type: mimeType,\n    });\n  } else {\n    // Base64 is a regular string\n    return new Blob([decodeBase64(base64)], { type: \"video/webm\" });\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/utils/blobToBase64.js",
    "content": "export const blobToBase64 = (blob) => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => resolve(reader.result);\n    reader.onerror = (error) => reject(error);\n    reader.readAsDataURL(blob);\n  });\n};\n"
  },
  {
    "path": "src/pages/Background/utils/browserHelpers.js",
    "content": "export const isPinned = async () => {\n  try {\n    const userSettings = await chrome.action.getUserSettings();\n    return userSettings.isOnToolbar;\n  } catch (error) {\n    console.error(\"Failed to check if the extension is pinned:\", error.message);\n    return false;\n  }\n};\n\nexport const getPlatformInfo = async () => {\n  try {\n    return await chrome.runtime.getPlatformInfo();\n  } catch (error) {\n    console.error(\"Failed to retrieve platform info:\", error.message);\n    return null;\n  }\n};\n\nexport const resizeWindow = async (width, height) => {\n  if (width === 0 || height === 0) {\n    return;\n  }\n\n  chrome.windows.getCurrent((window) => {\n    chrome.windows.update(window.id, {\n      width: width,\n      height: height,\n    });\n  });\n};\n\nexport const checkAvailableMemory = async () => {\n  try {\n    const data = await navigator.storage.estimate();\n\n    return { data };\n  } catch (error) {\n    console.error(\"Failed to estimate memory:\", error);\n    return { error: error.message };\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/utils/downloadHelpers.js",
    "content": "import { sendMessageTab } from \"../tabManagement\";\n\nexport const requestDownload = async (base64, title) => {\n  try {\n    // Open a new tab with the download page\n    const tab = await chrome.tabs.create({\n      url: \"download.html\",\n      active: false,\n    });\n\n    // Add a listener for when the tab finishes loading\n    const listener = (tabId, changeInfo) => {\n      if (tabId === tab.id && changeInfo.status === \"complete\") {\n        chrome.tabs.onUpdated.removeListener(listener);\n\n        // Send the message with the download data\n        sendMessageTab(tab.id, {\n          type: \"download-video\",\n          base64,\n          title,\n        });\n      }\n    };\n\n    chrome.tabs.onUpdated.addListener(listener);\n  } catch (error) {\n    console.error(\"Failed to request download:\", error.message);\n  }\n};\n\nexport const downloadIndexedDB = async () => {\n  try {\n    // Open a new tab with the download page\n    const tab = await chrome.tabs.create({\n      url: \"download.html\",\n      active: false,\n    });\n\n    // Add a listener for when the tab finishes loading\n    const listener = (tabId, changeInfo) => {\n      if (tabId === tab.id && changeInfo.status === \"complete\") {\n        chrome.tabs.onUpdated.removeListener(listener);\n\n        // Send the message to trigger the IndexedDB download\n        sendMessageTab(tab.id, {\n          type: \"download-indexed-db\",\n        });\n      }\n    };\n\n    chrome.tabs.onUpdated.addListener(listener);\n  } catch (error) {\n    console.error(\"Failed to initiate IndexedDB download:\", error.message);\n  }\n};\n"
  },
  {
    "path": "src/pages/Background/utils/executeScripts.js",
    "content": "export const executeScripts = async () => {\n  const contentScripts = chrome.runtime.getManifest().content_scripts;\n  const tabQueries = contentScripts.map((cs) =>\n    chrome.tabs.query({ url: cs.matches })\n  );\n  const tabResults = await Promise.all(tabQueries);\n\n  const executeScriptPromises = [];\n  for (let i = 0; i < tabResults.length; i++) {\n    const tabs = tabResults[i];\n    const cs = contentScripts[i];\n\n    for (const tab of tabs) {\n      const executeScriptPromise = chrome.scripting.executeScript(\n        {\n          target: { tabId: tab.id },\n          files: cs.js,\n        },\n        () => chrome.runtime.lastError\n      );\n      executeScriptPromises.push(executeScriptPromise);\n    }\n  }\n\n  await Promise.all(executeScriptPromises);\n};\n"
  },
  {
    "path": "src/pages/Background/utils/featureDetection.js",
    "content": "export const supportsWebCodecsUA = () => {\n  try {\n    const ua = navigator.userAgent;\n    const chromeMatch = ua.match(/Chrom(?:e|ium|iumWebview)\\/([0-9]+)/);\n    if (chromeMatch) return parseInt(chromeMatch[1], 10) >= 94;\n\n    const safariMatch = ua.match(/Version\\/([0-9]+\\.[0-9]+).*Safari/);\n    if (safariMatch && !chromeMatch) return parseFloat(safariMatch[1]) >= 16.4;\n\n    const operaMatch = ua.match(/OPR\\/([0-9]+)/);\n    if (operaMatch) return parseInt(operaMatch[1], 10) >= 80;\n\n    const edgeMatch = ua.match(/Edg\\/([0-9]+)/);\n    if (edgeMatch) return parseInt(edgeMatch[1], 10) >= 94;\n\n    return false;\n  } catch {\n    return false;\n  }\n};\n\nexport const getBrowserCapabilities = () => {\n  const ua = navigator.userAgent;\n  let browserName = \"Unknown\";\n  let browserVersion = \"Unknown\";\n\n  const chromeMatch = ua.match(/Chrom(?:e|ium)\\/([0-9]+)/);\n  if (chromeMatch) {\n    browserName = \"Chrome\";\n    browserVersion = chromeMatch[1];\n  }\n\n  const safariMatch = ua.match(/Version\\/([0-9]+\\.[0-9]+).*Safari/);\n  if (safariMatch && !chromeMatch) {\n    browserName = \"Safari\";\n    browserVersion = safariMatch[1];\n  }\n\n  const firefoxMatch = ua.match(/Firefox\\/([0-9]+)/);\n  if (firefoxMatch) {\n    browserName = \"Firefox\";\n    browserVersion = firefoxMatch[1];\n  }\n\n  const operaMatch = ua.match(/OPR\\/([0-9]+)/);\n  if (operaMatch) {\n    browserName = \"Opera\";\n    browserVersion = operaMatch[1];\n  }\n\n  const edgeMatch = ua.match(/Edg\\/([0-9]+)/);\n  if (edgeMatch) {\n    browserName = \"Edge\";\n    browserVersion = edgeMatch[1];\n  }\n\n  return {\n    webCodecsUA: supportsWebCodecsUA(),\n    browser: browserName,\n    version: browserVersion,\n    userAgent: ua,\n  };\n};\n\nexport const supportsWebCodecs = async () => {\n  const { realWebCodecsSupport } = await chrome.storage.local.get(\n    \"realWebCodecsSupport\"\n  );\n\n  if (realWebCodecsSupport === true) return true;\n\n  return supportsWebCodecsUA();\n};\n"
  },
  {
    "path": "src/pages/Background/utils/waitForContentScript.js",
    "content": "/**\n * Waits for a content script in the specified tab to be ready.\n * @param {number} tabId - The ID of the tab to wait for.\n * @param {number} [interval=500] - The interval in ms to ping the tab.\n * @param {number} [timeout=10000] - The max timeout in ms to wait.\n * @returns {Promise<void>} Resolves when the content script responds, rejects if it times out.\n */\nexport const waitForContentScript = async (\n  tabId,\n  interval = 500,\n  timeout = 10000\n) => {\n  return new Promise((resolve, reject) => {\n    const maxAttempts = Math.floor(timeout / interval);\n    let attempts = 0;\n\n    const intervalId = setInterval(() => {\n      attempts++;\n\n      if (attempts >= maxAttempts) {\n        clearInterval(intervalId);\n        console.error(`❌ Content script did not respond within ${timeout}ms.`);\n        reject(new Error(`Content script did not respond within ${timeout}ms`));\n        return;\n      }\n\n      // Ping the content script\n      chrome.tabs.sendMessage(tabId, { type: \"ping\" }, (response) => {\n        if (response?.status === \"ready\") {\n          clearInterval(intervalId);\n          resolve();\n        }\n      });\n    }, interval);\n  });\n};\n"
  },
  {
    "path": "src/pages/Backup/Backup.jsx",
    "content": "import React, { useState, useEffect, useRef } from \"react\";\n\nimport localforage from \"localforage\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\nconst chunksStore = localforage.createInstance({\n  name: \"chunks\",\n});\nconst localDirectoryStore = localforage.createInstance({\n  name: \"localDirectory\",\n});\n\nconst Backup = () => {\n  const [setupComplete, setSetupComplete] = useState(false);\n  const writable = useRef(null);\n  const request = useRef(null);\n  const tabId = useRef(null);\n  const repeatRef = useRef(0);\n  const [backupAgain, setBackupAgain] = useState(false);\n  const backupRef = useRef(false);\n  const writingFile = useRef(false);\n  const titleRef = useRef(null);\n  const [override, setOverride] = useState(false);\n  const waitWrite = useRef(false);\n  const closeRequest = useRef(false);\n\n  useEffect(() => {\n    backupRef.current = backupAgain;\n  }, [backupAgain]);\n\n  const verifyFilePermissions = async (fileHandle) => {\n    const opts = {\n      mode: \"readwrite\",\n    };\n    const permission = await fileHandle.queryPermission(opts);\n    if (permission === \"granted\") {\n      return true;\n    } else if (permission === \"prompt\") {\n      chrome.runtime.sendMessage({ type: \"focus-this-tab\" });\n      return false;\n    } else if ((await fileHandle.requestPermission(opts)) === \"granted\") {\n      chrome.runtime.sendMessage({ type: \"focus-this-tab\" });\n      return true;\n    } else {\n      return false;\n    }\n  };\n\n  const initLocalDirectory = async (directoryHandle, prompt = true) => {\n    const permissions = await verifyFilePermissions(directoryHandle);\n    if (permissions) {\n      let videoTitle = `Screenity video - ${new Date().toLocaleString(\"en-US\", {\n        month: \"short\",\n        day: \"numeric\",\n        year: \"numeric\",\n        hour: \"numeric\",\n        minute: \"numeric\",\n        second: \"numeric\",\n        hour12: true,\n      })}.webm`;\n\n      videoTitle = videoTitle.replace(/:/g, \"-\");\n\n      titleRef.current = videoTitle;\n\n      const fileHandle = await directoryHandle.getFileHandle(videoTitle, {\n        create: true,\n      });\n      writable.current = await fileHandle.createWritable();\n\n      setSetupComplete(true);\n      setBackupAgain(true);\n      if (prompt) {\n        chrome.storage.local.set({ backupSetup: true }).then(() => {\n          chrome.runtime.sendMessage({\n            type: \"backup-created\",\n            request: request.current,\n            tabId: tabId.current,\n          });\n        });\n      }\n      writingFile.current = true;\n    } else if (repeatRef.current < 3) {\n      chrome.runtime.sendMessage({ type: \"focus-this-tab\" });\n      repeatRef.current = repeatRef.current + 1;\n      localDirectoryStore.clear();\n\n      localSaving(prompt);\n    } else {\n      alert(\n        \"Failed to set up local backup. Reach out to us at support@screenity.io for more help. You can still record your screen.\",\n      );\n      chrome.storage.local.set({ backup: false });\n      chrome.runtime.sendMessage({\n        type: \"backup-created\",\n        request: request.current,\n        tabId: tabId.current,\n      });\n      setOverride(true);\n      window.close();\n    }\n  };\n\n  const directoryPicker = async (prompt = true) => {\n    chrome.runtime.sendMessage({ type: \"focus-this-tab\" });\n    let directoryPicker = null;\n    // Request access to create a file in a user-selected directory\n    try {\n      directoryPicker = await window.showDirectoryPicker({\n        startIn: \"videos\",\n        mode: \"readwrite\",\n      });\n    } catch (err) {\n      if (backupRef.current) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"backup-error\",\n          why: String(err),\n        });\n      }\n      return;\n    }\n    // check if user cancelled the prompt\n    if (!directoryPicker) {\n      if (backupRef.current) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"backup-error\",\n          why: String(err),\n        });\n      }\n      return;\n    }\n\n    let directoryHandle = directoryPicker;\n\n    if (directoryPicker.name === \"Screenity Recordings\") {\n      directoryHandle = directoryPicker;\n    } else {\n      directoryHandle = await directoryPicker.getDirectoryHandle(\n        \"Screenity Recordings\",\n        { create: true },\n      );\n    }\n\n    await localDirectoryStore.clear();\n    await localDirectoryStore.setItem(\"directoryHandle\", directoryHandle);\n\n    initLocalDirectory(directoryHandle, prompt);\n  };\n\n  const localSaving = async (prompt = true) => {\n    waitWrite.current = false;\n    closeRequest.current = false;\n\n    if (!navigator.userActivation.isActive) {\n      chrome.runtime.sendMessage({ type: \"focus-this-tab\" });\n      return;\n    }\n\n    if (!backupRef.current) {\n      localDirectoryStore.clear();\n    }\n\n    if (\"showDirectoryPicker\" in window) {\n      localDirectoryStore.getItem(\"directoryHandle\").then(async (directory) => {\n        if (directory) {\n          try {\n            const permissions = await verifyFilePermissions(\n              directory.directoryHandle,\n            );\n            if (!permissions) {\n              directoryPicker(prompt);\n            } else {\n              initLocalDirectory(directory.directoryHandle, prompt);\n            }\n          } catch (e) {\n            localDirectoryStore.clear();\n            directoryPicker(prompt);\n          }\n        } else {\n          directoryPicker(prompt);\n        }\n      });\n    } else {\n      alert(\n        \"Your browser doesn't support local backups. Reach out to us at support@screenity.io for more help. You can still record your screen.\",\n      );\n      chrome.storage.local.set({ backup: false });\n      chrome.runtime.sendMessage({\n        type: \"backup-created\",\n        request: request.current,\n        tabId: tabId.current,\n      });\n      setOverride(true);\n      window.close();\n    }\n  };\n\n  const writeFile = async (index) => {\n    if (!writable.current) return;\n    if (!writingFile.current) return;\n    waitWrite.current = true;\n    try {\n      const chunks = [];\n      chunksStore\n        .iterate((value, key, iterationNumber) => {\n          chunks.push(value);\n        })\n        .then(async () => {\n          if (chunks && chunks.length > 0) {\n            const chunk = chunks.find((chunk) => chunk.index === index);\n\n            if (chunk) {\n              await writable.current.write(chunk.chunk);\n              waitWrite.current = false;\n              if (closeRequest.current) {\n                closeRequest.current = false;\n                writable.current.close();\n              }\n            } else {\n              waitWrite.current = false;\n              if (closeRequest.current) {\n                closeRequest.current = false;\n                writable.current.close();\n              }\n            }\n          }\n        });\n    } catch {\n      waitWrite.current = false;\n      if (closeRequest.current) {\n        closeRequest.current = false;\n        writable.current.close();\n      }\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n      });\n      chrome.runtime.sendMessage({ type: \"stop-recording-tab\" });\n    }\n  };\n\n  // Delete the latest file saved in the local backup folder\n  const deleteFile = async (restart = null) => {\n    if (writingFile.current) {\n      await writable.current.close();\n      writingFile.current = false;\n\n      const directory = await localDirectoryStore.getItem(\"directoryHandle\");\n      if (directory && directory !== null) {\n        const permissions = await verifyFilePermissions(\n          directory.directoryHandle,\n        );\n        if (permissions) {\n          await directory.directoryHandle.removeEntry(titleRef.current);\n          if (restart) {\n            localSaving(false);\n          }\n        } else if (restart) {\n          localSaving(false);\n        }\n      }\n    } else if (restart) {\n      localSaving(false);\n    }\n  };\n\n  const skipBackup = () => {\n    chrome.storage.local.set({ backup: false });\n    chrome.runtime.sendMessage({\n      type: \"backup-created\",\n      request: request.current,\n      tabId: tabId.current,\n    });\n    setOverride(true);\n    window.close();\n  };\n\n  const stopBackup = () => {\n    chrome.storage.local.set({ backup: false });\n    chrome.runtime.sendMessage({\n      type: \"stop-recording-tab-backup\",\n    });\n    setOverride(true);\n    window.close();\n  };\n\n  const checkBackupSetup = async () => {\n    const { backupSetup } = await chrome.storage.local.get(\"backupSetup\");\n    if (backupSetup) {\n      setBackupAgain(true);\n    }\n  };\n\n  useEffect(() => {\n    checkBackupSetup();\n  }, []);\n\n  const onMessage = (message, sender, sendResponse) => {\n    if (message.type === \"init-backup\") {\n      request.current = message.request;\n      tabId.current = message.tabId;\n      localSaving(true);\n    } else if (message.type === \"write-file\") {\n      writeFile(message.index);\n    } else if (message.type === \"close-writable\") {\n      if (!waitWrite.current) {\n        writable.current.close();\n      } else {\n        closeRequest.current = true;\n      }\n    } else if (\n      message.type === \"discard-backup\" ||\n      message.type === \"recording-error\"\n    ) {\n      deleteFile(false);\n    } else if (message.type === \"discard-backup-restart\") {\n      deleteFile(true);\n    } else if (message.type === \"close-backup-tab\") {\n      setOverride(true);\n      window.close();\n    }\n  };\n  const closeTab = () => {\n    chrome.runtime.sendMessage({\n      type: \"stop-recording-tab-backup\",\n    });\n    setOverride(true);\n    window.close();\n  };\n\n  useEffect(() => {\n    chrome.runtime.onMessage.addListener(onMessage);\n\n    return () => {\n      chrome.runtime.onMessage.removeListener(onMessage);\n    };\n  }, []);\n\n  return (\n    <div className=\"setupBackground\">\n      {!setupComplete && !backupAgain && (\n        <div className=\"setupContainer\">\n          <div className=\"setupImage\">\n            <img src={chrome.runtime.getURL(\"assets/helper/backup.png\")} />\n          </div>\n          <div className=\"setupText\">\n            <div className=\"setupEmoji\">💾</div>\n            <div className=\"setupTitle\">\n              {chrome.i18n.getMessage(\"backupsTitle\")}\n            </div>\n            <div className=\"setupDescription\">\n              {chrome.i18n.getMessage(\"backupsDescription1\")}\n              <br />\n              {chrome.i18n.getMessage(\"backupsDescription2\")}{\" \"}\n              <a\n                href=\"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-backups-how-can-i-set-them-up/waYArvSwybZkJKKDdMXw1o\"\n                target=\"_blank\"\n              >\n                {chrome.i18n.getMessage(\"learnMoreDot\")}\n              </a>\n            </div>\n            <div className=\"setupActions\">\n              <button\n                className=\"setupButton\"\n                onClick={() => {\n                  localSaving(true);\n                }}\n              >\n                {chrome.i18n.getMessage(\"backupsSelectFolder\")}\n              </button>\n              <button\n                className=\"cancelButton\"\n                onClick={() => {\n                  skipBackup();\n                }}\n              >\n                {chrome.i18n.getMessage(\"backupsNotNow\")}\n              </button>\n            </div>\n          </div>\n        </div>\n      )}\n      {setupComplete && (\n        <div>\n          <div className=\"middle-area\">\n            <img src={chrome.runtime.getURL(\"assets/backup-icon.svg\")} />\n            <div className=\"title\">\n              {chrome.i18n.getMessage(\"backupsOnTitle\")}\n            </div>\n            <div className=\"subtitle\">\n              {chrome.i18n.getMessage(\"backupsOnDescription\")}\n            </div>\n\n            <div\n              className=\"button-stop\"\n              onClick={() => {\n                closeTab();\n              }}\n            >\n              {chrome.i18n.getMessage(\"backupsClose\")}\n            </div>\n            <div\n              className=\"button-cancel\"\n              onClick={() => {\n                stopBackup();\n              }}\n            >\n              {chrome.i18n.getMessage(\"backupsStop\")}\n            </div>\n          </div>\n        </div>\n      )}\n      {backupAgain && !setupComplete && (\n        <div>\n          <div className=\"middle-area\">\n            <img src={chrome.runtime.getURL(\"assets/backup-icon.svg\")} />\n            <div className=\"title\">\n              {chrome.i18n.getMessage(\"backupsConfirmTitle\")}\n            </div>\n            <div className=\"subtitle\">\n              {chrome.i18n.getMessage(\"backupsConfirmDescription\")}{\" \"}\n              <a\n                href=\"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-backups-how-can-i-set-them-up/waYArvSwybZkJKKDdMXw1o\"\n                target=\"_blank\"\n              >\n                {chrome.i18n.getMessage(\"learnMoreDot\")}\n              </a>\n            </div>\n\n            <div\n              className=\"button-strong\"\n              onClick={() => {\n                localSaving(true);\n              }}\n            >\n              {chrome.i18n.getMessage(\"backupsConfirmAllow\")}\n            </div>\n            <div\n              className=\"button-cancel\"\n              onClick={() => {\n                stopBackup();\n              }}\n            >\n              {chrome.i18n.getMessage(\"backupsStop\")}\n            </div>\n          </div>\n        </div>\n      )}\n      <img\n        className=\"setupLogo\"\n        src={chrome.runtime.getURL(\"assets/logo-text.svg\")}\n      />\n      <style>\n        {`\n\t\t\t\tbody {\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\tmargin: 0px;\n\t\t\t\t\tmargin: 0;\n\tpadding: 0;\n\tmin-height: 100%;\n\t\tbackground-color: #F6F7FB!important;\n\t\tbackground: url('` +\n          chrome.runtime.getURL(\"assets/helper/pattern-svg.svg\") +\n          `') repeat;\n\t\tbackground-size: 62px 23.5px;\n\t\tanimation: moveBackground 138s linear infinite;\n\t\ttransform: rotate(0deg);\n\t\t\t\t}\n\n\t\t\t\t.button-strong {\n\t\t\t\t\tpadding: 10px 20px;\n\t\t\t\t\tbackground: #29292F;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tmargin-top: 0px;\n\t\t\t\t\tmargin-left: auto;\n\t\t\t\t\tmargin-right: auto;\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t.button-stop {\n\t\t\t\t\tpadding: 10px 20px;\n\t\t\t\t\tbackground: #FFF;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #29292F;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tmargin-top: 0px;\n\t\t\t\t\tborder: 1px solid #E8E8E8;\n\t\t\t\t\tmargin-left: auto;\n\t\t\t\t\tmargin-right: auto;\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t.button-cancel {\n\t\t\t\t\tpadding: 10px 20px;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t\tmargin-left: auto;\n\t\t\t\t\tmargin-right: auto;\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t.wrap {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground-color: #F6F7FB;\n\t\t\t\t}\n\t\t\t\t\t.middle-area {\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tflex-direction: column;\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tjustify-content: center;\n\t\t\t\t\t\theight: 100%;\n\t\t\t\t\t\tfont-family: \"Satoshi Medium\", sans-serif;\n\t\t\t\t\t}\n\t\t\t\t\t.middle-area img {\n\t\t\t\t\t\twidth: 40px;\n\t\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t\t}\n\t\t\t\t\t.title {\n\t\t\t\t\t\tfont-size: 24px;\n\t\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\t\tcolor: #1A1A1A;\n\t\t\t\t\t\tmargin-bottom: 14px;\n\t\t\t\t\t\tfont-family: Satoshi-Medium, sans-serif;\n\t\t\t\t\t}\n\t\t\t\t\t.subtitle {\n\t\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\t\tfont-weight: 400;\n\t\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t\t\tline-height: 1.6;\n\t\t\t\t\t\tfont-family: Satoshi-Medium, sans-serif;\n\t\t\t\t\t\twidth: 600px;\n\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t}\n\n\t\t\t\t.setupInfo {\n\t\t\t\t\tmargin-top: 20px;\n\t\t\t\t}\n\t\t\t\ta {\n\t\t\t\t\ttext-decoration: none!important;\n\t\t\t\t\tcolor: #4C7DE2;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes moveBackground {\n\t\t\t\t\t0% {\n\t\t\t\t\t\tbackground-position: 0 0;\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\tbackground-position: 100% 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.setupActions {\n\t\t\t\t\tmargin-top: 28px;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tgap: 4px;\n\t\t\t\t}\n\n\t\t\t\t.setupButton {\n\t\t\t\t\tbackground-color: #29292F;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tpadding-left: 24px;\n\t\t\t\t\tpadding-right: 24px;\n\t\t\t\t\tpadding-top: 12px;\n\t\t\t\t\tpadding-bottom: 12px;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\toutline: none;\n\t\t\t\t\tborder: 0px;\n\t\t\t\t\tfont-family: \"Satoshi-Bold\", sans-serif!important;\n\t\t\t\t}\n\t\t\t\t.setupButton:hover {\n\t\t\t\t\tbackground: #000!important;\n\t\t\t\t}\n\n\t\t\t\t.cancelButton {\n\t\t\t\t\tbackground-color: transparent;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #7D8490;\n\t\t\t\t\tfont-family: \"Satoshi-Bold\", sans-serif!important;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tpadding-left: 24px;\n\t\t\t\t\tpadding-right: 24px;\n\t\t\t\t\tpadding-top: 12px;\n\t\t\t\t\tpadding-bottom: 12px;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\toutline: none;\n\t\t\t\t\tborder: 0px;\n\t\t\t\t}\n\t\t\t\t.cancelButton:hover {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tbackground: #F4F2F2!important;\n\t\t\t\t}\n\n\n\t\t\t\t.setupLogo {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tbottom: 30px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\twidth: 120px;\n\t\t\t\t}\n\n\n\t\t\t\t.setupBackground {\n\t\t\t\t\theight: 100vh;\n\t\t\t\t\twidth: 100vw;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupContainer {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tbottom: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\tz-index: 999;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\twidth: 60%;\n\t\t\t\t\theight: fit-content;\n\t\t\t\t\tbackground-color: #fff;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tpadding: 50px 50px;\n\t\t\t\t\tgap: 80px;\n\t\t\t\t\tfont-family: 'Satoshi-Medium', sans-serif;\n\t\t\t\t}\n\n\t\t\t\t.setupImage {\n\t\t\t\t\twidth: 70%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupImage img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t}\n\n\t\t\t\t.setupText {\n\t\t\t\t\twidth: 50%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\ttext-align: left;\n\t\t\t\t}\n\n\t\t\t\t.setupEmoji {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t}\n\n\t\t\t\t.setupTitle {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tcolor: #29292F;\n\t\t\t\t\tfont-family: 'Satoshi-Bold', sans-serif!important;\n\t\t\t\t\tletter-spacing: -0.5px;\n\t\t\t\t}\n\n\t\t\t\tbr {\n\t\t\t\t\tdisplay: block;\n\t\t\t\t\tcontent: \"\";\n\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t}\n\n\t\t\t\t.setupDescription {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tline-height: 1.6;\n\t\t\t\t}\n\n\t\t\t\t.setupDescription span {\n\t\t\t\t\tfont-family: 'Satoshi-Bold', sans-serif!important;\n\t\t\t\t\tcolor: #29292F!important;\n\t\t\t\t\tdisplay: contents!important;\n\t\t\t\t}\n\n\t\t\t\t.setupStep {\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t}\n\n\t\t\t\t.setupStep span {\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\twidth: 20px;\n\t\t\t\t\theight: 20px;\n\t\t\t\t\tpadding: 2px;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tdisplay: inline-flex;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\tmargin-left: 3px;\n\t\t\t\t\tmargin-right: 3px;\n\t\t\t\t\tbackground-color: #F4F2F2;\n\t\t\t\t}\n\n\t\t\t\t.setupStep img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\n\t\t\t\t.center {\n\t\t\t\t\ttext-align: center!important;\n\t\t\t\t}\n\t\t\t\t.setupText.center {\n\t\t\t\t\twidth: auto!important;\n\t\t\t\t}\n\t\t\t\t.setupContainer.center {\n\t\t\t\t\twidth: 40%!important;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@media only screen and (max-width: 800px) {\n\t\t\t\t\t.setupContainer {\n\t\t\t\t\t\tflex-direction: column;\n\t\t\t\t\t\tgap: 40px;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t.setupText, .setupImage {\n\t\t\t\t\t\twidth: 100%!important;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t@media only screen and (max-width: 500px) {\n\t\t\t\t\t.setupContainer {\n\t\t\t\t\t\twidth: 80%!important;\n\t\t\t\t\t\tpadding: 20px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupTitle {\n\t\t\t\t\t\tfont-size: 18px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupDescription {\n\t\t\t\t\t\tfont-size: 12px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupStep {\n\t\t\t\t\t\tfont-size: 12px!important;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\n\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default Backup;\n"
  },
  {
    "path": "src/pages/Backup/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Backups</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Gloria-Hallelujah;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular.ttf)\n          format(\"truetype\");\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <!-- <script src=\"chrome-extension://__MSG_@@extension_id__/assets/vendor/ffmpeg-core.js\"></script> -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Backup/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport Backup from \"./Backup\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Backup />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Backup/messaging/handlers.js",
    "content": ""
  },
  {
    "path": "src/pages/Camera/Camera.jsx",
    "content": "import React, { useEffect, useRef, useState } from \"react\";\nimport Background from \"./components/Background\";\nimport { useCameraContext } from \"./context/CameraContext\";\nimport {\n  getCameraStream,\n  stopCameraStream,\n  surfaceHandler,\n} from \"./utils/cameraUtils\";\nimport { setupHandlers } from \"./messaging/handlers\";\n\nconst Camera = () => {\n  const [loadingStates, setLoadingStates] = useState({\n    recordingType: true,\n    backgroundEffects: true,\n    videoElement: true,\n    modelLoading: false,\n  });\n  const {\n    width,\n    height,\n    pipMode,\n    setPipMode,\n    backgroundEffects,\n    setBackgroundEffects,\n    recordingTypeRef,\n    setWidth,\n    setHeight,\n    videoRef,\n    streamRef,\n    offScreenCanvasRef,\n    offScreenCanvasContextRef,\n    isModelLoaded,\n    isCameraMode,\n    setIsCameraMode,\n  } = useCameraContext();\n\n  // Helper function to update loading states\n  const updateLoadingState = (key, value) => {\n    setLoadingStates((prev) => ({ ...prev, [key]: value }));\n  };\n\n  // Calculate if anything is still loading\n  const isLoading = Object.values(loadingStates).some((state) => state);\n\n  // Ensure the offScreenCanvas is created exactly once\n  useEffect(() => {\n    if (!offScreenCanvasRef.current) {\n      offScreenCanvasRef.current = document.createElement(\"canvas\");\n    }\n  }, []);\n\n  // Initialize message listener\n  useEffect(() => {\n    setupHandlers({\n      setLoading: updateLoadingState,\n    });\n  }, []);\n\n  useEffect(() => {\n    chrome.storage.local.get([\"recordingType\"], (result) => {\n      recordingTypeRef.current =\n        result.recordingType === \"camera\" ? \"camera\" : \"screen\";\n      updateLoadingState(\"recordingType\", false);\n      setIsCameraMode(recordingTypeRef.current === \"camera\");\n    });\n  }, []);\n\n  useEffect(() => {\n    chrome.storage.local.get([\"backgroundEffectsActive\"], (result) => {\n      if (result.backgroundEffectsActive !== undefined) {\n        setBackgroundEffects(result.backgroundEffectsActive);\n      }\n\n      if (!result.backgroundEffectsActive) {\n        updateLoadingState(\"modelLoading\", false);\n      }\n\n      updateLoadingState(\"backgroundEffects\", false);\n    });\n  }, []);\n\n  useEffect(() => {\n    if (backgroundEffects && !isModelLoaded) {\n      updateLoadingState(\"modelLoading\", true);\n    } else if (!backgroundEffects || isModelLoaded) {\n      updateLoadingState(\"modelLoading\", false);\n    }\n  }, [backgroundEffects, isModelLoaded]);\n\n  useEffect(() => {\n    if (!videoRef.current || streamRef.current?.active) return;\n\n    updateLoadingState(\"videoElement\", true);\n\n    chrome.storage.local.get([\"defaultVideoInput\"], (result) => {\n      const constraints =\n        result.defaultVideoInput !== \"none\"\n          ? { video: { deviceId: { exact: result.defaultVideoInput } } }\n          : { video: true };\n\n      getCameraStream(\n        constraints,\n        streamRef,\n        videoRef,\n        offScreenCanvasRef,\n        offScreenCanvasContextRef,\n        {\n          onStart: () => updateLoadingState(\"videoElement\", true),\n          onFinish: () => updateLoadingState(\"videoElement\", false),\n        }\n      );\n    });\n  }, [videoRef.current]); // Will run once when videoRef is set\n\n  // After the camera stream is ready, check if we should re-enter PiP.\n  // This handles the case where the user navigated while recording a monitor —\n  // the old PiP was destroyed with the old page, so we need to re-enter it\n  // once the new camera iframe has a live stream.\n  useEffect(() => {\n    if (!videoRef.current || !streamRef.current?.active) return;\n\n    chrome.storage.local.get(\n      [\"surface\", \"recording\"],\n      (result) => {\n        if (result.recording && result.surface) {\n          surfaceHandler({ surface: result.surface }, videoRef);\n        }\n      },\n    );\n  }, [videoRef.current, streamRef.current?.active]);\n\n  // Handle PiP events\n  const handleEnterPip = () => {\n    setPipMode(true);\n    chrome.runtime.sendMessage({ type: \"pip-started\" });\n  };\n\n  const handleLeavePip = () => {\n    setPipMode(false);\n    chrome.runtime.sendMessage({ type: \"pip-ended\" });\n  };\n\n  useEffect(() => {\n    if (!videoRef.current) {\n      console.warn(\"Video element not ready for PiP events.\");\n      return;\n    }\n\n    const video = videoRef.current;\n    if (!video) return;\n    video.addEventListener(\"enterpictureinpicture\", handleEnterPip);\n    video.addEventListener(\"leavepictureinpicture\", handleLeavePip);\n\n    return () => {\n      video.removeEventListener(\"enterpictureinpicture\", handleEnterPip);\n      video.removeEventListener(\"leavepictureinpicture\", handleLeavePip);\n    };\n  }, [videoRef]);\n\n  return (\n    <div style={{ width: \"100%\", height: \"100%\", overflow: \"hidden\" }}>\n      {backgroundEffects && <Background />}\n      <video\n        style={{\n          // height: height,\n          // width: width,\n          height: \"100%\",\n          width: \"100%\",\n          position: \"absolute\",\n          top: \"50%\",\n          left: \"50%\",\n          transform: \"translate(-50%, -50%)\",\n          zIndex: 99,\n          display: !backgroundEffects ? \"block\" : \"none\",\n          objectFit: isCameraMode ? \"contain\" : \"cover\",\n          objectPosition: \"50% 50%\",\n        }}\n        ref={videoRef}\n        playsInline\n        muted\n      ></video>\n\n      {isLoading && (\n        <div\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            backgroundColor: \"#CBD0D8\",\n            zIndex: 999,\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <div className=\"loader\"></div>\n        </div>\n      )}\n\n      {pipMode && (\n        <img\n          src={chrome.runtime.getURL(\"assets/pip-mode.svg\")}\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            zIndex: 999,\n          }}\n        />\n      )}\n      <style>\n        {`.loader {\n  font-size: 10px;\n  width: 1em;\n  height: 1em;\n\tmargin: auto;\n  border-radius: 50%;\n  position: relative;\n  text-indent: -9999em;\n  animation: mulShdSpin 1.1s infinite ease;\n  transform: translateZ(0);\n}\n@keyframes mulShdSpin {\n  0%,\n  100% {\n    box-shadow: 0em -2.6em 0em 0em #ffffff, 1.8em -1.8em 0 0em rgba(255,255,255, 0.2), 2.5em 0em 0 0em rgba(255,255,255, 0.2), 1.75em 1.75em 0 0em rgba(255,255,255, 0.2), 0em 2.5em 0 0em rgba(255,255,255, 0.2), -1.8em 1.8em 0 0em rgba(255,255,255, 0.2), -2.6em 0em 0 0em rgba(255,255,255, 0.5), -1.8em -1.8em 0 0em rgba(255,255,255, 0.7);\n  }\n  12.5% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.7), 1.8em -1.8em 0 0em #ffffff, 2.5em 0em 0 0em rgba(255,255,255, 0.2), 1.75em 1.75em 0 0em rgba(255,255,255, 0.2), 0em 2.5em 0 0em rgba(255,255,255, 0.2), -1.8em 1.8em 0 0em rgba(255,255,255, 0.2), -2.6em 0em 0 0em rgba(255,255,255, 0.2), -1.8em -1.8em 0 0em rgba(255,255,255, 0.5);\n  }\n  25% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.5), 1.8em -1.8em 0 0em rgba(255,255,255, 0.7), 2.5em 0em 0 0em #ffffff, 1.75em 1.75em 0 0em rgba(255,255,255, 0.2), 0em 2.5em 0 0em rgba(255,255,255, 0.2), -1.8em 1.8em 0 0em rgba(255,255,255, 0.2), -2.6em 0em 0 0em rgba(255,255,255, 0.2), -1.8em -1.8em 0 0em rgba(255,255,255, 0.2);\n  }\n  37.5% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.2), 1.8em -1.8em 0 0em rgba(255,255,255, 0.5), 2.5em 0em 0 0em rgba(255,255,255, 0.7), 1.75em 1.75em 0 0em #ffffff, 0em 2.5em 0 0em rgba(255,255,255, 0.2), -1.8em 1.8em 0 0em rgba(255,255,255, 0.2), -2.6em 0em 0 0em rgba(255,255,255, 0.2), -1.8em -1.8em 0 0em rgba(255,255,255, 0.2);\n  }\n  50% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.2), 1.8em -1.8em 0 0em rgba(255,255,255, 0.2), 2.5em 0em 0 0em rgba(255,255,255, 0.5), 1.75em 1.75em 0 0em rgba(255,255,255, 0.7), 0em 2.5em 0 0em #ffffff, -1.8em 1.8em 0 0em rgba(255,255,255, 0.2), -2.6em 0em 0 0em rgba(255,255,255, 0.2), -1.8em -1.8em 0 0em rgba(255,255,255, 0.2);\n  }\n  62.5% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.2), 1.8em -1.8em 0 0em rgba(255,255,255, 0.2), 2.5em 0em 0 0em rgba(255,255,255, 0.2), 1.75em 1.75em 0 0em rgba(255,255,255, 0.5), 0em 2.5em 0 0em rgba(255,255,255, 0.7), -1.8em 1.8em 0 0em #ffffff, -2.6em 0em 0 0em rgba(255,255,255, 0.2), -1.8em -1.8em 0 0em rgba(255,255,255, 0.2);\n  }\n  75% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.2), 1.8em -1.8em 0 0em rgba(255,255,255, 0.2), 2.5em 0em 0 0em rgba(255,255,255, 0.2), 1.75em 1.75em 0 0em rgba(255,255,255, 0.2), 0em 2.5em 0 0em rgba(255,255,255, 0.5), -1.8em 1.8em 0 0em rgba(255,255,255, 0.7), -2.6em 0em 0 0em #ffffff, -1.8em -1.8em 0 0em rgba(255,255,255, 0.2);\n  }\n  87.5% {\n    box-shadow: 0em -2.6em 0em 0em rgba(255,255,255, 0.2), 1.8em -1.8em 0 0em rgba(255,255,255, 0.2), 2.5em 0em 0 0em rgba(255,255,255, 0.2), 1.75em 1.75em 0 0em rgba(255,255,255, 0.2), 0em 2.5em 0 0em rgba(255,255,255, 0.2), -1.8em 1.8em 0 0em rgba(255,255,255, 0.5), -2.6em 0em 0 0em rgba(255,255,255, 0.7), -1.8em -1.8em 0 0em #ffffff;\n  }\n}`}\n      </style>\n    </div>\n  );\n};\n\nexport default Camera;\n"
  },
  {
    "path": "src/pages/Camera/components/Background.js",
    "content": "import React, { useRef, useEffect, useState } from \"react\";\nimport { useCameraContext } from \"../context/CameraContext\";\nimport { resizeCanvases } from \"../utils/canvasUtils\";\nimport {\n  segmentFromVideo,\n  renderBlurFromVideo,\n  renderPersonCutoutFromVideo,\n  renderEffectBackground,\n} from \"../utils/backgroundUtils\";\n\nconst Background = () => {\n  const canvasRef = useRef(null);\n  const canvasContextRef = useRef(null);\n  const bottomCanvasRef = useRef(null);\n  const bottomCanvasContextRef = useRef(null);\n\n  const {\n    videoRef,\n    backgroundEffects,\n    setBackgroundEffects,\n    segmenterRef,\n    blurRef,\n    effectRef,\n  } = useCameraContext();\n\n  const [windowSize, setWindowSize] = useState({\n    width: window.innerWidth,\n    height: window.innerHeight,\n  });\n\n  useEffect(() => {\n    if (!canvasRef.current || !bottomCanvasRef.current) return;\n\n    canvasContextRef.current = canvasRef.current.getContext(\"2d\", {\n      alpha: true,\n      willReadFrequently: true,\n      preserveDrawingBuffer: true,\n    });\n    bottomCanvasContextRef.current = bottomCanvasRef.current.getContext(\"2d\", {\n      alpha: true,\n      willReadFrequently: true,\n      preserveDrawingBuffer: true,\n    });\n  }, []);\n\n  useEffect(() => {\n    const handleResize = () => {\n      setWindowSize({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      });\n    };\n\n    window.addEventListener(\"resize\", handleResize);\n    return () => window.removeEventListener(\"resize\", handleResize);\n  }, []);\n\n  // Re-render background image on resize or effect change\n  useEffect(() => {\n    if (!effectRef.current || blurRef.current) return;\n\n    if (\n      bottomCanvasRef.current &&\n      bottomCanvasContextRef.current &&\n      canvasRef.current\n    ) {\n      renderEffectBackground(\n        effectRef.current,\n        bottomCanvasRef,\n        bottomCanvasContextRef,\n      );\n\n      resizeCanvases(\n        effectRef.current.width,\n        effectRef.current.height,\n        true,\n        effectRef.current,\n        canvasRef,\n        bottomCanvasRef,\n        bottomCanvasContextRef,\n      );\n    }\n  }, [windowSize, effectRef.current, blurRef.current]);\n\n  // Self-contained render loop — segments video directly, no React state per frame\n  useEffect(() => {\n    if (!backgroundEffects) return;\n\n    let animFrameId;\n    let running = true;\n    let consecutiveErrors = 0;\n\n    const tick = () => {\n      if (!running) return;\n\n      try {\n        const video = videoRef.current;\n        const segmenter = segmenterRef.current;\n\n        if (video && video.readyState >= 2 && segmenter && video.videoWidth > 0) {\n          const result = segmentFromVideo(video, segmenter);\n\n          if (result && result.categoryMask) {\n            consecutiveErrors = 0;\n            if (blurRef.current) {\n              renderBlurFromVideo(video, result, canvasRef);\n            } else if (effectRef.current) {\n              renderEffectBackground(\n                effectRef.current,\n                bottomCanvasRef,\n                bottomCanvasContextRef,\n              );\n              renderPersonCutoutFromVideo(\n                video,\n                result,\n                canvasRef,\n                canvasContextRef,\n              );\n            }\n          }\n        }\n      } catch (error) {\n        consecutiveErrors++;\n        console.error(\"Background segmentation error:\", error);\n        if (consecutiveErrors >= 3) {\n          console.warn(\"Disabling background effects after repeated failures\");\n          setBackgroundEffects(false);\n          return;\n        }\n      }\n\n      animFrameId = requestAnimationFrame(tick);\n    };\n\n    animFrameId = requestAnimationFrame(tick);\n\n    return () => {\n      running = false;\n      cancelAnimationFrame(animFrameId);\n    };\n  }, [backgroundEffects]);\n\n  // Listen for Chrome extension messages\n  useEffect(() => {\n    const handleMessage = (request) => {\n      if (request.type === \"set-background-effect\") {\n        if (globalThis.SCREENITY_VERBOSE_LOGS) {\n          console.log(\"Background component received effect change message\");\n        }\n      }\n    };\n\n    chrome.runtime.onMessage.addListener(handleMessage);\n    return () => chrome.runtime.onMessage.removeListener(handleMessage);\n  }, []);\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        zIndex: 99999,\n        top: 0,\n        left: 0,\n        right: 0,\n        bottom: 0,\n        overflow: \"hidden\",\n      }}\n    >\n      <div style={{ position: \"relative\", width: \"100%\", height: \"100%\" }}>\n        <canvas\n          style={{\n            position: \"absolute\",\n            top: \"50%\",\n            left: \"50%\",\n            transform: \"translate(-50%, -50%)\",\n            width: \"100%\",\n            height: \"100%\",\n            zIndex: 999999,\n            opacity: backgroundEffects ? 1 : 0,\n            backgroundColor: \"transparent\",\n          }}\n          ref={canvasRef}\n        ></canvas>\n\n        <canvas\n          style={{\n            position: \"absolute\",\n            top: \"50%\",\n            left: \"50%\",\n            transform: \"translate(-50%, -50%)\",\n            width: \"100%\",\n            height: \"100%\",\n            zIndex: 999998,\n            opacity: backgroundEffects && !blurRef.current ? 1 : 0,\n            backgroundColor: \"transparent\",\n          }}\n          ref={bottomCanvasRef}\n        ></canvas>\n      </div>\n    </div>\n  );\n};\n\nexport default Background;\n"
  },
  {
    "path": "src/pages/Camera/context/CameraContext.js",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useRef,\n  useEffect,\n  useCallback,\n} from \"react\";\nimport {\n  loadSegmentationModel,\n  loadEffect,\n} from \"../utils/backgroundUtils\";\nimport { initializeCanvases, setupCanvasContexts } from \"../utils/canvasUtils\";\n\nconst CameraContext = createContext();\n\nexport const globalRefs = {\n  videoRef: null,\n  streamRef: null,\n  recordingTypeRef: null,\n  offScreenCanvasRef: null,\n  offScreenCanvasContextRef: null,\n  segmenterRef: null,\n  blurRef: null,\n  effectRef: null,\n  setWidth: null,\n  setHeight: null,\n  setBackgroundEffects: null,\n  backgroundEffectsRef: null,\n  width: null,\n  height: null,\n};\n\nexport const useCameraContext = () => useContext(CameraContext);\n\nexport const getContextRefs = () => {\n  const missingRefs = [];\n\n  if (!globalRefs.videoRef) missingRefs.push(\"videoRef\");\n  if (!globalRefs.streamRef) missingRefs.push(\"streamRef\");\n  if (!globalRefs.recordingTypeRef) missingRefs.push(\"recordingTypeRef\");\n  if (!globalRefs.offScreenCanvasRef) missingRefs.push(\"offScreenCanvasRef\");\n  if (!globalRefs.offScreenCanvasContextRef)\n    missingRefs.push(\"offScreenCanvasContextRef\");\n  if (!globalRefs.segmenterRef) missingRefs.push(\"segmenterRef\");\n  if (!globalRefs.blurRef) missingRefs.push(\"blurRef\");\n  if (!globalRefs.effectRef) missingRefs.push(\"effectRef\");\n  if (!globalRefs.setWidth) missingRefs.push(\"setWidth\");\n  if (!globalRefs.setHeight) missingRefs.push(\"setHeight\");\n  if (!globalRefs.setBackgroundEffects)\n    missingRefs.push(\"setBackgroundEffects\");\n  if (!globalRefs.backgroundEffectsRef)\n    missingRefs.push(\"backgroundEffectsRef\");\n\n  if (missingRefs.length > 0) {\n    console.warn(\n      `⚠️ Some context references are not initialized yet: ${missingRefs.join(\n        \", \"\n      )}`\n    );\n  }\n\n  return {\n    videoRef: globalRefs.videoRef ?? { current: null },\n    streamRef: globalRefs.streamRef ?? { current: new MediaStream() },\n    recordingTypeRef: globalRefs.recordingTypeRef ?? { current: \"screen\" },\n    offScreenCanvasRef: globalRefs.offScreenCanvasRef ?? { current: null },\n    offScreenCanvasContextRef: globalRefs.offScreenCanvasContextRef ?? {\n      current: null,\n    },\n    segmenterRef: globalRefs.segmenterRef ?? { current: null },\n    blurRef: globalRefs.blurRef ?? { current: false },\n    effectRef: globalRefs.effectRef ?? { current: null },\n    setWidth:\n      globalRefs.setWidth ??\n      ((width) => console.warn(\"⚠️ setWidth not initialized\")),\n    setHeight:\n      globalRefs.setHeight ??\n      ((height) => console.warn(\"⚠️ setHeight not initialized\")),\n    setBackgroundEffects: globalRefs.setBackgroundEffects ?? (() => {}),\n    backgroundEffectsRef: globalRefs.backgroundEffectsRef ?? { current: false },\n  };\n};\n\nexport const CameraProvider = ({ children }) => {\n  const [width, setWidth] = useState(\"auto\");\n  const [height, setHeight] = useState(\"100%\");\n  const [backgroundEffects, setBackgroundEffects] = useState(false);\n  const [isModelLoaded, setIsModelLoaded] = useState(false);\n  const [pipMode, setPipMode] = useState(false);\n  const [isCameraMode, setIsCameraMode] = useState(false);\n\n  const backgroundEffectsRef = useRef(false);\n  const recordingTypeRef = useRef(\"screen\");\n  const videoRef = useRef(null);\n  const streamRef = useRef(new MediaStream());\n\n  const offScreenCanvasRef = useRef(null);\n  const offScreenCanvasContextRef = useRef(null);\n\n  const segmenterRef = useRef(null);\n  const blurRef = useRef(false);\n  const effectRef = useRef(null);\n\n  useEffect(() => {\n    const { offScreenCanvas, offScreenCanvasContext } = initializeCanvases();\n\n    offScreenCanvasRef.current = offScreenCanvas;\n    offScreenCanvasContextRef.current = offScreenCanvasContext;\n\n    globalRefs.videoRef = videoRef;\n    globalRefs.streamRef = streamRef;\n    globalRefs.recordingTypeRef = recordingTypeRef;\n    globalRefs.offScreenCanvasRef = offScreenCanvasRef;\n    globalRefs.offScreenCanvasContextRef = offScreenCanvasContextRef;\n    globalRefs.segmenterRef = segmenterRef;\n    globalRefs.blurRef = blurRef;\n    globalRefs.effectRef = effectRef;\n    globalRefs.setWidth = handleSetWidth;\n    globalRefs.setHeight = handleSetHeight;\n    globalRefs.setBackgroundEffects = handleSetBackgroundEffects;\n    globalRefs.backgroundEffectsRef = backgroundEffectsRef;\n\n    const initializeModel = async () => {\n      try {\n        const model = await loadSegmentationModel();\n        if (model) {\n          segmenterRef.current = model;\n          setIsModelLoaded(true);\n        } else {\n          // Model returned null — fall back to raw camera feed\n          console.warn(\"Segmentation model unavailable, disabling background effects\");\n          handleSetBackgroundEffects(false);\n        }\n      } catch (error) {\n        console.error(\"Failed to load segmentation model:\", error);\n        handleSetBackgroundEffects(false);\n      }\n    };\n\n    initializeModel();\n\n    chrome.storage.local.get([\"backgroundEffect\"], (result) => {\n      if (result.backgroundEffect === \"blur\") {\n        blurRef.current = true;\n      } else if (result.backgroundEffect) {\n        blurRef.current = false;\n        loadCustomEffect(result.backgroundEffect);\n      }\n    });\n\n    return () => {\n      segmenterRef.current = null;\n      setIsModelLoaded(false);\n    };\n  }, []);\n\n  useEffect(() => {\n    backgroundEffectsRef.current = backgroundEffects;\n  }, [backgroundEffects]);\n\n  const handleSetBackgroundEffects = useCallback(\n    (active) => {\n      setBackgroundEffects(active);\n      backgroundEffectsRef.current = active;\n      if (videoRef.current) {\n        videoRef.current.style.display = !active ? \"block\" : \"none\";\n      }\n      chrome.storage.local.set({ backgroundEffectsActive: active });\n    },\n    [videoRef]\n  );\n\n  const handleSetWidth = useCallback(\n    (newWidth) => {\n      setWidth(newWidth);\n      if (videoRef.current) {\n        videoRef.current.style.width = newWidth;\n      }\n    },\n    [videoRef]\n  );\n\n  const handleSetHeight = useCallback(\n    (newHeight) => {\n      setHeight(newHeight);\n      if (videoRef.current) {\n        videoRef.current.style.height = newHeight;\n      }\n    },\n    [videoRef]\n  );\n\n  const loadCustomEffect = async (effectUrl) => {\n    try {\n      if (!effectUrl) {\n        effectRef.current = null;\n        return;\n      }\n\n      const image = await loadEffect(effectUrl);\n      effectRef.current = image;\n\n      return true;\n    } catch (error) {\n      console.error(\"Failed to load custom effect:\", error);\n      return false;\n    }\n  };\n\n  const enableBlur = (enabled) => {\n    blurRef.current = enabled;\n\n    chrome.storage.local.set({ backgroundEffect: enabled ? \"blur\" : \"\" });\n\n    return enabled;\n  };\n\n  const setCustomEffect = async (effectUrl) => {\n    try {\n      const success = await loadCustomEffect(effectUrl);\n\n      if (success) {\n        blurRef.current = false;\n\n        chrome.storage.local.set({ backgroundEffect: effectUrl });\n\n        return true;\n      }\n\n      return false;\n    } catch (error) {\n      console.error(\"Error setting custom effect:\", error);\n      return false;\n    }\n  };\n\n  const clearEffect = () => {\n    blurRef.current = false;\n    effectRef.current = null;\n\n    chrome.storage.local.set({ backgroundEffect: \"\" });\n\n    return true;\n  };\n\n  const contextValue = {\n    width,\n    height,\n    backgroundEffects,\n    isModelLoaded,\n    pipMode,\n    isCameraMode,\n    videoRef,\n    streamRef,\n    recordingTypeRef,\n    offScreenCanvasRef,\n    offScreenCanvasContextRef,\n    segmenterRef,\n    blurRef,\n    effectRef,\n    setWidth: handleSetWidth,\n    setHeight: handleSetHeight,\n    setBackgroundEffects: handleSetBackgroundEffects,\n    setPipMode,\n    setIsCameraMode,\n    loadCustomEffect,\n    enableBlur,\n    setCustomEffect,\n    clearEffect,\n  };\n\n  return (\n    <CameraContext.Provider value={contextValue}>\n      {children}\n    </CameraContext.Provider>\n  );\n};\n\nexport default CameraProvider;\n"
  },
  {
    "path": "src/pages/Camera/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n\t\t<style>\n\t\t\thtml, body, #app-container {\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tmargin: 0px;\n\t\t\t\tpadding: 0px;\n\t\t\t\toverflow: hidden;\n\t\t\t}\n\t\t</style>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Camera/index.jsx",
    "content": "// src/index.js\nimport React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport CameraProvider from \"./context/CameraContext\";\nimport Camera from \"./Camera\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(\n    <CameraProvider>\n      <Camera />\n    </CameraProvider>\n  );\n}\n\n// Hot Module Replacement\nif (import.meta.webpackHot) {\n  import.meta.webpackHot.accept();\n}\n"
  },
  {
    "path": "src/pages/Camera/messaging/handlers.js",
    "content": "import {\n  registerMessage,\n  messageRouter,\n} from \"../../../messaging/messageRouter\";\nimport { getContextRefs } from \"../context/CameraContext\";\nimport {\n  getCameraStream,\n  stopCameraStream,\n  togglePip,\n  surfaceHandler,\n  cameraToggledToolbar,\n} from \"../utils/cameraUtils\";\nimport { loadEffect } from \"../utils/backgroundUtils\";\nimport {\n  setWidth,\n  setHeight,\n  setPipMode,\n  setBackgroundEffects,\n} from \"../utils/uiState\";\n\nfunction waitForVideoRef(callback, attempts = 10) {\n  const { videoRef } = getContextRefs();\n\n  if (videoRef?.current) {\n    callback(videoRef.current);\n  } else if (attempts > 0) {\n    setTimeout(() => waitForVideoRef(callback, attempts - 1), 100);\n  }\n}\n\nexport const setupHandlers = ({ setLoading }) => {\n  registerMessage(\"toggle-blur\", handleToggleBlur);\n  registerMessage(\"load-custom-effect\", handleLoadCustomEffect);\n  registerMessage(\"set-background-effect\", handleSetBackgroundEffect);\n  registerMessage(\"stop-recording\", handleStopRecording);\n  registerMessage(\"dismiss-recording\", handleStopRecording);\n\n  let cameraSwitchTimeout;\n  registerMessage(\"switch-camera\", (message) => {\n    if (message.id !== \"none\") {\n      clearTimeout(cameraSwitchTimeout);\n      stopCameraStream();\n\n      cameraSwitchTimeout = setTimeout(() => {\n        const {\n          videoRef,\n          streamRef,\n          offScreenCanvasRef,\n          offScreenCanvasContextRef,\n        } = getContextRefs();\n\n        getCameraStream(\n          { video: { deviceId: { exact: message.id } } },\n          streamRef,\n          videoRef,\n          offScreenCanvasRef,\n          offScreenCanvasContextRef,\n          {\n            onStart: () => setLoading(\"videoElement\", true),\n            onFinish: () => setLoading(\"videoElement\", false),\n          },\n        );\n      }, 500);\n    }\n  });\n\n  registerMessage(\"background-effects-active\", () =>\n    setBackgroundEffects(true),\n  );\n  registerMessage(\"background-effects-inactive\", () =>\n    setBackgroundEffects(false),\n  );\n  registerMessage(\"camera-only-update\", handleCameraOnlyUpdate);\n  registerMessage(\"screen-update\", handleScreenUpdate);\n  registerMessage(\"toggle-pip\", () => togglePip(getContextRefs().videoRef));\n  registerMessage(\"set-surface\", (message) => {\n    console.log(\"Preparing Picture in Picture request\");\n\n    // Try synchronously first to preserve user gesture context for PiP.\n    // If videoRef isn't ready yet, fall back to polling (PiP may fail\n    // without gesture, but surfaceHandler handles that gracefully).\n    const { videoRef } = getContextRefs();\n    if (videoRef?.current) {\n      surfaceHandler(message, videoRef);\n    } else {\n      waitForVideoRef((videoEl) => {\n        surfaceHandler(message, { current: videoEl });\n      });\n    }\n  });\n  registerMessage(\"camera-toggled-toolbar\", cameraToggledToolbar);\n  registerMessage(\"turn-off-pip\", () => {\n    if (document.pictureInPictureElement) {\n      document.exitPictureInPicture().catch((error) => {\n        console.error(\"Failed to exit Picture in Picture:\", error);\n      });\n    }\n    setPipMode(false);\n    chrome.runtime.sendMessage({ type: \"pip-ended\" });\n  });\n\n  messageRouter();\n\n  // Fallback: if a runtime message is missed, close PiP when storage flag flips.\n  chrome.storage.local.get([\"pipForceClose\"], (res) => {\n    if (res.pipForceClose && document.pictureInPictureElement) {\n      document.exitPictureInPicture().catch(() => {});\n      setPipMode(false);\n      chrome.runtime.sendMessage({ type: \"pip-ended\" });\n    }\n  });\n  chrome.storage.onChanged.addListener((changes, area) => {\n    if (area !== \"local\") return;\n    if (!changes.pipForceClose) return;\n    if (document.pictureInPictureElement) {\n      document.exitPictureInPicture().catch(() => {});\n      setPipMode(false);\n      chrome.runtime.sendMessage({ type: \"pip-ended\" });\n    }\n  });\n};\n\nconst handleStopRecording = async (request) => {\n  if (document.pictureInPictureElement) {\n    document.exitPictureInPicture();\n    setPipMode(false);\n    chrome.runtime.sendMessage({ type: \"pip-ended\" });\n  }\n};\n\nconst safelyApplyFilter = (contextRef, filter) => {\n  if (contextRef.current) {\n    try {\n      contextRef.current.filter = filter;\n    } catch (error) {\n      console.warn(\"⚠️ Failed to apply filter:\", error.message);\n    }\n  }\n};\n\nconst handleSetBackgroundEffect = async (message) => {\n  const { blurRef, effectRef, offScreenCanvasContextRef } = getContextRefs();\n\n  await chrome.storage.local.set({ backgroundEffect: message.effect });\n\n  if (message.effect === \"blur\") {\n    blurRef.current = true;\n    effectRef.current = null;\n    safelyApplyFilter(offScreenCanvasContextRef, \"blur(5px)\");\n  } else if (message.effect) {\n    blurRef.current = false;\n\n    try {\n      const effectImage = await loadEffect(message.effect);\n      effectRef.current = effectImage;\n      safelyApplyFilter(offScreenCanvasContextRef, \"none\");\n    } catch (err) {\n      console.error(\"Failed to load effect:\", err);\n    }\n  } else {\n    blurRef.current = false;\n    effectRef.current = null;\n    safelyApplyFilter(offScreenCanvasContextRef, \"none\");\n  }\n};\n\nconst handleToggleBlur = async (message) => {\n  const { blurRef, offScreenCanvasContextRef } = getContextRefs();\n  const enabled = message.enabled ?? !blurRef.current;\n\n  blurRef.current = enabled;\n\n  await chrome.storage.local.set({ backgroundEffect: enabled ? \"blur\" : \"\" });\n\n  safelyApplyFilter(offScreenCanvasContextRef, enabled ? \"blur(5px)\" : \"none\");\n};\n\nconst handleLoadCustomEffect = async (message) => {\n  if (!message.effectUrl) {\n    console.warn(\"⚠️ No effect URL provided\");\n    return;\n  }\n\n  const { blurRef, effectRef, offScreenCanvasContextRef } = getContextRefs();\n\n  try {\n    const effectUrl = await loadEffect(message.effectUrl);\n    blurRef.current = false;\n    effectRef.current = message.effectUrl;\n\n    await chrome.storage.local.set({ backgroundEffect: message.effectUrl });\n\n    safelyApplyFilter(offScreenCanvasContextRef, \"none\");\n  } catch (error) {\n    console.error(\"Failed to load custom effect:\", error);\n  }\n};\n\nconst handleCameraOnlyUpdate = () => {\n  const { recordingTypeRef, setWidth, setHeight, setIsCameraMode } =\n    getContextRefs();\n\n  if (setWidth && setHeight) {\n    setWidth(\"auto\");\n    setHeight(\"100%\");\n  }\n\n  setIsCameraMode(true);\n  recordingTypeRef.current = \"camera\";\n};\n\nconst handleScreenUpdate = () => {\n  const { videoRef, recordingTypeRef, setWidth, setHeight, setIsCameraMode } =\n    getContextRefs();\n\n  if (!videoRef.current || !setWidth || !setHeight) {\n    console.warn(\"⚠️ Missing required refs for screen update\");\n    return;\n  }\n\n  setIsCameraMode(false);\n\n  const { videoWidth, videoHeight } = videoRef.current;\n\n  if (videoWidth > videoHeight) {\n    setWidth(\"auto\");\n    setHeight(\"100%\");\n  } else {\n    setWidth(\"100%\");\n    setHeight(\"auto\");\n  }\n\n  recordingTypeRef.current = \"screen\";\n};\n"
  },
  {
    "path": "src/pages/Camera/utils/backgroundUtils.js",
    "content": "import {\n  FilesetResolver,\n  ImageSegmenter,\n} from \"@mediapipe/tasks-vision\";\n\n// Pre-allocated canvases reused across frames to avoid GC pressure\nlet _frameCanvas = null;\nlet _frameCtx = null;\nlet _blurCanvas = null;\nlet _blurCtx = null;\nlet _maskCanvas = null;\nlet _maskCtx = null;\nlet _smoothMaskCanvas = null;\nlet _smoothMaskCtx = null;\nlet _personCanvas = null;\nlet _personCtx = null;\n\nfunction getReusableCanvases() {\n  if (!_frameCanvas) {\n    _frameCanvas = document.createElement(\"canvas\");\n    _frameCtx = _frameCanvas.getContext(\"2d\");\n    _blurCanvas = document.createElement(\"canvas\");\n    _blurCtx = _blurCanvas.getContext(\"2d\");\n    _maskCanvas = document.createElement(\"canvas\");\n    _maskCtx = _maskCanvas.getContext(\"2d\");\n    _smoothMaskCanvas = document.createElement(\"canvas\");\n    _smoothMaskCtx = _smoothMaskCanvas.getContext(\"2d\");\n    _personCanvas = document.createElement(\"canvas\");\n    _personCtx = _personCanvas.getContext(\"2d\");\n  }\n  return {\n    frameCanvas: _frameCanvas, frameCtx: _frameCtx,\n    blurCanvas: _blurCanvas, blurCtx: _blurCtx,\n    maskCanvas: _maskCanvas, maskCtx: _maskCtx,\n    smoothMaskCanvas: _smoothMaskCanvas, smoothMaskCtx: _smoothMaskCtx,\n    personCanvas: _personCanvas, personCtx: _personCtx,\n  };\n}\n\n// Pre-allocated typed array for mask ImageData, reused across frames\nlet _maskImageDataCache = null;\nlet _maskImageDataCacheSize = 0;\n\nfunction getOrCreateMaskImageData(ctx, width, height) {\n  const size = width * height;\n  if (_maskImageDataCache && _maskImageDataCacheSize === size) {\n    return _maskImageDataCache;\n  }\n  _maskImageDataCache = ctx.createImageData(width, height);\n  _maskImageDataCacheSize = size;\n  return _maskImageDataCache;\n}\n\nexport const loadSegmentationModel = async () => {\n  try {\n    const vision = await FilesetResolver.forVisionTasks(\n      \"./assets/mediapipeVision\"\n    );\n\n    const segmenter = await ImageSegmenter.createFromOptions(vision, {\n      baseOptions: {\n        modelAssetPath: \"./assets/mediapipeVision/selfie_segmenter.tflite\",\n        delegate: \"GPU\",\n      },\n      outputCategoryMask: true,\n      outputConfidenceMasks: false,\n      runningMode: \"VIDEO\",\n    });\n\n    return segmenter;\n  } catch (error) {\n    console.error(\"Error loading segmentation model:\", error);\n    return null;\n  }\n};\n\n// Segment directly from a video element — avoids ImageData round-trips.\n// The segmentForVideo callback fires synchronously, so this returns the result directly.\nexport const segmentFromVideo = (videoElement, segmenter) => {\n  if (!videoElement || !segmenter) return null;\n  if (videoElement.readyState < 2 || videoElement.videoWidth === 0) return null;\n\n  try {\n    const timestampMs = performance.now();\n    let result = null;\n\n    segmenter.segmentForVideo(videoElement, timestampMs, (r) => {\n      result = r;\n    });\n\n    if (!result || !result.categoryMask) return null;\n    return result;\n  } catch (error) {\n    console.error(\"Error during segmentation:\", error);\n    return null;\n  }\n};\n\n// Build a smoothed person mask from segmentation result onto reusable canvases.\n// Shared by both blur and cutout render paths.\nfunction buildSmoothedMask(segmentationResult, edgeBlurAmount, personOpaque) {\n  const mask = segmentationResult.categoryMask;\n  const maskData = mask.getAsUint8Array();\n  const maskW = mask.width;\n  const maskH = mask.height;\n\n  const { maskCanvas, maskCtx, smoothMaskCanvas, smoothMaskCtx } = getReusableCanvases();\n\n  if (maskCanvas.width !== maskW || maskCanvas.height !== maskH) {\n    maskCanvas.width = maskW;\n    maskCanvas.height = maskH;\n    smoothMaskCanvas.width = maskW;\n    smoothMaskCanvas.height = maskH;\n  }\n\n  const maskImageData = getOrCreateMaskImageData(maskCtx, maskW, maskH);\n  const maskPixels = maskImageData.data;\n\n  for (let i = 0; i < maskData.length; i++) {\n    const offset = i * 4;\n    const isPerson = maskData[i] === 1;\n    if (personOpaque) {\n      // Person = opaque white, background = transparent (for blur + cutout)\n      maskPixels[offset] = 255;\n      maskPixels[offset + 1] = 255;\n      maskPixels[offset + 2] = 255;\n      maskPixels[offset + 3] = isPerson ? 255 : 0;\n    } else {\n      // Person = transparent, background = opaque black (for destination-out masking)\n      maskPixels[offset] = 0;\n      maskPixels[offset + 1] = 0;\n      maskPixels[offset + 2] = 0;\n      maskPixels[offset + 3] = isPerson ? 0 : 255;\n    }\n  }\n  maskCtx.putImageData(maskImageData, 0, 0);\n\n  smoothMaskCtx.clearRect(0, 0, maskW, maskH);\n  smoothMaskCtx.filter = `blur(${edgeBlurAmount}px)`;\n  smoothMaskCtx.drawImage(maskCanvas, 0, 0);\n  smoothMaskCtx.filter = \"none\";\n\n  return { smoothMaskCanvas, maskW, maskH };\n}\n\n// Render blur effect using the video element directly (no ImageData round-trip)\nexport const renderBlurFromVideo = (videoElement, segmentationResult, canvasRef) => {\n  if (!videoElement || !segmentationResult || !canvasRef.current) return false;\n\n  try {\n    const backgroundBlurAmount = 16;\n    const edgeBlurAmount = 8;\n\n    const vw = videoElement.videoWidth;\n    const vh = videoElement.videoHeight;\n\n    const { frameCanvas, frameCtx, blurCanvas, blurCtx, personCanvas, personCtx } =\n      getReusableCanvases();\n\n    // Resize reusable canvases only when dimensions change\n    if (frameCanvas.width !== vw || frameCanvas.height !== vh) {\n      frameCanvas.width = vw;\n      frameCanvas.height = vh;\n      blurCanvas.width = vw;\n      blurCanvas.height = vh;\n    }\n\n    // Draw original frame from video\n    frameCtx.drawImage(videoElement, 0, 0);\n\n    // Draw blurred version\n    blurCtx.filter = `blur(${backgroundBlurAmount}px)`;\n    blurCtx.drawImage(frameCanvas, 0, 0);\n    blurCtx.filter = \"none\";\n\n    // Build smoothed person mask\n    const { smoothMaskCanvas, maskW, maskH } =\n      buildSmoothedMask(segmentationResult, edgeBlurAmount, true);\n\n    // Size output canvas\n    const ratio = vw / vh;\n    const outW = Math.round(window.innerHeight * ratio);\n    const outH = window.innerHeight;\n\n    if (canvasRef.current.width !== outW || canvasRef.current.height !== outH) {\n      canvasRef.current.width = outW;\n      canvasRef.current.height = outH;\n    }\n\n    if (personCanvas.width !== outW || personCanvas.height !== outH) {\n      personCanvas.width = outW;\n      personCanvas.height = outH;\n    }\n\n    const outputCtx = canvasRef.current.getContext(\"2d\");\n\n    // Base layer: blurred frame\n    outputCtx.drawImage(blurCanvas, 0, 0, vw, vh, 0, 0, outW, outH);\n\n    // Person layer: mask → source-in with sharp frame\n    personCtx.clearRect(0, 0, outW, outH);\n    personCtx.globalCompositeOperation = \"source-over\";\n    personCtx.drawImage(smoothMaskCanvas, 0, 0, maskW, maskH, 0, 0, outW, outH);\n    personCtx.globalCompositeOperation = \"source-in\";\n    personCtx.drawImage(frameCanvas, 0, 0, vw, vh, 0, 0, outW, outH);\n\n    // Composite person on top of blurred background\n    outputCtx.drawImage(personCanvas, 0, 0);\n\n    return true;\n  } catch (error) {\n    console.error(\"Error rendering blur:\", error);\n    return false;\n  }\n};\n\n// Render person cutout (transparent background) using the video element directly\nexport const renderPersonCutoutFromVideo = (\n  videoElement,\n  segmentationResult,\n  canvasRef,\n  canvasContextRef,\n) => {\n  if (!videoElement || !segmentationResult || !canvasRef.current) return false;\n\n  try {\n    const edgeBlurAmount = 4;\n\n    const vw = videoElement.videoWidth;\n    const vh = videoElement.videoHeight;\n\n    const { frameCanvas, frameCtx, personCanvas, personCtx } = getReusableCanvases();\n\n    if (frameCanvas.width !== vw || frameCanvas.height !== vh) {\n      frameCanvas.width = vw;\n      frameCanvas.height = vh;\n    }\n\n    // Draw original frame from video\n    frameCtx.drawImage(videoElement, 0, 0);\n\n    // Build smoothed person mask\n    const { smoothMaskCanvas, maskW, maskH } =\n      buildSmoothedMask(segmentationResult, edgeBlurAmount, true);\n\n    const canvas = canvasRef.current;\n    const ctx = canvasContextRef.current;\n\n    if (personCanvas.width !== canvas.width || personCanvas.height !== canvas.height) {\n      personCanvas.width = canvas.width;\n      personCanvas.height = canvas.height;\n    }\n\n    // Composite: mask → source-in with frame = person-only with smooth edges\n    personCtx.clearRect(0, 0, canvas.width, canvas.height);\n    personCtx.globalCompositeOperation = \"source-over\";\n    personCtx.drawImage(smoothMaskCanvas, 0, 0, maskW, maskH, 0, 0, canvas.width, canvas.height);\n    personCtx.globalCompositeOperation = \"source-in\";\n    personCtx.drawImage(frameCanvas, 0, 0, vw, vh, 0, 0, canvas.width, canvas.height);\n\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    ctx.drawImage(personCanvas, 0, 0);\n\n    return true;\n  } catch (error) {\n    console.error(\"Error rendering person cutout:\", error);\n    return false;\n  }\n};\n\nexport const loadEffect = (effectUrl) => {\n  return new Promise((resolve, reject) => {\n    if (!effectUrl) {\n      resolve(null);\n      return;\n    }\n\n    const img = new Image();\n    img.src = effectUrl;\n    img.onload = () => resolve(img);\n    img.onerror = (error) => {\n      console.error(\"Failed to load effect image:\", error);\n      reject(error);\n    };\n  });\n};\n\n// Render the background effect image to the bottom canvas\nexport const renderEffectBackground = (\n  effectImg,\n  bottomCanvasRef,\n  bottomCanvasContextRef\n) => {\n  if (!effectImg || !bottomCanvasRef.current || !bottomCanvasContextRef.current)\n    return false;\n\n  try {\n    const canvas = bottomCanvasRef.current;\n    const ctx = bottomCanvasContextRef.current;\n\n    canvas.width = window.innerWidth;\n    canvas.height = window.innerHeight;\n\n    // Calculate dimensions to cover the canvas while maintaining aspect ratio\n    const imgRatio = effectImg.width / effectImg.height;\n    const canvasRatio = canvas.width / canvas.height;\n    let drawWidth = canvas.width;\n    let drawHeight = canvas.height;\n\n    if (canvasRatio > imgRatio) {\n      drawWidth = canvas.height * imgRatio;\n      drawHeight = canvas.height;\n    } else {\n      drawWidth = canvas.width;\n      drawHeight = canvas.width / imgRatio;\n    }\n\n    const x = (canvas.width - drawWidth) / 2;\n    const y = (canvas.height - drawHeight) / 2;\n\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    ctx.drawImage(effectImg, x, y, drawWidth, drawHeight);\n\n    return true;\n  } catch (error) {\n    console.error(\"Error rendering effect background:\", error);\n    return false;\n  }\n};\n"
  },
  {
    "path": "src/pages/Camera/utils/cameraUtils.js",
    "content": "import { getContextRefs } from \"../context/CameraContext\";\nimport { setPipMode } from \"./uiState\";\nimport { getUserMediaWithFallback } from \"../../utils/mediaDeviceFallback\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nexport const getCameraStream = async (\n  constraints,\n  streamRef,\n  videoRef,\n  offScreenCanvasRef,\n  offScreenCanvasContextRef,\n  { onStart = () => {}, onFinish = () => {} } = {},\n) => {\n  const { setWidth, setHeight, recordingTypeRef } = getContextRefs();\n\n  onStart();\n\n  if (!streamRef) {\n    console.warn(\"⚠️ streamRef is undefined. Creating a new reference.\");\n    streamRef = { current: new MediaStream() };\n  }\n\n  if (!videoRef) {\n    console.warn(\"⚠️ videoRef is undefined. Creating a new reference.\");\n    videoRef = { current: null };\n  }\n\n  if (!offScreenCanvasRef) {\n    console.warn(\n      \"⚠️ offScreenCanvasRef is undefined. Creating a new reference.\",\n    );\n    offScreenCanvasRef = { current: null };\n  }\n\n  if (!offScreenCanvasContextRef) {\n    console.warn(\n      \"⚠️ offScreenCanvasContextRef is undefined. Creating a new reference.\",\n    );\n    offScreenCanvasContextRef = { current: null };\n  }\n\n  try {\n    const desiredVideoId = constraints?.video?.deviceId?.exact || null;\n    let desiredVideoLabel = \"\";\n\n    if (desiredVideoId) {\n      const { defaultVideoInputLabel, videoinput, videoInput } =\n        await chrome.storage.local.get([\n          \"defaultVideoInputLabel\",\n          \"videoinput\",\n          \"videoInput\",\n        ]);\n      const storedDevices = Array.isArray(videoinput)\n        ? videoinput\n        : Array.isArray(videoInput)\n        ? videoInput\n        : [];\n      desiredVideoLabel =\n        defaultVideoInputLabel ||\n        storedDevices.find((device) => device.deviceId === desiredVideoId)\n          ?.label ||\n        \"\";\n    }\n\n    const stream = await getUserMediaWithFallback({\n      constraints,\n      fallbacks:\n        desiredVideoId && desiredVideoLabel\n          ? [\n              {\n                kind: \"videoinput\",\n                desiredDeviceId: desiredVideoId,\n                desiredLabel: desiredVideoLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultVideoInput: resolvedId,\n                    defaultVideoInputLabel: desiredVideoLabel,\n                  });\n                },\n              },\n            ]\n          : [],\n    });\n\n    streamRef.current = stream;\n\n    const videoTrack = stream.getVideoTracks()[0];\n    const { width, height, deviceId, frameRate } = videoTrack.getSettings();\n\n    if (setWidth && setHeight) {\n      const isCamera = recordingTypeRef?.current === \"camera\";\n      const w = isCamera || width / height < 1 ? \"100%\" : \"auto\";\n      const h = isCamera || width / height < 1 ? \"auto\" : \"100%\";\n      setWidth(w);\n      setHeight(h);\n    } else {\n      console.warn(\"⚠️ setWidth or setHeight not available from context.\");\n    }\n\n    // VIDEO REF SETUP\n    if (!videoRef.current) {\n      console.warn(\"⏳ videoRef not ready yet. Will poll every 100ms.\");\n      const maxWaitTime = 5000;\n      const startTime = Date.now();\n\n      const intervalId = setInterval(() => {\n        if (videoRef.current) {\n          videoRef.current.srcObject = stream;\n          clearInterval(intervalId);\n        } else if (Date.now() - startTime > maxWaitTime) {\n          console.warn(\"⚠️ Timed out waiting for videoRef.current\");\n          clearInterval(intervalId);\n        }\n      }, 100);\n      return;\n    }\n\n    const video = videoRef.current;\n    video.srcObject = stream;\n\n    await new Promise((resolve) => {\n      video.onloadedmetadata = async () => {\n        await video.play().catch((err) => {\n          console.error(\"❌ Error during video.play():\", err);\n        });\n\n        await new Promise((r) => setTimeout(r, 100));\n\n        const canvas = offScreenCanvasRef?.current;\n        if (!canvas) {\n          console.warn(\"⚠️ offScreenCanvasRef.current is null.\");\n          return resolve();\n        }\n\n        canvas.width = video.videoWidth || 1280;\n        canvas.height = video.videoHeight || 720;\n\n        const context = canvas.getContext(\"2d\");\n        if (!context) {\n          console.error(\"❌ Failed to get 2D context from canvas.\");\n          return resolve();\n        }\n\n        offScreenCanvasContextRef.current = context;\n\n        resolve();\n      };\n    });\n\n    onFinish();\n  } catch (err) {\n    console.error(\"❌ Failed to get camera stream:\", err);\n    onFinish();\n  }\n};\n\nexport const stopCameraStream = (streamRef, videoRef) => {\n  if (!streamRef?.current) {\n    console.warn(\"⚠️ No active stream to stop.\");\n    return;\n  }\n\n  const stream = streamRef.current;\n\n  stream.getTracks().forEach((track) => track.stop());\n\n  if (videoRef && videoRef.current) {\n    videoRef.current.srcObject = null;\n  }\n};\n\nexport const togglePip = (videoRef) => {\n  if (document.pictureInPictureElement) {\n    document.exitPictureInPicture();\n  } else {\n    try {\n      videoRef.current.requestPictureInPicture().catch(() => {\n        setPipMode(false);\n        chrome.runtime.sendMessage({ type: \"pip-ended\" });\n      });\n    } catch (error) {\n      setPipMode(false);\n      chrome.runtime.sendMessage({ type: \"pip-ended\" });\n    }\n  }\n};\nexport const surfaceHandler = async (request, videoRef) => {\n  console.log(\"Picture in Picture request ready\");\n\n  if (\n    !videoRef?.requestPictureInPicture &&\n    !videoRef?.current?.requestPictureInPicture\n  ) {\n    setPipMode(false);\n    chrome.runtime.sendMessage({ type: \"pip-ended\" });\n    return;\n  }\n\n  if (!CLOUD_FEATURES_ENABLED) {\n    const shouldEnterPip = request.surface === \"monitor\";\n    if (shouldEnterPip && videoRef.current) {\n      try {\n        await videoRef.current.requestPictureInPicture();\n      } catch (err) {\n        console.error(\"❌ Failed to enter PiP:\", err);\n        setPipMode(false);\n        chrome.runtime.sendMessage({ type: \"pip-ended\" });\n      }\n    }\n    return;\n  }\n\n  // Auth data is passed along with the set-surface message from the background\n  // script (setSurface.js) to avoid an async round-trip that would lose the\n  // user gesture context required by requestPictureInPicture().\n  try {\n    const isSubscribed = request.subscribed || false;\n    const instantMode = request.instantMode || false;\n\n    const shouldEnterPip =\n      (request.surface === \"monitor\" && (!isSubscribed || instantMode)) ||\n      (request.surface !== \"monitor\" && isSubscribed && !instantMode);\n\n    if (shouldEnterPip && videoRef.current) {\n      await videoRef.current.requestPictureInPicture();\n    }\n  } catch (error) {\n    console.error(\"❌ Failed to enter Picture in Picture:\", error);\n    setPipMode(false);\n    chrome.runtime.sendMessage({ type: \"pip-ended\" });\n  }\n};\n\nexport const cameraToggledToolbar = async (request) => {\n  if (request.active) {\n    setTimeout(() => {\n      stopCameraStream();\n      getCameraStream({\n        video: {\n          deviceId: { exact: request.id },\n        },\n      });\n    }, 2000);\n    setPipMode(false);\n  }\n};\n"
  },
  {
    "path": "src/pages/Camera/utils/canvasUtils.js",
    "content": "export const initializeCanvases = () => {\n  const offScreenCanvas = document.createElement(\"canvas\");\n  const offScreenCanvasContext = offScreenCanvas.getContext(\"2d\", {\n    willReadFrequently: true,\n  });\n\n  return {\n    offScreenCanvas,\n    offScreenCanvasContext,\n  };\n};\n\nexport const setupCanvasContexts = (canvasRef, bottomCanvasRef) => {\n  if (!canvasRef.current || !bottomCanvasRef.current) {\n    console.error(\"Canvas references not available\");\n    return {\n      canvasContext: null,\n      bottomCanvasContext: null,\n    };\n  }\n\n  const canvasContext = canvasRef.current.getContext(\"2d\", {\n    willReadFrequently: true,\n  });\n\n  const bottomCanvasContext = bottomCanvasRef.current.getContext(\"2d\", {\n    willReadFrequently: true,\n  });\n\n  return {\n    canvasContext,\n    bottomCanvasContext,\n  };\n};\n\nexport const resizeCanvases = (\n  videoWidth,\n  videoHeight,\n  isBackgroundEffect,\n  effectImg,\n  canvasRef,\n  bottomCanvasRef,\n  bottomCanvasContextRef\n) => {\n  if (!canvasRef.current) return false;\n\n  const windowWidth = window.innerWidth;\n  const windowHeight = window.innerHeight;\n  const ratio = videoWidth / videoHeight;\n\n  canvasRef.current.width = windowHeight * ratio;\n  canvasRef.current.height = windowHeight;\n\n  if (isBackgroundEffect && bottomCanvasRef.current && effectImg) {\n    bottomCanvasRef.current.width = windowWidth;\n    bottomCanvasRef.current.height = windowHeight;\n\n    if (bottomCanvasContextRef.current && effectImg) {\n      bottomCanvasContextRef.current.drawImage(\n        effectImg,\n        0,\n        0,\n        effectImg.width,\n        effectImg.height,\n        0,\n        0,\n        windowWidth,\n        windowHeight\n      );\n    }\n  }\n\n  return true;\n};\n\nexport const calculateCanvasDimensions = (videoWidth, videoHeight) => {\n  const windowWidth = window.innerWidth;\n  const windowHeight = window.innerHeight;\n  const videoRatio = videoWidth / videoHeight;\n  const windowRatio = windowWidth / windowHeight;\n\n  let width, height;\n\n  if (videoRatio > windowRatio) {\n    width = windowWidth;\n    height = windowWidth / videoRatio;\n  } else {\n    height = windowHeight;\n    width = windowHeight * videoRatio;\n  }\n\n  return { width, height };\n};\n\nexport const captureVideoFrame = (videoRef, canvasRef, canvasContextRef) => {\n  if (!videoRef.current || !canvasRef.current || !canvasContextRef.current) {\n    return null;\n  }\n\n  const video = videoRef.current;\n  const canvas = canvasRef.current;\n  const context = canvasContextRef.current;\n\n  canvas.width = video.videoWidth;\n  canvas.height = video.videoHeight;\n\n  context.drawImage(video, 0, 0, canvas.width, canvas.height);\n\n  return context.getImageData(0, 0, canvas.width, canvas.height);\n};\n\nexport const clearCanvases = (canvasRefs) => {\n  const {\n    canvasRef,\n    canvasContextRef,\n    offScreenCanvasRef,\n    offScreenCanvasContextRef,\n    bottomCanvasRef,\n    bottomCanvasContextRef,\n  } = canvasRefs;\n\n  if (canvasRef.current && canvasContextRef.current) {\n    canvasContextRef.current.clearRect(\n      0,\n      0,\n      canvasRef.current.width,\n      canvasRef.current.height\n    );\n  }\n\n  if (offScreenCanvasRef.current && offScreenCanvasContextRef.current) {\n    offScreenCanvasContextRef.current.clearRect(\n      0,\n      0,\n      offScreenCanvasRef.current.width,\n      offScreenCanvasRef.current.height\n    );\n  }\n\n  if (bottomCanvasRef.current && bottomCanvasContextRef.current) {\n    bottomCanvasContextRef.current.clearRect(\n      0,\n      0,\n      bottomCanvasRef.current.width,\n      bottomCanvasRef.current.height\n    );\n  }\n};\n"
  },
  {
    "path": "src/pages/Camera/utils/effects.js",
    "content": "import { renderEffectBackground } from \"./backgroundUtils\";\nimport { getContextRefs } from \"../context/CameraContext\";\n\nexport const loadEffect = (effectUrl) => {\n  return new Promise((resolve, reject) => {\n    if (!effectUrl) {\n      console.warn(\"No effect URL provided\");\n      resolve(null);\n      return;\n    }\n\n    const { effectRef, blurRef, bottomCanvasRef, bottomCanvasContextRef } =\n      getContextRefs();\n\n    const img = new Image();\n    img.src = effectUrl;\n\n    img.onload = () => {\n      effectRef.current = img;\n      blurRef.current = false;\n\n      renderEffectBackground(img, bottomCanvasRef, bottomCanvasContextRef);\n\n      chrome.storage.local.set({ backgroundEffect: effectUrl });\n\n      resolve(img);\n    };\n\n    // Handle load failure\n    img.onerror = (error) => {\n      console.error(`❌ Failed to load effect: ${effectUrl}`, error);\n      reject(error);\n    };\n  });\n};\n\nexport const clearAllEffects = () => {\n  const { blurRef, effectRef } = getContextRefs();\n\n  blurRef.current = false;\n  effectRef.current = null;\n\n  chrome.storage.local.set({ backgroundEffect: \"\" });\n\n  return true;\n};\n\nexport const toggleBlur = (enabled) => {\n  const { blurRef, effectRef } = getContextRefs();\n\n  const newState = enabled !== undefined ? enabled : !blurRef.current;\n\n  blurRef.current = newState;\n\n  if (newState) {\n    effectRef.current = null;\n  }\n\n  chrome.storage.local.set({ backgroundEffect: newState ? \"blur\" : \"\" });\n\n  return newState;\n};\n\nexport const getCurrentEffect = () => {\n  const { blurRef, effectRef } = getContextRefs();\n\n  return {\n    isBlurEnabled: blurRef.current,\n    hasCustomEffect: effectRef.current !== null,\n    customEffectUrl: effectRef.current ? effectRef.current.src : null,\n  };\n};\n\n/**\n * Apply saved effect settings at startup\n */\nexport const applySavedEffectSettings = async () => {\n  try {\n    const result = await new Promise((resolve) => {\n      chrome.storage.local.get([\"backgroundEffect\"], resolve);\n    });\n\n    if (result.backgroundEffect === \"blur\") {\n      toggleBlur(true);\n    } else if (result.backgroundEffect) {\n      await loadEffect(result.backgroundEffect);\n    }\n\n    return true;\n  } catch (error) {\n    console.error(\"Error applying saved effect settings:\", error);\n    return false;\n  }\n};\n"
  },
  {
    "path": "src/pages/Camera/utils/uiState.js",
    "content": "import { getContextRefs } from \"../context/CameraContext\";\n\nexport const setWidth = (width) => {\n  const { videoRef, setWidth: contextSetWidth } = getContextRefs();\n  if (contextSetWidth) {\n    contextSetWidth(width);\n  } else if (videoRef?.current) {\n    videoRef.current.style.width = width;\n  }\n};\n\nexport const setHeight = (height) => {\n  const { videoRef, setHeight: contextSetHeight } = getContextRefs();\n  if (contextSetHeight) {\n    contextSetHeight(height);\n  } else if (videoRef?.current) {\n    videoRef.current.style.height = height;\n  }\n};\n\nexport const setPipMode = (mode) => {\n  const { videoRef, setPipMode: contextSetPipMode } = getContextRefs();\n  if (contextSetPipMode) {\n    contextSetPipMode(mode);\n  } else if (videoRef?.current) {\n    // Fallback behavior if context setter isn't available\n  }\n};\n\nexport const setBackgroundEffects = (active) => {\n  const { videoRef, setBackgroundEffects: contextSetBackgroundEffects } =\n    getContextRefs();\n\n  if (contextSetBackgroundEffects) {\n    contextSetBackgroundEffects(active);\n  } else {\n    chrome.storage.local.set({ backgroundEffectsActive: active }, () => {});\n\n    if (videoRef?.current) {\n      videoRef.current.style.display = !active ? \"block\" : \"none\";\n    }\n  }\n\n  return active;\n};\n"
  },
  {
    "path": "src/pages/CloudRecorder/CloudRecorder.jsx",
    "content": "import React, { useEffect, useState, useRef, useCallback } from \"react\";\nimport RecorderUI from \"./RecorderUI\";\nimport {\n  sendRecordingError as sendRecordingErrorBase,\n  sendStopRecording,\n} from \"./messaging\";\nimport { getBitrates, getResolutionForQuality } from \"./recorderConfig\";\nimport BunnyTusUploader from \"./bunnyTusUploader\";\nimport localforage from \"localforage\";\nimport { createVideoProject } from \"./createVideoProject\";\nimport { getUserMediaWithFallback } from \"../utils/mediaDeviceFallback\";\nimport { traceStep } from \"../utils/startFlowTrace\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\nconst API_BASE = process.env.SCREENITY_API_BASE_URL;\nconst DEBUG_START_FLOW =\n  typeof window !== \"undefined\" ? !!window.SCREENITY_DEBUG_RECORDER : false;\nconst SCREEN_CHUNK_MEMORY_WINDOW = 8;\nconst CAMERA_CHUNK_MEMORY_WINDOW = 8;\nconst AUDIO_CHUNK_MEMORY_WINDOW = 8;\nconst STORAGE_CHECK_INTERVAL_MS = 5000;\nconst STORAGE_LOW_HEADROOM_BYTES = 250 * 1024 * 1024;\nconst STORAGE_CRITICAL_HEADROOM_BYTES = 120 * 1024 * 1024;\nconst AUDIO_MAX_BUFFER_BYTES = 150 * 1024 * 1024;\nconst LOCAL_SCREEN_PLAYBACK_TTL_MS = 20 * 60 * 1000;\nconst LOCAL_SCREEN_PLAYBACK_MAX_BYTES = 250 * 1024 * 1024;\nconst MAX_UPLOAD_TELEMETRY_EVENTS = 300;\nconst UPLOAD_TELEMETRY_KEY = \"cloudUploadTelemetryEvents\";\nconst UPLOAD_TELEMETRY_ENDPOINT = `${API_BASE}/log/upload-event`;\nconst SESSION_STATE_INDEX_KEY = \"cloudRecorderSessionStateIndex\";\nconst RECOVERABLE_SESSION_STATUSES = new Set([\n  \"recording\",\n  \"hidden\",\n  \"unload\",\n  \"stopping\",\n  \"finalize-failed\",\n  \"upload-stalled\",\n]);\n\nconst chunksStore = localforage.createInstance({ name: \"chunks\" });\nconst audioChunksStore = localforage.createInstance({ name: \"audioChunks\" });\nconst cameraChunksStore = localforage.createInstance({ name: \"cameraChunks\" });\n\nconst urlParams = new URLSearchParams(window.location.search);\nconst IS_INJECTED_IFRAME = urlParams.has(\"injected\");\nconst IS_IFRAME_CONTEXT =\n  IS_INJECTED_IFRAME ||\n  (window.top !== window.self &&\n    !document.referrer.startsWith(\"chrome-extension://\"));\n\nconst CloudRecorder = () => {\n  // Debug bundle removed; keep a no-op logger to preserve calls\n\n  const screenTimer = useRef({ start: null, total: 0, paused: false });\n  const cameraTimer = useRef({ start: null, total: 0, paused: false });\n  const [started, setStarted] = useState(false);\n  const [initProject, setInitProject] = useState(false);\n  const [finalizeFailure, setFinalizeFailure] = useState(null);\n  const finalizeFailureRef = useRef(null);\n  const finalizeContextRef = useRef(null);\n  const simulateFinalizeFailureConsumedRef = useRef(false);\n  const [retryingFinalize, setRetryingFinalize] = useState(false);\n\n  const retryFinalize = async () => {\n    setRetryingFinalize(true);\n    try {\n      // Attempt to re-run finalize flow by calling stopRecording with shouldFinalize=true\n      await stopRecording(true, \"retry-finalize\");\n    } catch (err) {\n      console.warn(\"Retry finalize failed:\", err);\n    } finally {\n      setRetryingFinalize(false);\n    }\n  };\n\n  const isTab = useRef(false);\n  const tabID = useRef(null);\n  const recordingTabId = useRef(null);\n  const tabPreferred = useRef(false);\n\n  const screenStream = useRef(null);\n  const cameraStream = useRef(null);\n  const micStream = useRef(null);\n  const rawMicStream = useRef(null);\n\n  const screenRecorder = useRef(null);\n  const cameraRecorder = useRef(null);\n  const audioRecorder = useRef(null);\n\n  const screenUploader = useRef(null);\n  const cameraUploader = useRef(null);\n  const uploadMetaRef = useRef(null);\n  const localScreenPlaybackOfferRef = useRef(null);\n  const emptyCleanupRef = useRef(false);\n\n  const backupRef = useRef(false);\n  const audioInputGain = useRef(null);\n  const audioOutputGain = useRef(null);\n\n  const uploadersInitialized = useRef(false);\n  const pendingStartRef = useRef(false);\n  const pendingStartAttempts = useRef(0);\n  const pendingStartTimer = useRef(null);\n\n  const isRestarting = useRef(false);\n  const isFinishing = useRef(false);\n  const sentLast = useRef(false);\n  const lastTimecode = useRef(0);\n  const hasChunks = useRef(false);\n  const index = useRef(0);\n\n  const target = useRef(null);\n  const regionRef = useRef(null);\n  const regionWidth = useRef(0);\n  const regionHeight = useRef(0);\n\n  const screenChunks = useRef([]);\n  const cameraChunks = useRef([]);\n  const audioChunks = useRef([]);\n\n  const recordingType = useRef(\"screen\");\n\n  const instantMode = useRef(false);\n\n  const consecutiveScreenFailures = useRef(0);\n  const consecutiveCameraFailures = useRef(0);\n  const firstChunkTime = useRef(null);\n  const firstChunkLoggedRef = useRef(false);\n\n  const recorderSession = useRef(null);\n  const recordingSessionId = useRef(null);\n  const unloadGuardRef = useRef({\n    pagehideSeen: false,\n    beforeUnloadSeen: false,\n    abandonedSent: false,\n    stopTriggeredFromUnload: false,\n  });\n  const telemetryRuntimeRef = useRef({\n    extensionVersion: null,\n    browserVersion: null,\n    platform: null,\n    arch: null,\n    os: null,\n  });\n  const uploadTelemetryTokenRef = useRef(null);\n  const uploadTelemetryNetworkDisabledRef = useRef(false);\n  const audioChunkStoreReadyRef = useRef(false);\n  const audioChunkIndexRef = useRef(0);\n  const cameraChunkStoreReadyRef = useRef(false);\n  const cameraChunkIndexRef = useRef(0);\n  const sessionStateIndexedRef = useRef(false);\n  const sessionHeartbeat = useRef(null);\n  const chunkStallTimer = useRef(null);\n  const uploadHeartbeatTimer = useRef(null);\n  const lastUploadProgress = useRef({\n    screen: 0,\n    camera: 0,\n    ts: 0,\n    lastPersistAt: 0,\n  });\n  const stallNotified = useRef(false);\n  const fatalErrorRef = useRef(false);\n  const idbReadyRef = useRef(false);\n  const recoveryAttempted = useRef(false);\n  const recoveryExportedRef = useRef(false);\n  const screenTrackLostRef = useRef(false);\n  const screenTrackMonitor = useRef(null);\n  const pausedStateRef = useRef(false);\n  const audioCaptureDegradedRef = useRef(false);\n  const storagePressureRef = useRef({\n    lastEstimateAt: 0,\n    quota: null,\n    usage: null,\n    headroom: null,\n    lowSpace: false,\n    critical: false,\n    warned: false,\n    stopSent: false,\n  });\n  const networkStateRef = useRef({\n    online: navigator.onLine,\n    offlineSince: null,\n  });\n  const sessionTrackState = useRef({\n    screen: {\n      chunkCount: 0,\n      bytesRecorded: 0,\n      lastChunkAt: null,\n      inMemoryChunkCount: 0,\n      droppedInMemoryChunks: 0,\n      durableChunkCount: 0,\n      lastPersistedChunkIndex: null,\n      lastPersistedAt: null,\n      lastUploadOffset: 0,\n      uploaderUpdatedAt: null,\n    },\n    camera: {\n      chunkCount: 0,\n      bytesRecorded: 0,\n      lastChunkAt: null,\n      inMemoryChunkCount: 0,\n      droppedInMemoryChunks: 0,\n      durableChunkCount: 0,\n      lastPersistedChunkIndex: null,\n      lastPersistedAt: null,\n      lastUploadOffset: 0,\n      uploaderUpdatedAt: null,\n    },\n    audio: {\n      chunkCount: 0,\n      bytesRecorded: 0,\n      lastChunkAt: null,\n      inMemoryChunkCount: 0,\n      droppedInMemoryChunks: 0,\n      durableChunkCount: 0,\n      lastPersistedChunkIndex: null,\n      lastPersistedAt: null,\n      lastUploadOffset: 0,\n      uploaderUpdatedAt: null,\n    },\n  });\n\n  // This checks if the recording was previously initialized\n  const isInit = useRef(false);\n\n  const aCtx = useRef(null);\n  const destination = useRef(null);\n\n  const keepAliveInterval = useRef(null);\n  const keepAliveAudioCtx = useRef(null);\n  const keepAliveOscillator = useRef(null);\n\n  const logDebugEvent = async () => {\n    // debug bundle feature removed; noop\n  };\n\n  const setCloudRestartPhase = async (phase, details = {}) => {\n    try {\n      await chrome.storage.local.set({\n        lastCloudRegionRestartPhase: {\n          phase,\n          ts: Date.now(),\n          isIframe: Boolean(IS_IFRAME_CONTEXT),\n          ...details,\n        },\n      });\n    } catch {}\n  };\n\n  const clearPendingStart = () => {\n    pendingStartRef.current = false;\n    pendingStartAttempts.current = 0;\n    if (pendingStartTimer.current) {\n      clearTimeout(pendingStartTimer.current);\n      pendingStartTimer.current = null;\n    }\n  };\n\n  const canBeginRecording = () => {\n    const hasStreams =\n      Boolean(screenStream.current) || Boolean(cameraStream.current);\n    return Boolean(uploadersInitialized.current) && hasStreams;\n  };\n\n  const maybeStartRecording = (reason = \"unknown\") => {\n    if (!pendingStartRef.current || isFinishing.current) return;\n    if (canBeginRecording()) {\n      clearPendingStart();\n      startRecording();\n      return;\n    }\n\n    const attempt = pendingStartAttempts.current + 1;\n    pendingStartAttempts.current = attempt;\n    if (attempt > 75) {\n      // ~15s max with 200ms delay\n      clearPendingStart();\n      sendRecordingError(\n        \"Recording is taking too long to start. Please try again.\",\n      );\n      return;\n    }\n\n    if (pendingStartTimer.current) {\n      clearTimeout(pendingStartTimer.current);\n    }\n    pendingStartTimer.current = setTimeout(() => {\n      pendingStartTimer.current = null;\n      maybeStartRecording(reason);\n    }, 200);\n  };\n\n  const logStartFlow = (event, data = {}) => {\n    if (!DEBUG_START_FLOW) return;\n    const payload = { ts: Date.now(), event, ...data };\n    console.info(\"[Screenity][StartFlow]\", payload);\n    try {\n      const update = {\n        startFlowDebug: {\n          ...(data || {}),\n          event,\n          ts: payload.ts,\n        },\n      };\n      if (event === \"recording_start\") {\n        update.recordingStartAt = payload.ts;\n      } else if (event === \"first_chunk\") {\n        update.firstChunkAt = payload.ts;\n      } else if (event === \"recording_stop\") {\n        update.recordingStopAt = payload.ts;\n      } else if (event === \"recording_error\") {\n        update.recordingErrorAt = payload.ts;\n      }\n      chrome.storage.local.set(update);\n    } catch {}\n  };\n\n  const assertCountdownBeforeFirstChunk = async (firstChunkAt) => {\n    if (!DEBUG_START_FLOW) return;\n    try {\n      const { countdownFinishedAt } = await chrome.storage.local.get([\n        \"countdownFinishedAt\",\n      ]);\n      if (\n        typeof countdownFinishedAt === \"number\" &&\n        firstChunkAt < countdownFinishedAt\n      ) {\n        console.error(\"[Screenity][StartFlow] Chunk before countdown end\", {\n          firstChunkAt,\n          countdownFinishedAt,\n        });\n      }\n    } catch (err) {\n      console.warn(\"[Screenity][StartFlow] Countdown assert failed\", err);\n    }\n  };\n\n  const ensureRecordingSessionId = () => {\n    if (!recordingSessionId.current) {\n      recordingSessionId.current = crypto.randomUUID();\n    }\n    return recordingSessionId.current;\n  };\n\n  const getBrowserVersion = () => {\n    const ua = navigator.userAgent || \"\";\n    const chromeMatch = ua.match(/Chrome\\/([0-9.]+)/);\n    return chromeMatch?.[1] || null;\n  };\n\n  const getBrowserName = () => {\n    const ua = navigator.userAgent || \"\";\n    if (ua.includes(\"Edg/\")) return \"Edge\";\n    if (ua.includes(\"Chrome/\")) return \"Chrome\";\n    if (ua.includes(\"Firefox/\")) return \"Firefox\";\n    if (ua.includes(\"Safari/\") && !ua.includes(\"Chrome/\")) return \"Safari\";\n    return \"Other\";\n  };\n\n  const getOsName = () => {\n    const p = navigator.platform || \"\";\n    if (p.includes(\"Mac\")) return \"macOS\";\n    if (p.includes(\"Win\")) return \"Windows\";\n    if (p.includes(\"Linux\")) return \"Linux\";\n    return \"Other\";\n  };\n\n  const ensureTelemetryRuntimeContext = async () => {\n    if (telemetryRuntimeRef.current.extensionVersion) {\n      return telemetryRuntimeRef.current;\n    }\n    const manifestVersion =\n      chrome?.runtime?.getManifest?.()?.version || null;\n    let platformInfo = null;\n    try {\n      platformInfo = await chrome.runtime.sendMessage({\n        type: \"get-platform-info\",\n      });\n    } catch {\n      platformInfo = null;\n    }\n    telemetryRuntimeRef.current = {\n      extensionVersion: manifestVersion,\n      browserVersion: getBrowserVersion(),\n      platform: platformInfo?.os || navigator.platform || null,\n      arch: platformInfo?.arch || null,\n      os: platformInfo?.os || null,\n    };\n    return telemetryRuntimeRef.current;\n  };\n\n  const appendUploadTelemetryEvent = async (eventPayload) => {\n    try {\n      const existing = await chrome.storage.local.get([UPLOAD_TELEMETRY_KEY]);\n      const current = Array.isArray(existing?.[UPLOAD_TELEMETRY_KEY])\n        ? existing[UPLOAD_TELEMETRY_KEY]\n        : [];\n      const next = [...current, eventPayload].slice(-MAX_UPLOAD_TELEMETRY_EVENTS);\n      await chrome.storage.local.set({\n        [UPLOAD_TELEMETRY_KEY]: next,\n        lastUploadTelemetryEvent: eventPayload,\n      });\n    } catch (err) {\n      console.warn(\"Failed to persist upload telemetry event:\", err);\n    }\n  };\n\n  const resolveUploadTelemetryToken = async () => {\n    if (uploadTelemetryTokenRef.current) {\n      return uploadTelemetryTokenRef.current;\n    }\n    try {\n      const { screenityToken } = await chrome.storage.local.get([\n        \"screenityToken\",\n      ]);\n      if (screenityToken) {\n        uploadTelemetryTokenRef.current = screenityToken;\n        return screenityToken;\n      }\n    } catch {\n      // ignore\n    }\n\n    try {\n      const res = await fetch(`${API_BASE}/auth/get-extension-token`, {\n        method: \"GET\",\n        credentials: \"include\",\n      });\n      if (res.ok) {\n        const data = await res.json().catch(() => null);\n        const token = data?.token || data?.extensionToken || null;\n        if (token) {\n          uploadTelemetryTokenRef.current = token;\n          return token;\n        }\n      }\n    } catch {\n      // ignore\n    }\n    return null;\n  };\n\n  const toUploadTelemetryRequest = async (eventPayload) => {\n    const mediaId =\n      eventPayload.mediaId ||\n      screenUploader.current?.getMeta?.()?.mediaId ||\n      cameraUploader.current?.getMeta?.()?.mediaId ||\n      null;\n    if (!mediaId) {\n      return null;\n    }\n\n    const browserVersion = eventPayload.browserVersion || null;\n    const browserMajor =\n      browserVersion && Number.isFinite(parseInt(browserVersion, 10))\n        ? parseInt(browserVersion, 10)\n        : null;\n\n    return {\n      recordingId: mediaId,\n      recordingSessionId: eventPayload.recordingSessionId || null,\n      projectId: eventPayload.projectId || null,\n      sceneId: eventPayload.sceneId || null,\n      mediaId: eventPayload.mediaId || null,\n      bunnyVideoId: eventPayload.bunnyVideoId || null,\n      source: \"extension\",\n      extVersion: eventPayload.extensionVersion || null,\n      env: {\n        os: getOsName(),\n        browser: getBrowserName(),\n        browserMajor,\n        appVersion: null,\n      },\n      event: {\n        type: eventPayload.event,\n        t: eventPayload.ts || Date.now(),\n        trackType: eventPayload.trackType || null,\n        offsetBytes:\n          typeof eventPayload.offset === \"number\"\n            ? eventPayload.offset\n            : null,\n        fileSize:\n          typeof eventPayload.totalBytes === \"number\"\n            ? eventPayload.totalBytes\n            : typeof eventPayload.finalizedBytes === \"number\"\n            ? eventPayload.finalizedBytes\n            : null,\n        online: typeof navigator !== \"undefined\" ? navigator.onLine : null,\n        visibilityState:\n          typeof document !== \"undefined\" ? document.visibilityState : null,\n        errCode: eventPayload.errorCode || null,\n        errMsg: eventPayload.message || eventPayload.error || null,\n        reason: eventPayload.reason || null,\n        mediaId: eventPayload.mediaId || null,\n        bunnyVideoId: eventPayload.bunnyVideoId || null,\n        projectId: eventPayload.projectId || null,\n        sceneId: eventPayload.sceneId || null,\n        recordingSessionId: eventPayload.recordingSessionId || null,\n      },\n    };\n  };\n\n  const sendUploadTelemetryNetwork = async (eventPayload) => {\n    if (uploadTelemetryNetworkDisabledRef.current) return;\n    try {\n      const requestBody = await toUploadTelemetryRequest(eventPayload);\n      if (!requestBody) {\n        return;\n      }\n      const body = JSON.stringify(requestBody);\n      const token = await resolveUploadTelemetryToken();\n      const headers = {\n        \"Content-Type\": \"application/json\",\n        \"x-screenity-source\": \"extension\",\n      };\n      if (eventPayload.extensionVersion) {\n        headers[\"x-screenity-ext-version\"] = String(\n          eventPayload.extensionVersion,\n        );\n      }\n      if (token) {\n        headers.Authorization = `Bearer ${token}`;\n      }\n      if (\n        (eventPayload.event === \"upload_pagehide\" ||\n          eventPayload.event === \"upload_abandoned_on_unload\") &&\n        navigator.sendBeacon\n      ) {\n        const sent = navigator.sendBeacon(\n          UPLOAD_TELEMETRY_ENDPOINT,\n          new Blob([body], { type: \"application/json\" }),\n        );\n        if (sent) return;\n      }\n      const res = await fetch(UPLOAD_TELEMETRY_ENDPOINT, {\n        method: \"POST\",\n        headers,\n        credentials: \"include\",\n        keepalive: true,\n        body,\n      });\n      if (res.status === 404 || res.status === 405) {\n        uploadTelemetryNetworkDisabledRef.current = true;\n      }\n    } catch {\n      // best effort\n    }\n  };\n\n  const emitUploadTelemetry = async (event, payload = {}) => {\n    const runtime = await ensureTelemetryRuntimeContext();\n    const screenSceneId = screenUploader.current?.getMeta?.()?.sceneId || null;\n    const cameraSceneId = cameraUploader.current?.getMeta?.()?.sceneId || null;\n    const eventPayload = {\n      event,\n      ts: Date.now(),\n      recordingSessionId:\n        payload.recordingSessionId ||\n        recorderSession.current?.id ||\n        recordingSessionId.current ||\n        null,\n      projectId:\n        payload.projectId || recorderSession.current?.projectId || null,\n      sceneId: payload.sceneId || screenSceneId || cameraSceneId || null,\n      mediaId: payload.mediaId || null,\n      bunnyVideoId: payload.bunnyVideoId || null,\n      trackType: payload.trackType || null,\n      uploaderType: payload.uploaderType || \"cloud_recorder\",\n      extensionVersion: runtime.extensionVersion,\n      browserVersion: runtime.browserVersion,\n      platform: runtime.platform,\n      arch: runtime.arch,\n      os: runtime.os,\n      ...payload,\n    };\n\n    await appendUploadTelemetryEvent(eventPayload);\n    if (event !== \"upload_progress\") {\n      void sendUploadTelemetryNetwork(eventPayload);\n    }\n  };\n\n  const resetUnloadGuard = () => {\n    unloadGuardRef.current = {\n      pagehideSeen: false,\n      beforeUnloadSeen: false,\n      abandonedSent: false,\n      stopTriggeredFromUnload: false,\n    };\n  };\n\n  const emitAbandonedOnUnloadOnce = (reason, extra = {}) => {\n    if (unloadGuardRef.current.abandonedSent) return;\n    unloadGuardRef.current.abandonedSent = true;\n    void emitUploadTelemetry(\"upload_abandoned_on_unload\", {\n      reason,\n      ...extra,\n    });\n  };\n\n  const resetSessionTrackState = () => {\n    sessionTrackState.current = {\n      screen: {\n        chunkCount: 0,\n        bytesRecorded: 0,\n        lastChunkAt: null,\n        inMemoryChunkCount: 0,\n        droppedInMemoryChunks: 0,\n        durableChunkCount: 0,\n        lastPersistedChunkIndex: null,\n        lastPersistedAt: null,\n        lastUploadOffset: 0,\n        uploaderUpdatedAt: null,\n      },\n      camera: {\n        chunkCount: 0,\n        bytesRecorded: 0,\n        lastChunkAt: null,\n        inMemoryChunkCount: 0,\n        droppedInMemoryChunks: 0,\n        durableChunkCount: 0,\n        lastPersistedChunkIndex: null,\n        lastPersistedAt: null,\n        lastUploadOffset: 0,\n        uploaderUpdatedAt: null,\n      },\n      audio: {\n        chunkCount: 0,\n        bytesRecorded: 0,\n        lastChunkAt: null,\n        inMemoryChunkCount: 0,\n        droppedInMemoryChunks: 0,\n        durableChunkCount: 0,\n        lastPersistedChunkIndex: null,\n        lastPersistedAt: null,\n        lastUploadOffset: 0,\n        uploaderUpdatedAt: null,\n      },\n    };\n  };\n\n  const trimChunkBuffer = (ref, maxChunks) => {\n    if (!ref?.current || ref.current.length <= maxChunks) return 0;\n    const removed = ref.current.length - maxChunks;\n    ref.current.splice(0, removed);\n    return removed;\n  };\n\n  const noteTrackChunk = (track, blob, extra = {}) => {\n    if (!blob || !track || !sessionTrackState.current[track]) return;\n    const current = sessionTrackState.current[track];\n    sessionTrackState.current[track] = {\n      ...current,\n      chunkCount: (current.chunkCount || 0) + 1,\n      bytesRecorded: (current.bytesRecorded || 0) + (blob.size || 0),\n      lastChunkAt: Date.now(),\n      ...extra,\n    };\n  };\n\n  const patchTrackState = (track, extra = {}) => {\n    if (!track || !sessionTrackState.current[track]) return;\n    sessionTrackState.current[track] = {\n      ...sessionTrackState.current[track],\n      ...extra,\n    };\n  };\n\n  const getUploaderResumeMeta = (uploaderRef) => {\n    if (!uploaderRef?.current) return null;\n    if (typeof uploaderRef.current.getResumeState === \"function\") {\n      return uploaderRef.current.getResumeState();\n    }\n    if (typeof uploaderRef.current.getMeta === \"function\") {\n      return uploaderRef.current.getMeta();\n    }\n    return null;\n  };\n\n  const buildTrackSnapshot = () => ({\n    screen: {\n      ...sessionTrackState.current.screen,\n      inMemoryChunkCount: screenChunks.current.length,\n      hasDurableLocalChunks: true,\n      uploader: getUploaderResumeMeta(screenUploader),\n    },\n    camera: {\n      ...sessionTrackState.current.camera,\n      inMemoryChunkCount: cameraChunks.current.length,\n      hasDurableLocalChunks: cameraChunkStoreReadyRef.current,\n      uploader: getUploaderResumeMeta(cameraUploader),\n    },\n    audio: {\n      ...sessionTrackState.current.audio,\n      inMemoryChunkCount: audioChunks.current.length,\n      hasDurableLocalChunks: audioChunkStoreReadyRef.current,\n      uploader: null,\n    },\n  });\n\n  const setPipelineState = async (step, extra = {}) => {\n    const state = {\n      step,\n      ts: Date.now(),\n      projectId: extra.projectId || null,\n      sceneId: extra.sceneId || null,\n      status: extra.status || null,\n      details: extra.details || null,\n    };\n    try {\n      await chrome.storage.local.set({ recorderPipelineState: state });\n    } catch {\n      // ignore\n    }\n  };\n\n  const getOrCreateSceneId = async ({ forceNew = false } = {}) => {\n    try {\n      const { sceneId, sceneIdStatus } = await chrome.storage.local.get([\n        \"sceneId\",\n        \"sceneIdStatus\",\n      ]);\n      if (!forceNew && sceneId && sceneIdStatus === \"recording\") {\n        return sceneId;\n      }\n    } catch {\n      // ignore\n    }\n    const next = crypto.randomUUID();\n    try {\n      await chrome.storage.local.set({\n        sceneId: next,\n        sceneIdStatus: \"recording\",\n      });\n    } catch {\n      // ignore\n    }\n    return next;\n  };\n\n  const markSceneComplete = async (sceneId) => {\n    if (!sceneId) return;\n    try {\n      await chrome.storage.local.set({\n        sceneIdStatus: \"completed\",\n        lastCompletedSceneId: sceneId,\n      });\n    } catch {\n      // ignore\n    }\n  };\n\n  const upsertPendingScene = async (sceneId, payload) => {\n    if (!sceneId) return;\n    const indexKey = \"pendingSceneIndex\";\n    const sceneKey = `pendingScene:${sceneId}`;\n    const { pendingSceneIndex = [] } = await chrome.storage.local.get([\n      indexKey,\n    ]);\n    const nextIndex = pendingSceneIndex.includes(sceneId)\n      ? pendingSceneIndex\n      : [...pendingSceneIndex, sceneId];\n    await chrome.storage.local.set({\n      [sceneKey]: { ...payload, sceneId, updatedAt: Date.now() },\n      [indexKey]: nextIndex,\n    });\n  };\n\n  const removePendingScene = async (sceneId) => {\n    if (!sceneId) return;\n    const indexKey = \"pendingSceneIndex\";\n    const sceneKey = `pendingScene:${sceneId}`;\n    const { pendingSceneIndex = [] } = await chrome.storage.local.get([\n      indexKey,\n    ]);\n    const nextIndex = pendingSceneIndex.filter((id) => id !== sceneId);\n    await chrome.storage.local.remove([sceneKey]);\n    await chrome.storage.local.set({ [indexKey]: nextIndex });\n  };\n\n  const getSceneCreateStatus = async (sceneId) => {\n    if (!sceneId) return null;\n    const key = `sceneCreateStatus:${sceneId}`;\n    const result = await chrome.storage.local.get([key]);\n    return result?.[key] || null;\n  };\n\n  const setSceneCreateStatus = async (sceneId, status, extra = {}) => {\n    if (!sceneId) return;\n    const key = `sceneCreateStatus:${sceneId}`;\n    await chrome.storage.local.set({\n      [key]: {\n        status,\n        updatedAt: Date.now(),\n        ...extra,\n      },\n    });\n  };\n\n  const linkMediaToScene = async (projectId, sceneId, mediaId) => {\n    if (!projectId || !sceneId || !mediaId) return;\n    await fetch(`${API_BASE}/media/${mediaId}/scene`, {\n      method: \"PATCH\",\n      headers: { \"Content-Type\": \"application/json\" },\n      credentials: \"include\",\n      body: JSON.stringify({ projectId, sceneId }),\n    });\n  };\n\n  const confirmLinkedMedia = async (projectId, sceneId, mediaIds = []) => {\n    const filtered = mediaIds.filter(Boolean);\n    if (!projectId || !sceneId || filtered.length === 0) return;\n    await fetch(`${API_BASE}/media/confirm-linked`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      credentials: \"include\",\n      body: JSON.stringify({ mediaIds: filtered, projectId, sceneId }),\n    });\n  };\n\n  const ensureMediaLinked = async ({ projectId, sceneId, mediaIds }) => {\n    const filtered = (mediaIds || []).filter(Boolean);\n    if (!projectId || !sceneId || filtered.length === 0) return;\n    await Promise.allSettled(\n      filtered.map((mediaId) => linkMediaToScene(projectId, sceneId, mediaId)),\n    );\n    await confirmLinkedMedia(projectId, sceneId, filtered);\n  };\n\n  const recoverScene = async ({\n    projectId,\n    sceneId,\n    screenMediaId,\n    cameraMediaId,\n    audioMediaId,\n  }) => {\n    if (!projectId || !sceneId) return { ok: false, error: \"missing-ids\" };\n    const mediaIds = [screenMediaId, cameraMediaId, audioMediaId].filter(\n      Boolean,\n    );\n    const res = await fetch(`${API_BASE}/videos/${projectId}/recover-scene`, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application/json\" },\n      credentials: \"include\",\n      body: JSON.stringify({\n        sceneId,\n        mediaIds,\n        screenMediaId: screenMediaId || null,\n        cameraMediaId: cameraMediaId || null,\n        audioMediaId: audioMediaId || null,\n      }),\n    });\n    if (!res.ok) {\n      const errorText = await res.text();\n      return { ok: false, error: errorText || \"recover-scene-failed\" };\n    }\n    return { ok: true };\n  };\n\n  // exportDebugBundle removed\n\n  // Note: We no longer try to \"keep alive\" the service worker with pings.\n  // Instead, we persist recording state to chrome.storage.local so the SW can\n  // recover context when it restarts. The CloudRecorder tab is the source of truth.\n  // However, we DO need to keep the tab itself alive using silent audio to prevent\n  // Chrome from freezing/discarding this background tab.\n\n  /**\n   * Start silent audio playback to prevent Chrome from freezing this tab.\n   * Chrome throttles/freezes background tabs after ~5 minutes of inactivity,\n   * which would stop our recording. Playing silent audio keeps the tab active.\n   */\n  const startTabKeepAlive = () => {\n    try {\n      if (keepAliveAudioCtx.current) return; // Already running\n\n      const ctx = new (window.AudioContext || window.webkitAudioContext)();\n      keepAliveAudioCtx.current = ctx;\n\n      // Create a silent oscillator (inaudible frequency at zero gain)\n      const oscillator = ctx.createOscillator();\n      const gainNode = ctx.createGain();\n\n      oscillator.frequency.value = 0; // DC signal (no audible tone)\n      gainNode.gain.value = 0; // Completely silent\n\n      oscillator.connect(gainNode);\n      gainNode.connect(ctx.destination);\n      oscillator.start();\n\n      keepAliveOscillator.current = oscillator;\n\n      // Also request that Chrome not discard this tab\n      chrome.runtime.sendMessage({\n        type: \"set-tab-auto-discardable\",\n        discardable: false,\n      });\n\n      if (globalThis.SCREENITY_VERBOSE_LOGS) {\n        if (DEBUG_START_FLOW) {\n          console.log(\"[CloudRecorder] Tab keep-alive started\");\n        }\n      }\n    } catch (err) {\n      console.warn(\"[CloudRecorder] Failed to start tab keep-alive:\", err);\n    }\n  };\n\n  /**\n   * Stop the silent audio playback when recording ends.\n   */\n  const stopTabKeepAlive = () => {\n    try {\n      if (keepAliveOscillator.current) {\n        keepAliveOscillator.current.stop();\n        keepAliveOscillator.current.disconnect();\n        keepAliveOscillator.current = null;\n      }\n      if (keepAliveAudioCtx.current) {\n        keepAliveAudioCtx.current.close();\n        keepAliveAudioCtx.current = null;\n      }\n\n      // Allow Chrome to discard this tab again if needed\n      chrome.runtime.sendMessage({\n        type: \"set-tab-auto-discardable\",\n        discardable: true,\n      });\n\n      if (globalThis.SCREENITY_VERBOSE_LOGS) {\n        if (DEBUG_START_FLOW) {\n          console.log(\"[CloudRecorder] Tab keep-alive stopped\");\n        }\n      }\n    } catch (err) {\n      console.warn(\"[CloudRecorder] Failed to stop tab keep-alive:\", err);\n    }\n  };\n\n  const logScreenTrackEvent = async (event, track) => {\n    try {\n      const settings = track?.getSettings?.() || {};\n      const now = Date.now();\n      const data = {\n        event,\n        ts: now,\n        label: track?.label || null,\n        readyState: track?.readyState || null,\n        muted: track?.muted || false,\n        settings,\n      };\n      const { screenTrackLog = [] } = await chrome.storage.local.get([\n        \"screenTrackLog\",\n      ]);\n      const next = [...screenTrackLog, data].slice(-50);\n      chrome.storage.local.set({ screenTrackLog: next });\n    } catch (err) {\n      console.warn(\"Failed to log screen track event\", err);\n    }\n  };\n\n  const bindScreenTrack = (track) => {\n    if (!track) return;\n    screenTrackLostRef.current = false;\n    logScreenTrackEvent(\"track-start\", track);\n\n    track.onended = () => {\n      screenTrackLostRef.current = true;\n      logScreenTrackEvent(\"track-ended\", track);\n      const label = track?.label || null;\n\n      // Collect diagnostic info to understand WHY the track ended\n      const diagnosticInfo = {\n        reason: \"screen-track-ended\",\n        tabId: recordingTabId.current || null,\n        label,\n        readyState: track?.readyState || null,\n        ts: Date.now(),\n        isTab: isTab.current,\n        recordingType: recordingType.current,\n        isIframe: IS_IFRAME_CONTEXT,\n        documentHidden: document.hidden,\n        documentVisibility: document.visibilityState,\n        recorderState: screenRecorder.current?.state || null,\n        hasChunks: hasChunks.current,\n        chunkCount: index.current,\n        lastChunkTime: lastTimecode.current,\n        timeSinceLastChunk: lastTimecode.current\n          ? Date.now() - lastTimecode.current\n          : null,\n      };\n\n      console.error(\"🔴 Screen track ended unexpectedly!\", diagnosticInfo);\n\n      chrome.storage.local.set({\n        screenTrackLost: true,\n        lastTrackEndedEvent: diagnosticInfo,\n        lastTrackEnded: diagnosticInfo,\n      });\n\n      // When screen track ends, we MUST stop the recording\n      // Otherwise we'd be recording black/frozen video with no way for user to know\n      if (!isFinishing.current && !sentLast.current) {\n        console.warn(\"⚠️ Screen track ended - stopping recording\");\n        stopRecording(true, \"screen-track-ended\");\n      }\n    };\n\n    track.onmute = () => {\n      screenTrackLostRef.current = true;\n      logScreenTrackEvent(\"track-muted\", track);\n    };\n    track.onunmute = () => {\n      screenTrackLostRef.current = false;\n      logScreenTrackEvent(\"track-unmuted\", track);\n    };\n\n    startScreenTrackMonitor();\n  };\n\n  const startScreenTrackMonitor = () => {\n    if (screenTrackMonitor.current) clearInterval(screenTrackMonitor.current);\n    screenTrackMonitor.current = setInterval(() => {\n      const track = screenStream.current?.getVideoTracks?.()[0] || null;\n      if (!track) return;\n      if (track.readyState === \"ended\" && !screenTrackLostRef.current) {\n        screenTrackLostRef.current = true;\n        logScreenTrackEvent(\"monitor-ended\", track);\n\n        // Stop the recording when track dies\n        if (!isFinishing.current && !sentLast.current) {\n          console.warn(\n            \"⚠️ Screen track monitor detected ended track - stopping recording\",\n          );\n          stopRecording(true, \"screen-track-monitor-ended\");\n        }\n      }\n    }, 5000);\n  };\n\n  const ensureChunkStoreReady = async () => {\n    if (idbReadyRef.current) return true;\n    try {\n      await chunksStore.setItem(\"__probe\", { ts: Date.now() });\n      await chunksStore.removeItem(\"__probe\");\n      idbReadyRef.current = true;\n      void emitUploadTelemetry(\"upload_local_backup_enabled\", {\n        backupType: \"screen\",\n      });\n      return true;\n    } catch (err) {\n      fatalErrorRef.current = true;\n      void emitUploadTelemetry(\"upload_local_backup_unavailable\", {\n        backupType: \"screen\",\n        error: err?.message || String(err),\n      });\n      sendRecordingError(\n        \"Local storage is blocked (IndexedDB unavailable). Recording cannot start.\",\n      );\n      return false;\n    }\n  };\n\n  const ensureAudioChunkStoreReady = async () => {\n    if (audioChunkStoreReadyRef.current) return true;\n    try {\n      await audioChunksStore.setItem(\"__probe\", { ts: Date.now() });\n      await audioChunksStore.removeItem(\"__probe\");\n      audioChunkStoreReadyRef.current = true;\n      void emitUploadTelemetry(\"upload_local_backup_enabled\", {\n        backupType: \"audio\",\n      });\n      return true;\n    } catch (err) {\n      audioChunkStoreReadyRef.current = false;\n      void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n        backupType: \"audio\",\n        error: err?.message || String(err),\n      });\n      return false;\n    }\n  };\n\n  const clearAudioChunkStore = async (reason = \"unknown\") => {\n    audioChunkIndexRef.current = 0;\n    // Always attempt clear regardless of audioChunkStoreReadyRef state.\n    // If IDB degraded mid-session the flag is false, but partial chunks written\n    // before degradation must still be cleared to avoid stale data on next session.\n    try {\n      await audioChunksStore.clear();\n      console.info(\"[CloudRecorder] clearAudioChunkStore:\", reason);\n    } catch (err) {\n      console.warn(\"Failed to clear audio chunk store:\", err);\n    }\n  };\n\n  const ensureCameraChunkStoreReady = async () => {\n    if (cameraChunkStoreReadyRef.current) return true;\n    try {\n      await cameraChunksStore.setItem(\"__probe\", { ts: Date.now() });\n      await cameraChunksStore.removeItem(\"__probe\");\n      cameraChunkStoreReadyRef.current = true;\n      void emitUploadTelemetry(\"upload_local_backup_enabled\", {\n        backupType: \"camera\",\n      });\n      return true;\n    } catch (err) {\n      cameraChunkStoreReadyRef.current = false;\n      void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n        backupType: \"camera\",\n        error: err?.message || String(err),\n      });\n      return false;\n    }\n  };\n\n  const clearCameraChunkStore = async (reason = \"unknown\") => {\n    cameraChunkIndexRef.current = 0;\n    // Always attempt clear regardless of cameraChunkStoreReadyRef state.\n    // If IDB degraded mid-session the flag is false, but partial chunks written\n    // before degradation must still be cleared to avoid stale data on next session.\n    try {\n      await cameraChunksStore.clear();\n      console.info(\"[CloudRecorder] clearCameraChunkStore:\", reason);\n    } catch (err) {\n      console.warn(\"Failed to clear camera chunk store:\", err);\n    }\n  };\n\n  const clearLocalScreenPlaybackOffer = async (reason = \"unknown\") => {\n    const existingOffer = localScreenPlaybackOfferRef.current;\n    localScreenPlaybackOfferRef.current = null;\n    try {\n      const result = await chrome.runtime.sendMessage({\n        type: \"cloud-local-playback-clear\",\n        offerId: existingOffer?.offerId || null,\n        reason,\n      });\n      if (result?.ok) {\n        console.info(\n          \"[CloudRecorder] Cleared local screen playback offer\",\n          {\n            reason,\n            offerId: existingOffer?.offerId || null,\n          },\n        );\n      }\n    } catch (err) {\n      console.warn(\n        \"[CloudRecorder] Failed to clear local screen playback offer\",\n        {\n          reason,\n          error: err?.message || err,\n        },\n      );\n    }\n  };\n\n  const registerLocalScreenPlaybackOffer = async ({\n    projectId,\n    sceneId,\n    uploadMeta,\n  }) => {\n    if (!projectId || !sceneId) {\n      return null;\n    }\n\n    let chunkCount = 0;\n    try {\n      chunkCount = await chunksStore.length();\n    } catch {\n      chunkCount = 0;\n    }\n    if (!chunkCount) {\n      console.info(\n        \"[CloudRecorder] Local-first screen offer unavailable: no local chunks\",\n        {\n          projectId,\n          sceneId,\n        },\n      );\n      return null;\n    }\n\n    const estimatedBytes =\n      screenUploader.current?.offset ||\n      sessionTrackState.current?.screen?.bytesRecorded ||\n      0;\n    if (!estimatedBytes || estimatedBytes <= 0) {\n      console.info(\n        \"[CloudRecorder] Local-first screen offer unavailable: no byte estimate\",\n        {\n          projectId,\n          sceneId,\n          chunkCount,\n        },\n      );\n      return null;\n    }\n\n    if (estimatedBytes > LOCAL_SCREEN_PLAYBACK_MAX_BYTES) {\n      console.info(\n        \"[CloudRecorder] Local-first screen offer skipped: source too large\",\n        {\n          projectId,\n          sceneId,\n          chunkCount,\n          estimatedBytes,\n          maxBytes: LOCAL_SCREEN_PLAYBACK_MAX_BYTES,\n        },\n      );\n      return null;\n    }\n\n    const now = Date.now();\n    const nextOffer = {\n      offerId: crypto.randomUUID(),\n      projectId,\n      sceneId,\n      recordingSessionId:\n        recorderSession.current?.id || recordingSessionId.current || null,\n      trackType: \"screen\",\n      chunkCount,\n      estimatedBytes,\n      mediaId: uploadMeta?.screen?.mediaId || null,\n      bunnyVideoId: uploadMeta?.screen?.videoId || null,\n      createdAt: now,\n      expiresAt: now + LOCAL_SCREEN_PLAYBACK_TTL_MS,\n      status: \"available\",\n      source: \"indexeddb-screen-chunks\",\n    };\n\n    try {\n      const result = await chrome.runtime.sendMessage({\n        type: \"cloud-local-playback-register\",\n        offer: nextOffer,\n      });\n      if (result?.ok && result.offer) {\n        localScreenPlaybackOfferRef.current = result.offer;\n        console.info(\n          \"[CloudRecorder] Local-first screen offer registered\",\n          {\n            projectId,\n            sceneId,\n            offerId: result.offer.offerId,\n            chunkCount: result.offer.chunkCount,\n            estimatedBytes: result.offer.estimatedBytes,\n            expiresAt: result.offer.expiresAt,\n          },\n        );\n        return result.offer;\n      }\n    } catch (err) {\n      console.warn(\n        \"[CloudRecorder] Failed to register local-first screen offer\",\n        {\n          projectId,\n          sceneId,\n          error: err?.message || err,\n        },\n      );\n    }\n\n    return null;\n  };\n\n  const buildAudioBlobFromDurableStore = async () => {\n    // Always attempt IDB read regardless of audioChunkStoreReadyRef state.\n    // The flag can be false if IDB degraded mid-session, but partial IDB data\n    // written before the failure is still more complete than the 8-chunk memory\n    // window. Falling back to memory prematurely silently drops early audio.\n    try {\n      const recovered = [];\n      await audioChunksStore.iterate((value) => {\n        if (value?.chunk) {\n          recovered.push(value);\n        }\n      });\n      recovered.sort((a, b) => (a.index || 0) - (b.index || 0));\n      if (recovered.length > 0) {\n        console.info(\"[CloudRecorder] buildAudioBlobFromDurableStore: using IDB\", {\n          chunks: recovered.length,\n          storeWasReady: audioChunkStoreReadyRef.current,\n        });\n        return createBlobFromChunks(\n          recovered.map((entry) => entry.chunk),\n          \"audio/webm\",\n        );\n      }\n      console.info(\"[CloudRecorder] buildAudioBlobFromDurableStore: IDB empty, using memory\", {\n        memoryChunks: audioChunks.current.length,\n        storeWasReady: audioChunkStoreReadyRef.current,\n      });\n    } catch (err) {\n      console.warn(\"[CloudRecorder] buildAudioBlobFromDurableStore: IDB read failed, falling back to memory\", {\n        error: err?.message || String(err),\n        memoryChunks: audioChunks.current.length,\n        storeWasReady: audioChunkStoreReadyRef.current,\n      });\n      void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n        backupType: \"audio\",\n        reason: \"audio-idb-read-failed-at-finalize\",\n        error: err?.message || String(err),\n      });\n    }\n    return createBlobFromChunks(audioChunks.current, \"audio/webm\");\n  };\n\n  const getSessionStateKey = (sessionId) =>\n    sessionId ? `cloudRecorderSession:${sessionId}` : null;\n\n  const persistSessionState = async (overrides = {}) => {\n    if (!recorderSession.current) return;\n    const tracks = buildTrackSnapshot();\n    const sessionStateKey = getSessionStateKey(recorderSession.current.id);\n    const nextState = {\n      ...recorderSession.current,\n      recordingSessionId: recorderSession.current.id,\n      lastChunkIndex: index.current - 1,\n      lastChunkTime: lastTimecode.current || null,\n      lastUploadOffset: {\n        screen: screenUploader.current?.offset || 0,\n        camera: cameraUploader.current?.offset || 0,\n      },\n      tracks,\n      network: {\n        online: networkStateRef.current.online,\n        offlineSince: networkStateRef.current.offlineSince,\n      },\n      storagePressure: {\n        quota: storagePressureRef.current.quota,\n        usage: storagePressureRef.current.usage,\n        headroom: storagePressureRef.current.headroom,\n        lowSpace: storagePressureRef.current.lowSpace,\n        critical: storagePressureRef.current.critical,\n      },\n      updatedAt: Date.now(),\n      ...overrides,\n    };\n    recorderSession.current = nextState;\n    try {\n      const payload = {\n        recorderSession: nextState,\n        [sessionStateKey]: nextState,\n      };\n      if (!sessionStateIndexedRef.current) {\n        const existing = await chrome.storage.local.get([SESSION_STATE_INDEX_KEY]);\n        const index = Array.isArray(existing?.[SESSION_STATE_INDEX_KEY])\n          ? existing[SESSION_STATE_INDEX_KEY]\n          : [];\n        payload[SESSION_STATE_INDEX_KEY] = index.includes(recorderSession.current.id)\n          ? index\n          : [...index, recorderSession.current.id].slice(-30);\n        sessionStateIndexedRef.current = true;\n      }\n      await chrome.storage.local.set(payload);\n    } catch (err) {\n      console.warn(\"Failed to persist recorder session state:\", err);\n    }\n  };\n\n  const startRecorderSession = async (meta = {}) => {\n    let recorderOwnerTabId = null;\n    try {\n      const tabRes = await chrome.runtime.sendMessage({ type: \"get-tab-id\" });\n      recorderOwnerTabId = tabRes?.tabId || null;\n    } catch {}\n\n    const sessionId = meta.sessionId || ensureRecordingSessionId();\n    const session = {\n      id: sessionId,\n      recordingSessionId: sessionId,\n      startedAt: Date.now(),\n      recorderTabId: recorderOwnerTabId,\n      capturedTabId: recordingTabId.current || null,\n      tabId: recordingTabId.current || null,\n      tabCaptureId: tabID.current || null,\n      isTab: isTab.current,\n      region: Boolean(regionRef.current),\n      status: \"recording\",\n      tracks: buildTrackSnapshot(),\n      network: {\n        online: networkStateRef.current.online,\n        offlineSince: networkStateRef.current.offlineSince,\n      },\n      ...meta,\n    };\n    recorderSession.current = session;\n    const sessionStateKey = getSessionStateKey(sessionId);\n    const existing = await chrome.storage.local.get([SESSION_STATE_INDEX_KEY]);\n    const index = Array.isArray(existing?.[SESSION_STATE_INDEX_KEY])\n      ? existing[SESSION_STATE_INDEX_KEY]\n      : [];\n    const nextIndex = index.includes(sessionId)\n      ? index\n      : [...index, sessionId].slice(-30);\n    await chrome.storage.local.set({\n      recorderSession: session,\n      [sessionStateKey]: session,\n      [SESSION_STATE_INDEX_KEY]: nextIndex,\n    });\n    sessionStateIndexedRef.current = true;\n    logDebugEvent(\"recorder-session-started\", {\n      sessionId: session.id,\n      projectId: meta.projectId || null,\n    });\n    try {\n      const result = await chrome.runtime.sendMessage({\n        type: \"register-recording-session\",\n        session,\n      });\n      if (result?.ok === false) {\n        fatalErrorRef.current = true;\n        recorderSession.current = null;\n        recordingSessionId.current = null;\n        sessionStateIndexedRef.current = false;\n        const sessionStateKey = getSessionStateKey(sessionId);\n        chrome.storage.local.remove([\"recorderSession\", sessionStateKey]);\n        sendRecordingError(\n          result?.error ||\n            \"Another Screenity recorder is already running. Please close it and try again.\",\n        );\n        return false;\n      }\n    } catch (err) {\n      console.warn(\n        \"Failed to register recording session with background:\",\n        err,\n      );\n    }\n    return true;\n  };\n\n  const finalizeRecorderSession = async (status = \"completed\") => {\n    if (!recorderSession.current) return;\n    const sessionId = recorderSession.current.id;\n    const sessionStateKey = getSessionStateKey(sessionId);\n    logDebugEvent(\"recorder-session-finalize-start\", {\n      status,\n      sessionId,\n    });\n    try {\n      const finalizedState = {\n        ...recorderSession.current,\n        status,\n        finishedAt: Date.now(),\n      };\n      await chrome.storage.local.set({\n        recorderSession: finalizedState,\n        [sessionStateKey]: finalizedState,\n      });\n      await chrome.runtime.sendMessage({\n        type: \"clear-recording-session\",\n        reason: `cloud-finalize-${status}`,\n      });\n      logDebugEvent(\"recorder-session-finalize-complete\", { status });\n    } catch (err) {\n      console.warn(\"Failed to finalize recorder session cleanly:\", err);\n      chrome.runtime\n        .sendMessage({\n          type: \"clear-recording-session-safe\",\n          reason: `cloud-finalize-fallback-${status}`,\n        })\n        .catch(() => {});\n      throw err;\n    } finally {\n      recorderSession.current = null;\n      recordingSessionId.current = null;\n      sessionStateIndexedRef.current = false;\n      resetSessionTrackState();\n      logDebugEvent(\"recorder-session-finalized\", { status });\n    }\n  };\n\n  const startSessionHeartbeat = () => {\n    if (sessionHeartbeat.current) clearInterval(sessionHeartbeat.current);\n    sessionHeartbeat.current = setInterval(() => {\n      if (!recorderSession.current) return;\n      persistSessionState({\n        heartbeatAt: Date.now(),\n      });\n    }, 5000);\n  };\n\n  const startChunkWatchdog = () => {\n    if (chunkStallTimer.current) clearInterval(chunkStallTimer.current);\n    chunkStallTimer.current = setInterval(() => {\n      if (!hasChunks.current || fatalErrorRef.current) return;\n      const last = lastTimecode.current || 0;\n      if (!last) return;\n      if (Date.now() - last > 15000) {\n        if (!stallNotified.current) {\n          stallNotified.current = true;\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"stream-warning\",\n            why: \"No video data received for 15 seconds. Recording may be stalled.\",\n          });\n        }\n      }\n    }, 5000);\n  };\n\n  const startUploadHeartbeat = () => {\n    if (uploadHeartbeatTimer.current)\n      clearInterval(uploadHeartbeatTimer.current);\n    uploadHeartbeatTimer.current = setInterval(() => {\n      const now = Date.now();\n      const screenOffset = screenUploader.current?.offset || 0;\n      const cameraOffset = cameraUploader.current?.offset || 0;\n      const { screen, camera, ts } = lastUploadProgress.current;\n\n      if (screenOffset !== screen || cameraOffset !== camera) {\n        lastUploadProgress.current = {\n          screen: screenOffset,\n          camera: cameraOffset,\n          ts: now,\n          lastPersistAt: lastUploadProgress.current.lastPersistAt || 0,\n        };\n        stallNotified.current = false;\n        recoveryExportedRef.current = false;\n        persistSessionState();\n        return;\n      }\n\n      if (\n        hasChunks.current &&\n        ts &&\n        now - ts > 30000 &&\n        !stallNotified.current\n      ) {\n        stallNotified.current = true;\n        chrome.runtime.sendMessage({\n          type: \"upload-stalled\",\n          offset: { screen: screenOffset, camera: cameraOffset },\n        });\n      } else if (\n        hasChunks.current &&\n        stallNotified.current &&\n        ts &&\n        now - ts > 45000\n      ) {\n        if (!recoveryExportedRef.current) {\n          recoveryExportedRef.current = true;\n          exportLocalRecovery(\"upload-stalled\");\n        }\n      }\n    }, 5000);\n  };\n\n  const stopAllIntervals = () => {\n    if (sessionHeartbeat.current) clearInterval(sessionHeartbeat.current);\n    if (chunkStallTimer.current) clearInterval(chunkStallTimer.current);\n    if (uploadHeartbeatTimer.current)\n      clearInterval(uploadHeartbeatTimer.current);\n    if (screenTrackMonitor.current) clearInterval(screenTrackMonitor.current);\n    sessionHeartbeat.current = null;\n    chunkStallTimer.current = null;\n    uploadHeartbeatTimer.current = null;\n    screenTrackMonitor.current = null;\n  };\n\n  const shouldSimulateFinalizeFailure = () => {\n    try {\n      const params = new URLSearchParams(window.location.search);\n      if (params.get(\"simulateFinalizeFailure\") === \"1\") return true;\n      return window.SCREENITY_SIMULATE_FINALIZE_FAILURE === true;\n    } catch {\n      return false;\n    }\n  };\n\n  const buildFinalizeDiagnostics = ({\n    stage,\n    settledResults = [],\n    rejectedResults = [],\n    incompleteUploaders = [],\n    reason = \"finalize-failed\",\n  }) => {\n    const screenMeta = screenUploader.current?.getMeta?.() || null;\n    const cameraMeta = cameraUploader.current?.getMeta?.() || null;\n\n    return {\n      reason,\n      stage,\n      ts: Date.now(),\n      settledResults: settledResults.map((result, i) => ({\n        index: i,\n        status: result?.status || \"unknown\",\n        reason:\n          result?.status === \"rejected\"\n            ? String(result.reason?.message || result.reason || \"unknown\")\n            : null,\n      })),\n      rejectedResults: rejectedResults.map((result, i) => ({\n        index: i,\n        reason: String(result.reason?.message || result.reason || \"unknown\"),\n      })),\n      incompleteUploaders,\n      uploaderMeta: {\n        screen: screenMeta,\n        camera: cameraMeta,\n      },\n      recorderSession: recorderSession.current,\n      finalizeContext: finalizeContextRef.current,\n    };\n  };\n\n  const exportFinalizeDiagnostics = async (diagnostics) => {\n    try {\n      const payload = diagnostics ||\n        finalizeFailureRef.current || { reason: \"no-diagnostics\" };\n      const blob = new Blob([JSON.stringify(payload, null, 2)], {\n        type: \"application/json\",\n      });\n      const objectUrl = URL.createObjectURL(blob);\n      try {\n        await chrome.downloads.download({\n          url: objectUrl,\n          filename: `Screenity-Finalize-Diagnostics-${new Date().toISOString()}.json`,\n          saveAs: false,\n        });\n      } finally {\n        URL.revokeObjectURL(objectUrl);\n      }\n    } catch (err) {\n      console.warn(\"[CloudRecorder][Finalize] Failed to export diagnostics\", {\n        error: err?.message || err,\n      });\n    }\n  };\n\n  const markFinalizeFailure = async (diagnostics) => {\n    finalizeFailureRef.current = diagnostics;\n    setFinalizeFailure(diagnostics);\n    isFinishing.current = false;\n\n    console.error(\"[CloudRecorder][Finalize] Finalization failed\", diagnostics);\n\n    await chrome.storage.local.set({\n      lastFinalizeFailure: diagnostics,\n    });\n    await persistSessionState({\n      status: \"finalize-failed\",\n      finalizeFailureAt: Date.now(),\n      finalizeFailureReason: diagnostics?.reason || \"finalize-failed\",\n    });\n    await setPipelineState(\"finalize-failed\", {\n      status: diagnostics?.reason || \"finalize-failed\",\n    });\n    try {\n      await chrome.storage.local.set({ sceneIdStatus: \"failed\" });\n    } catch {\n      // ignore\n    }\n  };\n  const exportLocalRecovery = async (reason = \"upload failed\") => {\n    try {\n      let blob = null;\n      const recovered = [];\n      await chunksStore.iterate((value) => {\n        recovered.push(value);\n      });\n      recovered.sort((a, b) => a.index - b.index);\n      if (recovered.length > 0) {\n        blob = createBlobFromChunks(\n          recovered.map((c) => c.chunk),\n          \"video/webm\",\n        );\n      }\n      if (!blob) {\n        blob =\n          createBlobFromChunks(screenChunks.current, \"video/webm\") ||\n          createBlobFromChunks(cameraChunks.current, \"video/webm\");\n      }\n      if (!blob) return;\n      const objectUrl = URL.createObjectURL(blob);\n      try {\n        await chrome.downloads.download({\n          url: objectUrl,\n          filename: `Screenity-Recovery-${new Date().toISOString()}-${reason}.webm`,\n          saveAs: false,\n        });\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: \"Upload stalled. A recovery file was saved locally.\",\n        });\n      } finally {\n        URL.revokeObjectURL(objectUrl);\n      }\n    } catch (err) {\n      console.warn(\"Failed to export local recovery:\", err);\n    }\n  };\n\n  // Called inside tryRecoverPreviousSession after IDB chunks are cleared.\n  // Removes stale TUS upload journals, lookup keys, and Bunny video-map entries\n  // that survive a process crash. Without this cleanup the next recording attempt\n  // can pick up the old journal, \"resume\" from the wrong server offset, and\n  // append fresh MediaRecorder chunks to the old partial Bunny upload — producing\n  // a garbled video. Also resets sceneId so the next session gets a fresh scene.\n  const clearStaleUploadJournals = async (storedSession) => {\n    const keysToRemove = [];\n    const tracks = storedSession?.tracks || {};\n\n    for (const trackData of Object.values(tracks)) {\n      const upl = trackData?.uploader;\n      if (!upl) continue;\n\n      if (upl.journalKey) keysToRemove.push(upl.journalKey);\n      if (upl.journalLookupKey) keysToRemove.push(upl.journalLookupKey);\n\n      // Also clear the Bunny video-map fallback entry\n      const pid = upl.projectId || storedSession?.projectId || null;\n      const sid = upl.sceneId || null;\n      const t = upl.type || upl.trackType || null;\n      if (pid && t) {\n        keysToRemove.push(\n          `bunnyVideoMap-${pid}-${sid || \"none\"}-${t || \"none\"}`,\n        );\n      }\n    }\n\n    // Force a fresh sceneId on the next recording so getOrCreateSceneId doesn't\n    // reuse the stale scene that is tied to the now-cleared journals.\n    keysToRemove.push(\"sceneId\", \"sceneIdStatus\");\n\n    const uniqKeys = [...new Set(keysToRemove)];\n\n    const screenUpl = tracks.screen?.uploader || null;\n    const cameraUpl = tracks.camera?.uploader || null;\n\n    console.info(\n      \"[CloudRecorder] clearStaleUploadJournals: removing stale journal + sceneId keys\",\n      {\n        sessionId: storedSession?.id || null,\n        keyCount: uniqKeys.length,\n        screen: {\n          journalKey: screenUpl?.journalKey || null,\n          mediaId: screenUpl?.mediaId || null,\n          journalOffset: screenUpl?.offset || 0,\n          journalTotalBytes: screenUpl?.totalBytes || 0,\n          journalStatus: screenUpl?.status || null,\n        },\n        camera: {\n          journalKey: cameraUpl?.journalKey || null,\n          mediaId: cameraUpl?.mediaId || null,\n          journalOffset: cameraUpl?.offset || 0,\n        },\n      },\n    );\n\n    void emitUploadTelemetry(\"upload_recovery_journals_cleared\", {\n      reason: \"post-crash-recovery\",\n      sessionId: storedSession?.id || null,\n      projectId: storedSession?.projectId || null,\n      clearedKeyCount: uniqKeys.length,\n      screenJournalKey: screenUpl?.journalKey || null,\n      screenMediaId: screenUpl?.mediaId || null,\n      screenJournalOffset: screenUpl?.offset || 0,\n      screenJournalTotalBytes: screenUpl?.totalBytes || 0,\n      screenJournalStatus: screenUpl?.status || null,\n      cameraJournalKey: cameraUpl?.journalKey || null,\n      cameraMediaId: cameraUpl?.mediaId || null,\n      cameraJournalOffset: cameraUpl?.offset || 0,\n    });\n\n    if (!uniqKeys.length) return;\n    try {\n      await chrome.storage.local.remove(uniqKeys);\n    } catch (err) {\n      console.warn(\n        \"[CloudRecorder] clearStaleUploadJournals: failed to remove keys\",\n        { keys: uniqKeys, error: err?.message || String(err) },\n      );\n    }\n  };\n\n  const tryRecoverPreviousSession = useCallback(async () => {\n    if (recoveryAttempted.current) return;\n    recoveryAttempted.current = true;\n    try {\n      const { recorderSession: storedSession } = await chrome.storage.local.get(\n        [\"recorderSession\"],\n      );\n\n      const [chunkCount, cameraChunkCount, audioChunkCount] = await Promise.all([\n        chunksStore.length().catch(() => 0),\n        cameraChunksStore.length().catch(() => 0),\n        audioChunksStore.length().catch(() => 0),\n      ]);\n      const isRecoverable = RECOVERABLE_SESSION_STATUSES.has(\n        storedSession?.status,\n      );\n      const hasDurableChunks =\n        chunkCount > 0 || cameraChunkCount > 0 || audioChunkCount > 0;\n      if (storedSession && isRecoverable && hasDurableChunks) {\n        void emitUploadTelemetry(\"upload_recovery_available\", {\n          reason: \"durable-chunks\",\n          recoveredChunkCount: chunkCount,\n          recoveredCameraChunkCount: cameraChunkCount,\n          recoveredAudioChunkCount: audioChunkCount,\n          recordingSessionId: storedSession?.id || null,\n          projectId: storedSession?.projectId || null,\n        });\n        const ts = new Date().toISOString();\n\n        if (chunkCount > 0) {\n          const recovered = [];\n          await chunksStore.iterate((value) => {\n            recovered.push(value);\n          });\n          recovered.sort((a, b) => a.index - b.index);\n          const blob = createBlobFromChunks(\n            recovered.map((c) => c.chunk),\n            \"video/webm\",\n          );\n          if (blob) {\n            const objectUrl = URL.createObjectURL(blob);\n            try {\n              await chrome.downloads.download({\n                url: objectUrl,\n                filename: `Screenity-Recovered-${ts}.webm`,\n                saveAs: false,\n              });\n            } finally {\n              URL.revokeObjectURL(objectUrl);\n            }\n          }\n        }\n\n        if (cameraChunkCount > 0) {\n          const cameraRecovered = [];\n          await cameraChunksStore.iterate((value) => {\n            cameraRecovered.push(value);\n          });\n          cameraRecovered.sort((a, b) => (a.index || 0) - (b.index || 0));\n          const cameraBlob = createBlobFromChunks(\n            cameraRecovered.map((c) => c.chunk),\n            \"video/webm\",\n          );\n          if (cameraBlob) {\n            const cameraObjectUrl = URL.createObjectURL(cameraBlob);\n            try {\n              await chrome.downloads.download({\n                url: cameraObjectUrl,\n                filename: `Screenity-Recovered-Camera-${ts}.webm`,\n                saveAs: false,\n              });\n            } finally {\n              URL.revokeObjectURL(cameraObjectUrl);\n            }\n          }\n        }\n\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastRecoveredSession\"),\n        });\n\n        await chunksStore.clear();\n        await clearAudioChunkStore(\"recovery\");\n        await clearCameraChunkStore(\"recovery\");\n        // Clear stale journals and sceneId so the next recording can't accidentally\n        // resume the old partial Bunny upload and produce a garbled video.\n        await clearStaleUploadJournals(storedSession);\n        await chrome.storage.local.set({\n          recorderSession: {\n            ...storedSession,\n            status: \"recovered\",\n            recoveredAt: Date.now(),\n            recoveredChunkCount: chunkCount,\n            recoveredCameraChunkCount: cameraChunkCount,\n          },\n        });\n      } else if (storedSession && isRecoverable) {\n        void emitUploadTelemetry(\"upload_recovery_available\", {\n          reason: \"metadata-only\",\n          recoveredChunkCount: 0,\n          recordingSessionId: storedSession?.id || null,\n          projectId: storedSession?.projectId || null,\n        });\n        // Even with no durable chunks, stale journals must be cleared — the video\n        // map and lookup keys are enough to cause a garbled resume on next start.\n        await clearStaleUploadJournals(storedSession);\n        await chrome.storage.local.set({\n          recorderSession: {\n            ...storedSession,\n            status: \"recovery-metadata-only\",\n            recoveredAt: Date.now(),\n            recoveredChunkCount: 0,\n          },\n        });\n      } else {\n        void emitUploadTelemetry(\"upload_recovery_unavailable\", {\n          reason: \"no-recoverable-session\",\n        });\n      }\n    } catch (err) {\n      console.warn(\"Recovery check failed:\", err);\n    }\n  }, []);\n\n  const recoverPendingScenes = useCallback(async () => {\n    try {\n      const { pendingSceneIndex = [] } = await chrome.storage.local.get([\n        \"pendingSceneIndex\",\n      ]);\n      if (!pendingSceneIndex.length) return;\n\n      for (const sceneId of pendingSceneIndex) {\n        const sceneKey = `pendingScene:${sceneId}`;\n        const result = await chrome.storage.local.get([sceneKey]);\n        const pending = result?.[sceneKey];\n        if (!pending || pending.status === \"created\") {\n          await removePendingScene(sceneId);\n          continue;\n        }\n\n        const { projectId, screenMediaId, cameraMediaId, audioMediaId } =\n          pending;\n\n        if (!projectId) continue;\n\n        await setPipelineState(\"scene-recovering\", {\n          projectId,\n          sceneId,\n        });\n        logDebugEvent(\"scene-recover-attempt\", {\n          projectId,\n          sceneId,\n        });\n\n        const recoverResult = await recoverScene({\n          projectId,\n          sceneId,\n          screenMediaId,\n          cameraMediaId,\n          audioMediaId,\n        });\n\n        if (recoverResult.ok) {\n          await ensureMediaLinked({\n            projectId,\n            sceneId,\n            mediaIds: [screenMediaId, cameraMediaId, audioMediaId],\n          });\n          await setSceneCreateStatus(sceneId, \"created\", {\n            recovered: true,\n          });\n          await removePendingScene(sceneId);\n          await markSceneComplete(sceneId);\n          await setPipelineState(\"scene-recovered\", {\n            projectId,\n            sceneId,\n          });\n          logDebugEvent(\"scene-recover-success\", {\n            projectId,\n            sceneId,\n          });\n        } else {\n          logDebugEvent(\"scene-recover-failed\", {\n            projectId,\n            sceneId,\n            error: recoverResult.error,\n          });\n        }\n      }\n    } catch (err) {\n      console.warn(\"Failed to recover pending scenes\", err);\n    }\n  }, []);\n\n  const attachMicToStream = (videoStream, micStream) => {\n    if (!videoStream || !micStream) return videoStream;\n    return new MediaStream([\n      ...videoStream.getVideoTracks(),\n      ...micStream.getAudioTracks(),\n    ]);\n  };\n\n  const checkMaxMemory = () => {\n    const now = Date.now();\n    if (now - storagePressureRef.current.lastEstimateAt < STORAGE_CHECK_INTERVAL_MS) {\n      return;\n    }\n    storagePressureRef.current.lastEstimateAt = now;\n\n    navigator.storage\n      .estimate()\n      .then((data) => {\n      const minQuota = 25 * 1024 * 1024;\n      const quota = data?.quota || 0;\n      const usage = data?.usage || 0;\n      const headroom = Math.max(0, quota - usage);\n      const lowSpace = headroom > 0 && headroom < STORAGE_LOW_HEADROOM_BYTES;\n      const critical =\n        headroom > 0 && headroom < STORAGE_CRITICAL_HEADROOM_BYTES;\n\n      storagePressureRef.current = {\n        ...storagePressureRef.current,\n        quota,\n        usage,\n        headroom,\n        lowSpace,\n        critical,\n      };\n\n      if (quota < minQuota) {\n        void emitUploadTelemetry(\"upload_storage_pressure\", {\n          severity: \"quota-too-low\",\n          quota,\n          usage,\n          headroom,\n        });\n        chrome.storage.local.set({\n          memoryError: true,\n          cloudRecorderDegradedMode: {\n            reason: \"low-storage-quota\",\n            quota,\n            usage,\n            headroom,\n            at: Date.now(),\n          },\n        });\n        if (!storagePressureRef.current.stopSent) {\n          storagePressureRef.current.stopSent = true;\n          sendStopRecording(\"low-storage-quota\");\n        }\n        return;\n      }\n\n      if (critical) {\n        void emitUploadTelemetry(\"upload_storage_pressure\", {\n          severity: \"critical\",\n          quota,\n          usage,\n          headroom,\n        });\n        chrome.storage.local.set({\n          cloudRecorderDegradedMode: {\n            reason: \"critical-storage-headroom\",\n            quota,\n            usage,\n            headroom,\n            at: Date.now(),\n          },\n        });\n        if (!storagePressureRef.current.stopSent) {\n          storagePressureRef.current.stopSent = true;\n          chrome.runtime.sendMessage({\n            type: \"show-toast\",\n            message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          });\n          sendStopRecording(\"critical-storage-headroom\");\n        }\n      } else if (lowSpace && !storagePressureRef.current.warned) {\n        storagePressureRef.current.warned = true;\n        void emitUploadTelemetry(\"upload_storage_pressure\", {\n          severity: \"warning\",\n          quota,\n          usage,\n          headroom,\n        });\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageLow\"),\n        });\n      } else if (!lowSpace) {\n        storagePressureRef.current.warned = false;\n      }\n      })\n      .catch((err) => {\n        console.warn(\"Failed to estimate storage usage:\", err);\n      });\n  };\n\n  const setMic = async (result) => {\n    if (micStream.current && audioInputGain.current) {\n      // Mute merged audio in the main stream\n      audioInputGain.current.gain.value = result.active ? 1 : 0;\n\n      // Mute transcription-only mic stream too\n      if (rawMicStream.current) {\n        rawMicStream.current.getAudioTracks().forEach((track) => {\n          track.enabled = result.active;\n        });\n      }\n    }\n  };\n\n  const setAudioOutputVolume = (volume) => {\n    if (audioOutputGain.current) {\n      audioOutputGain.current.gain.value = volume;\n    }\n  };\n\n  const setRecordingTimingState = async (nextState) => {\n    try {\n      await chrome.storage.local.set(nextState);\n    } catch (err) {\n      console.warn(\"Failed to persist recording timing state:\", err);\n    }\n  };\n\n  const flushPendingChunks = async () => {\n    if (screenUploader.current) {\n      try {\n        await screenUploader.current.waitForPendingUploads?.();\n      } catch (e) {\n        console.warn(\"Error waiting for screen chunks to finish:\", e);\n      }\n    }\n    if (cameraUploader.current) {\n      try {\n        await cameraUploader.current.waitForPendingUploads?.();\n      } catch (e) {\n        console.warn(\"Error waiting for camera chunks to finish:\", e);\n      }\n    }\n  };\n\n  const deleteProject = async (projectId, uploadMeta, deleteVideo = true) => {\n    const screenMeta = uploadMeta?.screen;\n    const cameraMeta = uploadMeta?.camera;\n\n    const mediaToDelete = [];\n\n    if (screenMeta?.videoId && screenMeta?.mediaId) {\n      mediaToDelete.push({\n        videoId: screenMeta.videoId,\n        mediaId: screenMeta.mediaId,\n        type: \"video\",\n      });\n    }\n\n    if (cameraMeta?.videoId && cameraMeta?.mediaId) {\n      mediaToDelete.push({\n        videoId: cameraMeta.videoId,\n        mediaId: cameraMeta.mediaId,\n        type: \"video\",\n      });\n    }\n\n    if (uploadMeta?.audio?.mediaId && uploadMeta?.audio?.path) {\n      mediaToDelete.push({\n        mediaId: uploadMeta.audio.mediaId,\n        path: uploadMeta.audio.path,\n        type: \"audio\",\n      });\n    }\n\n    await fetch(`${API_BASE}/videos/${projectId}/delete`, {\n      method: \"POST\",\n      credentials: \"include\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        mediaToDelete,\n        deleteProject: deleteVideo,\n        sceneId: uploadMeta.sceneId,\n        purgeAllPendingDeletes: false,\n        deferDeleteUntilCommit: false,\n        forceDelete: true,\n        overrideUsedInCheck: true,\n      }),\n    });\n  };\n\n  const cleanupIfEmptyUploads = async (reason) => {\n    if (emptyCleanupRef.current) return;\n    const screenOffset = screenUploader.current?.offset ?? null;\n    const cameraOffset = cameraUploader.current?.offset ?? null;\n    const hasScreen = screenOffset !== null;\n    const hasCamera = cameraOffset !== null;\n\n    if (!hasScreen && !hasCamera) return;\n\n    const isEmpty =\n      (!hasScreen || screenOffset === 0) && (!hasCamera || cameraOffset === 0);\n\n    if (!isEmpty) return;\n\n    emptyCleanupRef.current = true;\n\n    console.warn(\"[Screenity] empty upload cleanup\", {\n      reason,\n      screenOffset,\n      cameraOffset,\n    });\n\n    await Promise.allSettled([\n      screenUploader.current?.abort?.(),\n      cameraUploader.current?.abort?.(),\n    ]);\n\n    try {\n      const { projectId, recordingToScene } = await chrome.storage.local.get([\n        \"projectId\",\n        \"recordingToScene\",\n      ]);\n      const uploadMeta = uploadMetaRef.current || {\n        screen: screenUploader.current?.getMeta?.() || null,\n        camera: cameraUploader.current?.getMeta?.() || null,\n        audio: null,\n        sceneId:\n          screenUploader.current?.getMeta?.()?.sceneId ||\n          cameraUploader.current?.getMeta?.()?.sceneId ||\n          null,\n      };\n\n      if (projectId && uploadMeta) {\n        await deleteProject(projectId, uploadMeta, !recordingToScene);\n      }\n    } catch (err) {\n      console.warn(\"❌ Failed to cleanup empty upload:\", err);\n    }\n  };\n\n  const sendRecordingError = (why, cancel = false) => {\n    void cleanupIfEmptyUploads(\"error\");\n    sendRecordingErrorBase(why, cancel);\n  };\n\n  const dismissRecording = async (restarting = false) => {\n    clearPendingStart();\n    setInitProject(false);\n    await cleanupIfEmptyUploads(restarting ? \"restart\" : \"dismiss\");\n\n    // Stop the silent audio keep-alive\n    stopTabKeepAlive();\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    await clearAudioChunkStore(restarting ? \"dismiss-restart\" : \"dismiss-recording\");\n    await clearCameraChunkStore(restarting ? \"dismiss-restart\" : \"dismiss-recording\");\n    await clearLocalScreenPlaybackOffer(\n      restarting ? \"dismiss-restart\" : \"dismiss-recording\",\n    );\n\n    const { projectId, multiMode, multiSceneCount, recordingToScene } =\n      await chrome.storage.local.get([\n        \"projectId\",\n        \"multiMode\",\n        \"multiSceneCount\",\n        \"recordingToScene\",\n      ]);\n\n    if (restarting) {\n      await Promise.allSettled([\n        screenUploader.current?.abort?.(),\n        cameraUploader.current?.abort?.(),\n      ]);\n      if (!recordingToScene) {\n        try {\n          const uploadMeta = uploadMetaRef.current;\n          const { projectId } = await chrome.storage.local.get([\"projectId\"]);\n          if (projectId && uploadMeta) {\n            await deleteProject(projectId, uploadMeta, false);\n          }\n        } catch (err) {\n          console.warn(\"❌ Failed to delete media:\", err);\n        }\n      }\n      try {\n        await finalizeRecorderSession(\"restarting\");\n      } catch {\n        // best effort\n      }\n      uploadMetaRef.current = null;\n      sentLast.current = false;\n      screenUploader.current = null;\n      cameraUploader.current = null;\n      uploadersInitialized.current = false;\n      cleanupTimers();\n      return;\n    }\n\n    cleanupTimers();\n    stopAllIntervals();\n\n    isRestarting.current = true;\n\n    await Promise.allSettled([\n      screenUploader.current?.abort?.(),\n      cameraUploader.current?.abort?.(),\n    ]);\n\n    await stopRecording(false);\n\n    const uploadMeta = uploadMetaRef.current;\n    if (!uploadMeta) {\n      console.warn(\"No upload metadata available for cleanup\");\n      return;\n    }\n\n    if (multiMode) {\n      if (multiSceneCount === 0) {\n        if (!recordingToScene) {\n          // Only delete if new project\n          if (projectId) {\n            try {\n              await deleteProject(projectId, uploadMeta);\n            } catch (err) {\n              console.warn(\"❌ Failed to delete project:\", err);\n            }\n          }\n        }\n        await chrome.storage.local.remove([\n          \"multiProjectId\",\n          \"multiSceneCount\",\n        ]);\n        await chrome.storage.local.remove(\"projectId\");\n      }\n\n      uploadMetaRef.current = null;\n\n      // FLAG: decide whether to close or not\n      // window.close();\n      return;\n    }\n\n    // Not multi-mode: original logic\n    if (projectId && !recordingToScene) {\n      // Only delete if new project\n      try {\n        await deleteProject(projectId, uploadMeta);\n      } catch (err) {\n        console.warn(\"❌ Failed to delete project:\", err);\n      }\n    } else if (projectId) {\n      // Only delete media, not project\n      try {\n        await deleteProject(projectId, uploadMeta, false);\n      } catch (err) {\n        console.warn(\"❌ Failed to delete media:\", err);\n      }\n    }\n\n    if (!multiMode) {\n      await chrome.storage.local.remove(\"projectId\");\n    }\n    uploadMetaRef.current = null;\n    isInit.current = false;\n\n    // FLAG: decide whether to close or not\n    if (!IS_IFRAME_CONTEXT) {\n      try {\n        window.close();\n      } catch {}\n      return;\n    }\n    // iframe context\n    try {\n      window.parent.postMessage({ type: \"screenity-exit\", mode }, \"*\");\n    } catch {}\n    // fallback\n    window.location.reload();\n  };\n\n  const restartRecording = async () => {\n    await setCloudRestartPhase(\"restart-requested\", {\n      hasTarget: Boolean(target.current),\n      hadRegionMode: Boolean(regionRef.current),\n    });\n    isRestarting.current = true;\n    await dismissRecording(true);\n    await setCloudRestartPhase(\"restart-dismissed\");\n    // Restart keeps existing capture streams; only recorder instances should stop.\n    await stopAllRecorders({ stopStreams: false });\n    await setCloudRestartPhase(\"restart-recorders-stopped\");\n    screenChunks.current = [];\n    cameraChunks.current = [];\n    audioChunks.current = [];\n    await clearAudioChunkStore(\"restart\");\n    await clearCameraChunkStore(\"restart\");\n    resetSessionTrackState();\n    recoveryExportedRef.current = false;\n    audioCaptureDegradedRef.current = false;\n    index.current = 0;\n    lastTimecode.current = 0;\n\n    if (IS_IFRAME_CONTEXT && !target.current) {\n      sendRecordingError(\"No crop target for restart.\");\n      return false;\n    }\n\n    uploadersInitialized.current = await initializeUploaders({\n      forceNewSceneId: true,\n    });\n    if (!uploadersInitialized.current) {\n      await setCloudRestartPhase(\"restart-failed\", {\n        reason: \"uploaders-init-failed\",\n      });\n      sendRecordingError(\"Failed to re-initialize uploaders on restart.\");\n      return false;\n    }\n    if (IS_IFRAME_CONTEXT && target.current) {\n      regionRef.current = true;\n    }\n    await setCloudRestartPhase(\"restart-uploaders-initialized\", {\n      hasTarget: Boolean(target.current),\n      regionMode: Boolean(regionRef.current),\n    });\n    await setCloudRestartPhase(\"restart-ready\");\n    return true;\n  };\n\n  const initializeUploaders = async ({ forceNewSceneId = false } = {}) => {\n    try {\n      emptyCleanupRef.current = false;\n      const { projectId } = await chrome.storage.local.get([\"projectId\"]);\n      const sessionId = ensureRecordingSessionId();\n\n      const sceneId = await getOrCreateSceneId({\n        forceNew: forceNewSceneId,\n      });\n      await chrome.storage.local.set({\n        sceneId,\n        sceneIdStatus: \"recording\",\n      });\n      await setPipelineState(\"uploaders-initializing\", {\n        projectId,\n        sceneId,\n      });\n      logDebugEvent(\"uploaders-init-start\", { projectId, sceneId });\n\n      if (!projectId) {\n        throw new Error(\"Missing projectId\");\n      }\n\n      const onUploaderTelemetry =\n        (trackType) => (eventName, payload = {}) => {\n          void emitUploadTelemetry(eventName, {\n            projectId,\n            sceneId,\n            trackType,\n            ...payload,\n          });\n        };\n\n      const onUploaderStateChange = (trackType) => (state = {}) => {\n        patchTrackState(trackType, {\n          lastUploadOffset: state?.offset || 0,\n          uploaderUpdatedAt: Date.now(),\n        });\n        persistSessionState();\n        if (state?.status === \"error\" && state?.error) {\n          persistSessionState({\n            uploadHealth: \"error\",\n            uploadError: state.error,\n            uploadErrorTrack: trackType,\n          });\n        }\n      };\n\n      const onStall = (trackType) => (payload) => {\n        stallNotified.current = true;\n        persistSessionState({\n          uploadHealth: \"stalled\",\n          stalledAt: Date.now(),\n          stalledMediaId: payload?.mediaId || null,\n          stalledOffset: payload?.offset || 0,\n          stalledTrack: trackType,\n        });\n        void emitUploadTelemetry(\"upload_stalled\", {\n          projectId,\n          sceneId,\n          trackType,\n          mediaId: payload?.mediaId || null,\n          bunnyVideoId: payload?.videoId || null,\n          offset: payload?.offset || 0,\n          stallMs: payload?.diff || null,\n          reason: payload?.reason || \"heartbeat\",\n          uploaderType: \"bunny_tus\",\n        });\n        chrome.runtime.sendMessage({\n          type: \"upload-stalled\",\n          offset: {\n            screen: payload?.offset || screenUploader.current?.offset || 0,\n            camera: cameraUploader.current?.offset || 0,\n          },\n        });\n      };\n\n      if (screenStream.current) {\n        screenUploader.current = new BunnyTusUploader({\n          sessionId,\n          trackType: \"screen\",\n          onProgress: ({ offset }) => {\n            const now = Date.now();\n            lastUploadProgress.current = {\n              ...lastUploadProgress.current,\n              screen: offset,\n              ts: now,\n            };\n            patchTrackState(\"screen\", {\n              lastUploadOffset: offset,\n              uploaderUpdatedAt: now,\n            });\n            if (now - (lastUploadProgress.current.lastPersistAt || 0) > 2000) {\n              lastUploadProgress.current.lastPersistAt = now;\n              persistSessionState();\n            }\n          },\n          onStall: onStall(\"screen\"),\n          onTelemetry: onUploaderTelemetry(\"screen\"),\n          onStateChange: onUploaderStateChange(\"screen\"),\n        });\n        const track = screenStream.current.getVideoTracks()[0];\n        let width, height;\n        if (regionRef.current && target.current) {\n          width = regionWidth.current;\n          height = regionHeight.current;\n        } else {\n          const settings = track.getSettings();\n          width = settings.width;\n          height = settings.height;\n        }\n        await screenUploader.current.initialize(projectId, {\n          title: \"Screen Recording\",\n          type: \"screen\",\n          width,\n          height,\n          sceneId,\n          sessionId,\n        });\n        logDebugEvent(\"uploader-ready\", {\n          type: \"screen\",\n          projectId,\n          sceneId,\n          mediaId: screenUploader.current?.getMeta()?.mediaId || null,\n          videoId: screenUploader.current?.getMeta()?.videoId || null,\n        });\n      }\n\n      if (cameraStream.current) {\n        cameraUploader.current = new BunnyTusUploader({\n          sessionId,\n          trackType: \"camera\",\n          onProgress: ({ offset }) => {\n            const now = Date.now();\n            lastUploadProgress.current = {\n              ...lastUploadProgress.current,\n              camera: offset,\n              ts: now,\n            };\n            patchTrackState(\"camera\", {\n              lastUploadOffset: offset,\n              uploaderUpdatedAt: now,\n            });\n            if (now - (lastUploadProgress.current.lastPersistAt || 0) > 2000) {\n              lastUploadProgress.current.lastPersistAt = now;\n              persistSessionState();\n            }\n          },\n          onStall: onStall(\"camera\"),\n          onTelemetry: onUploaderTelemetry(\"camera\"),\n          onStateChange: onUploaderStateChange(\"camera\"),\n        });\n        const track = cameraStream.current.getVideoTracks()[0];\n        if (track?.readyState === \"ended\") {\n          throw new Error(\"Camera track has ended\");\n        }\n        const { width, height } = track.getSettings();\n        await cameraUploader.current.initialize(projectId, {\n          title: \"Camera Recording\",\n          type: \"camera\",\n          linkedMediaId: screenUploader.current?.getMeta()?.mediaId || null,\n          width,\n          height,\n          sceneId,\n          sessionId,\n        });\n        logDebugEvent(\"uploader-ready\", {\n          type: \"camera\",\n          projectId,\n          sceneId,\n          mediaId: cameraUploader.current?.getMeta()?.mediaId || null,\n          videoId: cameraUploader.current?.getMeta()?.videoId || null,\n        });\n      }\n\n      await setPipelineState(\"uploaders-ready\", {\n        projectId,\n        sceneId,\n      });\n      logDebugEvent(\"uploaders-init-complete\", { projectId, sceneId });\n      return true;\n    } catch (err) {\n      console.error(\"❌ Failed to initialize uploaders:\", err);\n      void emitUploadTelemetry(\"upload_error\", {\n        reason: \"uploaders-init-failed\",\n        message: err?.message || String(err),\n        uploaderType: \"cloud_recorder\",\n      });\n      sendRecordingError(\"Failed to initialize uploaders: \" + err.message);\n      await setPipelineState(\"uploaders-error\", {\n        status: \"error\",\n        details: err?.message || String(err),\n      });\n      logDebugEvent(\"uploaders-init-failed\", { error: err?.message || err });\n      return false;\n    }\n  };\n\n  const createMediaRecorder = (stream, options, onDataAvailable) => {\n    try {\n      const recorder = new MediaRecorder(stream, options);\n\n      recorder.ondataavailable = (event) => {\n        if (event.data && event.data.size > 0) {\n          try {\n            const maybePromise = onDataAvailable(event.data);\n            if (maybePromise && typeof maybePromise.catch === \"function\") {\n              maybePromise.catch((err) => {\n                console.warn(\"onDataAvailable failed:\", err);\n              });\n            }\n          } catch (err) {\n            console.warn(\"onDataAvailable failed:\", err);\n          }\n        }\n      };\n\n      recorder.onerror = (event) => {\n        console.error(\"MediaRecorder error:\", event.error);\n        sendRecordingError(\"Recording error: \" + event.error?.message);\n      };\n\n      return recorder;\n    } catch (err) {\n      console.error(\"Failed to create MediaRecorder:\", err);\n      throw err;\n    }\n  };\n\n  const startRecording = async () => {\n    setInitProject(false);\n    await clearLocalScreenPlaybackOffer(\"start-recording\");\n    // Start silent audio to prevent Chrome from freezing this background tab\n    startTabKeepAlive();\n\n    const storageReady = await ensureChunkStoreReady();\n    if (!storageReady) return;\n    await ensureAudioChunkStoreReady();\n    await ensureCameraChunkStoreReady();\n\n    if (!uploadersInitialized.current) {\n      sendRecordingError(\n        \"Uploaders not initialized. Please restart recording.\",\n      );\n      return;\n    }\n\n    if (!screenStream.current && !cameraStream.current) {\n      sendRecordingError(\"No streams to record\");\n      logStartFlow(\"recording_error\", { reason: \"no-streams\" });\n      return;\n    }\n\n    const { projectId } = await chrome.storage.local.get([\"projectId\"]);\n\n    if (!projectId) {\n      sendRecordingError(\"No project ID found. Please restart recording.\");\n      return;\n    }\n\n    await setPipelineState(\"recording-starting\", { projectId });\n    logDebugEvent(\"recording-starting\", { projectId });\n\n    if (isTab.current && recordingTabId.current) {\n      const tabAlive = await chrome.tabs\n        .get(recordingTabId.current)\n        .then(() => true)\n        .catch(() => false);\n      if (!tabAlive) {\n        sendRecordingError(\n          \"The tab you selected for recording was closed. Please start again.\",\n        );\n        return;\n      }\n    }\n\n    if (!recorderSession.current) {\n      const registered = await startRecorderSession({\n        projectId,\n        sessionId: ensureRecordingSessionId(),\n      });\n      if (!registered) return;\n    }\n\n    await Promise.allSettled([\n      screenUploader.current?.setSessionId?.(recorderSession.current?.id || null),\n      cameraUploader.current?.setSessionId?.(recorderSession.current?.id || null),\n    ]);\n\n    if (!screenUploader.current && !cameraUploader.current) {\n      sendRecordingError(\"Uploaders not ready. Please restart recording.\");\n      logStartFlow(\"recording_error\", { reason: \"uploaders-not-ready\" });\n      return;\n    }\n\n    try {\n      await chunksStore.clear();\n      await clearAudioChunkStore(\"start-recording\");\n      await clearCameraChunkStore(\"start-recording\");\n    } catch (err) {\n      fatalErrorRef.current = true;\n      sendRecordingError(\n        \"Unable to initialize local buffer (IndexedDB issue).\",\n      );\n      return;\n    }\n    lastTimecode.current = 0;\n    hasChunks.current = false;\n    index.current = 0;\n    firstChunkLoggedRef.current = false;\n    resetUnloadGuard();\n    recoveryExportedRef.current = false;\n    storagePressureRef.current.stopSent = false;\n    storagePressureRef.current.warned = false;\n    networkStateRef.current = {\n      online: navigator.onLine,\n      offlineSince: navigator.onLine ? null : Date.now(),\n    };\n    audioCaptureDegradedRef.current = false;\n    resetSessionTrackState();\n\n    // Clear blob storage arrays\n    screenChunks.current = [];\n    cameraChunks.current = [];\n    audioChunks.current = [];\n\n    navigator.storage.persist();\n\n    const screenTrack =\n      screenStream.current?.getVideoTracks?.()[0] ?? null;\n    const cameraTrack =\n      cameraStream.current?.getVideoTracks?.()[0] ?? null;\n\n    if (screenStream.current && !screenTrack) {\n      sendRecordingError(\"No screen video track available.\");\n      logStartFlow(\"recording_error\", { reason: \"no-screen-track\" });\n      return;\n    }\n\n    if (recordingType.current === \"camera\" && !cameraTrack) {\n      sendRecordingError(\"No camera video track available.\");\n      logStartFlow(\"recording_error\", { reason: \"no-camera-track\" });\n      return;\n    }\n\n    try {\n      logStartFlow(\"recording_start\", {\n        recordingType: recordingType.current,\n        hasScreen: Boolean(screenStream.current),\n        hasCamera: Boolean(cameraStream.current),\n        hasMic: Boolean(micStream.current),\n      });\n      // Screen recorder setup\n      if (screenStream.current) {\n        const stream = attachMicToStream(\n          screenStream.current,\n          micStream.current,\n        );\n\n        const screenOptions = {\n          mimeType: \"video/webm;codecs=vp9,opus\",\n          videoBitsPerSecond: 16000000, // 16 Mbps\n          audioBitsPerSecond: 128000, // 128 Kbps\n        };\n\n        screenRecorder.current = createMediaRecorder(\n          stream,\n          screenOptions,\n          async (blob) => {\n            checkMaxMemory();\n\n            // Detect empty chunks which indicate recording failure\n            if (!blob || blob.size === 0) {\n              console.error(\"❌ MediaRecorder produced empty chunk!\");\n              consecutiveScreenFailures.current++;\n              if (consecutiveScreenFailures.current > 3) {\n                sendRecordingError(\n                  \"Recording failed - no video data being captured. Please try again.\",\n                );\n                sendStopRecording(\"empty-screen-chunk\");\n              }\n              return;\n            }\n\n            // Store for final blob creation\n            screenChunks.current.push(blob);\n            const screenDropped = trimChunkBuffer(\n              screenChunks,\n              SCREEN_CHUNK_MEMORY_WINDOW,\n            );\n            noteTrackChunk(\"screen\", blob, {\n              inMemoryChunkCount: screenChunks.current.length,\n              droppedInMemoryChunks:\n                (sessionTrackState.current.screen?.droppedInMemoryChunks || 0) +\n                screenDropped,\n            });\n\n            const timestamp = Date.now();\n\n            if (!hasChunks.current) {\n              hasChunks.current = true;\n              lastTimecode.current = timestamp;\n              firstChunkTime.current = timestamp;\n              if (!firstChunkLoggedRef.current) {\n                firstChunkLoggedRef.current = true;\n                logStartFlow(\"first_chunk\", { type: \"screen\" });\n                assertCountdownBeforeFirstChunk(timestamp);\n              }\n            } else if (timestamp < lastTimecode.current) {\n              return; // Skip duplicate\n            } else {\n              lastTimecode.current = timestamp;\n            }\n\n            await chunksStore\n              .setItem(`chunk_${index.current}`, {\n                index: index.current,\n                chunk: blob,\n                timestamp,\n              })\n              .catch((err) => {\n                fatalErrorRef.current = true;\n                sendRecordingError(\n                  \"Could not buffer recording data locally (IndexedDB blocked).\",\n                );\n                sendStopRecording();\n                throw err;\n              });\n\n            patchTrackState(\"screen\", {\n              durableChunkCount: index.current + 1,\n              lastPersistedChunkIndex: index.current,\n              lastPersistedAt: timestamp,\n            });\n\n            persistSessionState({ lastChunkIndex: index.current });\n\n            if (uploadersInitialized.current && screenUploader.current) {\n              try {\n                if (screenUploader.current?.isPaused) return;\n                if (screenUploader.current.queuedBytes > 15 * 1024 * 1024) {\n                  await screenUploader.current\n                    .waitForPendingUploads?.()\n                    .catch(() => {});\n                }\n                await screenUploader.current.write(blob);\n                consecutiveScreenFailures.current = 0; // Reset on success\n              } catch (uploadErr) {\n                console.error(\"Failed to upload chunk to Bunny:\", uploadErr);\n                consecutiveScreenFailures.current++;\n                if (consecutiveScreenFailures.current > 3) {\n                  stallNotified.current = true;\n                  sendRecordingError(\n                    \"Screen upload failed repeatedly. Continuing locally; upload will retry on finalize.\",\n                  );\n                  screenUploader.current?.pause?.();\n                }\n              }\n            }\n\n            if (backupRef.current) {\n              chrome.runtime.sendMessage({\n                type: \"write-file\",\n                index: index.current,\n              });\n            }\n\n            index.current++;\n          },\n        );\n\n        // Start recording with 2-second time slices\n        screenRecorder.current.start(2000);\n\n        if (screenTrack) {\n          bindScreenTrack(screenTrack);\n        }\n      }\n\n      // Audio recorder setup (for transcription)\n      if (rawMicStream.current?.getAudioTracks?.().length) {\n        const audioOptions = {\n          mimeType: \"audio/webm\", // Using webm instead of wav for better browser support\n          audioBitsPerSecond: 128000,\n        };\n\n        audioRecorder.current = createMediaRecorder(\n          rawMicStream.current,\n          audioOptions,\n          async (blob) => {\n            const timestamp = Date.now();\n            // Keep a small in-memory window for UI/quick fallback only.\n            audioChunks.current.push(blob);\n            const droppedAudio = trimChunkBuffer(\n              audioChunks,\n              AUDIO_CHUNK_MEMORY_WINDOW,\n            );\n            noteTrackChunk(\"audio\", blob, {\n              inMemoryChunkCount: audioChunks.current.length,\n              droppedInMemoryChunks:\n                (sessionTrackState.current.audio?.droppedInMemoryChunks || 0) +\n                droppedAudio,\n            });\n            if (audioChunkStoreReadyRef.current) {\n              const audioChunkIndex = audioChunkIndexRef.current;\n              audioChunkIndexRef.current += 1;\n              try {\n                await audioChunksStore.setItem(`audio_chunk_${audioChunkIndex}`, {\n                  index: audioChunkIndex,\n                  chunk: blob,\n                  timestamp,\n                });\n                patchTrackState(\"audio\", {\n                  durableChunkCount: audioChunkIndex + 1,\n                  lastPersistedChunkIndex: audioChunkIndex,\n                  lastPersistedAt: timestamp,\n                });\n              } catch (err) {\n                audioChunkStoreReadyRef.current = false;\n                void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n                  backupType: \"audio\",\n                  reason: \"audio-idb-write-failed\",\n                  error: err?.message || String(err),\n                });\n              }\n            }\n            if (\n              !audioCaptureDegradedRef.current &&\n              (sessionTrackState.current.audio?.bytesRecorded || 0) >\n                AUDIO_MAX_BUFFER_BYTES\n            ) {\n              audioCaptureDegradedRef.current = true;\n              void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n                backupType: \"audio\",\n                reason: \"audio-buffer-limit\",\n                bytesRecorded:\n                  sessionTrackState.current.audio?.bytesRecorded || 0,\n              });\n              persistSessionState({\n                degradedReason: \"audio-buffer-limit\",\n                audioCaptureDegradedAt: Date.now(),\n              });\n              chrome.runtime.sendMessage({\n                type: \"show-toast\",\n                message: chrome.i18n.getMessage(\"toastAudioCaptureDegraded\"),\n              });\n              try {\n                if (audioRecorder.current?.state === \"recording\") {\n                  audioRecorder.current.stop();\n                }\n              } catch (err) {\n                console.warn(\"Failed to stop audio recorder after degradation:\", err);\n              }\n              return;\n            }\n            if (!firstChunkLoggedRef.current) {\n              firstChunkLoggedRef.current = true;\n              logStartFlow(\"first_chunk\", { type: \"audio\" });\n              assertCountdownBeforeFirstChunk(Date.now());\n            }\n          },\n        );\n\n        audioRecorder.current.start(2000);\n      }\n\n      // Camera recorder setup\n      if (cameraStream.current) {\n        let streamToRecord = cameraStream.current;\n\n        if (recordingType.current === \"camera\") {\n          if (micStream.current) {\n            streamToRecord = attachMicToStream(\n              cameraStream.current,\n              micStream.current,\n            );\n          } else {\n            console.warn(\"⚠️ Camera-only recording: microphone not available\");\n          }\n        }\n\n        const cameraOptions = {\n          mimeType:\n            recordingType.current === \"camera\"\n              ? \"video/webm;codecs=vp9,opus\"\n              : \"video/webm;codecs=vp9\",\n          videoBitsPerSecond: 16000000, // 16 Mbps\n          audioBitsPerSecond: 128000, // 128 Kbps\n        };\n\n        cameraRecorder.current = createMediaRecorder(\n          streamToRecord,\n          cameraOptions,\n          async (blob) => {\n            // Detect empty chunks\n            if (!blob || blob.size === 0) {\n              console.warn(\"⚠️ Camera MediaRecorder produced empty chunk\");\n              consecutiveCameraFailures.current++;\n              return;\n            }\n\n            // Store for final blob creation\n            cameraChunks.current.push(blob);\n            const cameraDropped = trimChunkBuffer(\n              cameraChunks,\n              CAMERA_CHUNK_MEMORY_WINDOW,\n            );\n            noteTrackChunk(\"camera\", blob, {\n              inMemoryChunkCount: cameraChunks.current.length,\n              droppedInMemoryChunks:\n                (sessionTrackState.current.camera?.droppedInMemoryChunks || 0) +\n                cameraDropped,\n            });\n            if (!firstChunkLoggedRef.current) {\n              firstChunkLoggedRef.current = true;\n              logStartFlow(\"first_chunk\", { type: \"camera\" });\n              assertCountdownBeforeFirstChunk(Date.now());\n            }\n\n            if (cameraChunkStoreReadyRef.current) {\n              const cameraChunkIndex = cameraChunkIndexRef.current;\n              cameraChunkIndexRef.current += 1;\n              try {\n                await cameraChunksStore.setItem(\n                  `camera_chunk_${cameraChunkIndex}`,\n                  {\n                    index: cameraChunkIndex,\n                    chunk: blob,\n                    timestamp: Date.now(),\n                  },\n                );\n                patchTrackState(\"camera\", {\n                  durableChunkCount: cameraChunkIndex + 1,\n                  lastPersistedChunkIndex: cameraChunkIndex,\n                  lastPersistedAt: Date.now(),\n                });\n              } catch (err) {\n                cameraChunkStoreReadyRef.current = false;\n                console.warn(\"Failed to persist camera chunk to IDB:\", err);\n                void emitUploadTelemetry(\"upload_local_backup_degraded\", {\n                  backupType: \"camera\",\n                  reason: \"camera-idb-write-failed\",\n                  error: err?.message || String(err),\n                });\n              }\n            }\n\n            if (uploadersInitialized.current && cameraUploader.current) {\n              try {\n                if (cameraUploader.current?.isPaused) return;\n                await cameraUploader.current.write(blob);\n                consecutiveCameraFailures.current = 0; // Reset on success\n              } catch (uploadErr) {\n                console.error(\n                  \"Failed to upload camera chunk to Bunny:\",\n                  uploadErr,\n                );\n                consecutiveCameraFailures.current++;\n                if (consecutiveCameraFailures.current > 3) {\n                  console.error(\n                    \"Camera upload failing repeatedly; pausing camera uploader\",\n                  );\n                  cameraUploader.current?.pause?.();\n                }\n              }\n            }\n          },\n        );\n\n        cameraRecorder.current.start(2000);\n      }\n\n      let warned = false;\n      const MAX_DURATION =\n        parseFloat(process.env.MAX_RECORDING_DURATION) || 60 * 60;\n      const WARNING_THRESHOLD =\n        parseFloat(process.env.RECORDING_WARNING_THRESHOLD) || 60;\n\n      if (screenTimer.current.notificationInterval)\n        clearInterval(screenTimer.current.notificationInterval);\n\n      const timerInterval = setInterval(() => {\n        // stop if not recording anymore\n        if (!screenStream.current && !cameraStream.current) {\n          clearInterval(timerInterval);\n          cleanupTimers();\n          console.warn(\"Recording stopped, clearing timer\");\n          return;\n        }\n\n        const elapsed = getActiveVideoTime() / 1000;\n        const remaining = MAX_DURATION - elapsed;\n\n        if (!warned && remaining <= WARNING_THRESHOLD) {\n          warned = true;\n          chrome.runtime.sendMessage({\n            type: \"time-warning\",\n          });\n        }\n\n        if (remaining <= 0) {\n          clearInterval(timerInterval);\n          chrome.runtime.sendMessage({\n            type: \"time-stopped\",\n          });\n          sendStopRecording(\"max-duration\");\n        }\n      }, 1000);\n\n      // Save to clear later\n      screenTimer.current.notificationInterval = timerInterval;\n      screenTimer.current.warned = warned;\n\n      const recordingStartTime = Date.now();\n      await setRecordingTimingState({\n        recording: true,\n        paused: false,\n        recordingStartTime,\n        pausedAt: null,\n        totalPausedMs: 0,\n      });\n      pausedStateRef.current = false;\n      persistSessionState({ status: \"recording\" });\n      startSessionHeartbeat();\n      setStarted(true);\n      await setPipelineState(\"recording\", {\n        projectId,\n        sceneId:\n          screenUploader.current?.getMeta()?.sceneId ||\n          cameraUploader.current?.getMeta()?.sceneId ||\n          null,\n      });\n      logDebugEvent(\"recording-started\", {\n        projectId,\n        sceneId:\n          screenUploader.current?.getMeta()?.sceneId ||\n          cameraUploader.current?.getMeta()?.sceneId ||\n          null,\n      });\n    } catch (err) {\n      sendRecordingError(\"Recording failed: \" + err.message);\n    }\n\n    const now = Date.now();\n    if (screenStream.current) {\n      screenTimer.current.start = now;\n      screenTimer.current.paused = false;\n      screenTimer.current.total = 0;\n    }\n    if (cameraStream.current) {\n      cameraTimer.current.start = now;\n      cameraTimer.current.paused = false;\n      cameraTimer.current.total = 0;\n    }\n\n    lastUploadProgress.current = {\n      screen: screenUploader.current?.offset || 0,\n      camera: cameraUploader.current?.offset || 0,\n      ts: Date.now(),\n      lastPersistAt: Date.now(),\n    };\n    startChunkWatchdog();\n    startUploadHeartbeat();\n  };\n\n  function cleanupTimers() {\n    const interval = screenTimer.current.notificationInterval;\n    if (interval) clearInterval(interval);\n    screenTimer.current.notificationInterval = null;\n    screenTimer.current.warned = false;\n    screenTimer.current.start = null;\n    screenTimer.current.paused = false;\n    cameraTimer.current.start = null;\n    cameraTimer.current.paused = false;\n  }\n\n  async function uploadAudioToBunny(audioFile, projectId) {\n    const formData = new FormData();\n    formData.append(\"file\", audioFile);\n    formData.append(\"projectId\", projectId);\n    formData.append(\"type\", \"audio\");\n\n    const res = await fetch(`${API_BASE}/bunny/upload`, {\n      method: \"POST\",\n      body: formData,\n      credentials: \"include\",\n    });\n\n    const result = await res.json();\n    if (!res.ok) throw new Error(result?.error || \"Audio upload failed\");\n\n    return result;\n  }\n\n  const stopAllRecorders = async ({ stopStreams = true } = {}) => {\n    const stopRecorder = async (recorderRef) => {\n      if (recorderRef.current && recorderRef.current.state !== \"inactive\") {\n        try {\n          await Promise.race([\n            new Promise((resolve) => {\n              recorderRef.current.onstop = resolve;\n              recorderRef.current.stop();\n            }),\n            new Promise((_, reject) =>\n              setTimeout(() => reject(new Error(\"Stop timeout\")), 5000),\n            ),\n          ]);\n        } catch (err) {\n          console.error(\"Error stopping recorder:\", err);\n          // Force resolve to prevent hanging\n        }\n      }\n    };\n\n    await Promise.all([\n      stopRecorder(screenRecorder),\n      stopRecorder(cameraRecorder),\n      stopRecorder(audioRecorder),\n    ]);\n\n    if (stopStreams) {\n      [screenStream, cameraStream, micStream, rawMicStream].forEach((ref) => {\n        ref.current?.getTracks().forEach((track) => track.stop());\n      });\n    }\n  };\n\n  const createBlobFromChunks = (chunks, mimeType) => {\n    if (!chunks || chunks.length === 0) return null;\n    return new Blob(chunks, { type: mimeType });\n  };\n\n  const MIN_DURATION_SECONDS = 1;\n\n  const calculateDurations = () => {\n    const now = Date.now();\n\n    const getDuration = (timer) => {\n      const elapsed = !timer.paused && timer.start ? now - timer.start : 0;\n      const total = (timer.total || 0) + elapsed;\n      const duration = total / 1000;\n\n      // If timer ever started but duration is zero, use min duration\n      if (timer.start && duration < MIN_DURATION_SECONDS) {\n        return MIN_DURATION_SECONDS;\n      }\n\n      return duration;\n    };\n\n    return {\n      screen: getDuration(screenTimer.current),\n      camera: getDuration(cameraTimer.current),\n      fallbackMs:\n        firstChunkTime.current && lastTimecode.current\n          ? Math.max(0, lastTimecode.current - firstChunkTime.current)\n          : null,\n    };\n  };\n\n  const handleAudioUpload = async (audioBlob, projectId, uploadMeta) => {\n    if (!audioBlob) return;\n\n    try {\n      const audioFile = new File([audioBlob], \"audio-recording.webm\", {\n        type: \"audio/webm\",\n      });\n      logDebugEvent(\"audio-upload-start\", {\n        projectId,\n        sceneId: uploadMeta?.sceneId || null,\n      });\n      const result = await uploadAudioToBunny(audioFile, projectId);\n\n      logDebugEvent(\"audio-upload-complete\", {\n        projectId,\n        sceneId: uploadMeta?.sceneId || null,\n        mediaId: result?.mediaId || null,\n      });\n      return result;\n    } catch (err) {\n      console.warn(\"❌ Audio upload/transcription failed:\", err);\n      logDebugEvent(\"audio-upload-failed\", {\n        projectId,\n        sceneId: uploadMeta?.sceneId || null,\n        error: err?.message || String(err),\n      });\n    }\n  };\n\n  const handleTranscription = async (uploadMeta, projectId) => {\n    if (!uploadMeta.audio || !uploadMeta.audio.mediaId) return;\n    try {\n      const transcriptionTarget =\n        uploadMeta.screen?.mediaId || uploadMeta.camera?.mediaId;\n\n      if (transcriptionTarget && uploadMeta.audio?.url) {\n        const dedupeKey = `transcriptionQueued:${uploadMeta.sceneId}:${uploadMeta.audio.mediaId}`;\n        const dedupe = await chrome.storage.local.get([dedupeKey]);\n        if (dedupe?.[dedupeKey]) return;\n\n        await fetch(`${API_BASE}/transcription/queue`, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          credentials: \"include\",\n          body: JSON.stringify({\n            input: uploadMeta.audio.url,\n            output: `transcriptions/${uploadMeta.audio.mediaId}.json`,\n            videoId: projectId,\n            sceneId: uploadMeta.sceneId,\n            inputMediaId: uploadMeta.audio.mediaId,\n            targetMediaId: transcriptionTarget,\n            lang: \"en\",\n            model: \"tiny\",\n          }),\n        });\n        await chrome.storage.local.set({ [dedupeKey]: true });\n        logDebugEvent(\"transcription-queued\", {\n          projectId,\n          sceneId: uploadMeta.sceneId,\n          inputMediaId: uploadMeta.audio.mediaId,\n          targetMediaId: transcriptionTarget,\n        });\n      }\n    } catch (err) {\n      console.warn(\"❌ Transcription failed:\", err);\n      logDebugEvent(\"transcription-failed\", {\n        projectId,\n        sceneId: uploadMeta.sceneId,\n        error: err?.message || String(err),\n      });\n    }\n  };\n\n  useEffect(() => {\n    const onHide = (event) => {\n      // Only finalize if the page is actually being unloaded (persisted = false)\n      // Don't finalize just because the tab went to background\n      // event.persisted indicates if the page might be restored from bfcache\n      const isActualUnload = !event.persisted;\n\n      // Only finalize if an active recording really exists\n      const hasActiveRecorder =\n        screenRecorder.current?.state === \"recording\" ||\n        cameraRecorder.current?.state === \"recording\" ||\n        audioRecorder.current?.state === \"recording\";\n\n      if (!unloadGuardRef.current.pagehideSeen) {\n        unloadGuardRef.current.pagehideSeen = true;\n        void emitUploadTelemetry(\"upload_pagehide\", {\n          persisted: Boolean(event.persisted),\n          hasActiveRecorder,\n        });\n      }\n\n      if (\n        hasActiveRecorder &&\n        !sentLast.current &&\n        !isFinishing.current &&\n        isActualUnload &&\n        !unloadGuardRef.current.stopTriggeredFromUnload\n      ) {\n        unloadGuardRef.current.stopTriggeredFromUnload = true;\n        emitAbandonedOnUnloadOnce(\"pagehide\");\n        console.warn(\"⚠️ Recorder page unloading — finalizing\");\n        // Use sendBeacon or sync storage write for reliability\n        navigator.sendBeacon?.(\n          `${API_BASE}/log/recorder-unload`,\n          JSON.stringify({ reason: \"pagehide\", ts: Date.now() }),\n        );\n        stopRecording(true, \"pagehide-unload\");\n      }\n    };\n\n    window.addEventListener(\"pagehide\", onHide);\n    return () => window.removeEventListener(\"pagehide\", onHide);\n  }, []);\n\n  useEffect(() => {\n    const onVisibilityChange = () => {\n      if (document.visibilityState === \"hidden\") {\n        persistSessionState({\n          visibilityState: \"hidden\",\n          visibilityChangedAt: Date.now(),\n        });\n      }\n    };\n\n    const onBeforeUnload = () => {\n      if (hasChunks.current) {\n        if (!unloadGuardRef.current.beforeUnloadSeen) {\n          unloadGuardRef.current.beforeUnloadSeen = true;\n          emitAbandonedOnUnloadOnce(\"beforeunload\");\n        }\n        persistSessionState({\n          unloadAt: Date.now(),\n          unloadReason: \"beforeunload\",\n        });\n      }\n    };\n\n    document.addEventListener(\"visibilitychange\", onVisibilityChange);\n    window.addEventListener(\"beforeunload\", onBeforeUnload);\n\n    return () => {\n      document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n      window.removeEventListener(\"beforeunload\", onBeforeUnload);\n    };\n  }, []);\n\n  useEffect(() => {\n    const onOffline = () => {\n      networkStateRef.current = {\n        online: false,\n        offlineSince: Date.now(),\n      };\n      screenUploader.current?.pause?.();\n      cameraUploader.current?.pause?.();\n      void emitUploadTelemetry(\"upload_stalled\", {\n        reason: \"network-offline\",\n        uploaderType: \"cloud_recorder\",\n      });\n      persistSessionState({\n        networkOfflineAt: networkStateRef.current.offlineSince,\n        degradedReason: \"network-offline\",\n      });\n      chrome.runtime.sendMessage({\n        type: \"show-toast\",\n        message: chrome.i18n.getMessage(\"toastNetworkOffline\"),\n      });\n    };\n\n    const RESUMABLE_UPLOADER_STATUSES = new Set([\n      \"uploading\",\n      \"paused\",\n      \"error\",\n      \"ready\",\n    ]);\n\n    const onOnline = () => {\n      networkStateRef.current = {\n        online: true,\n        offlineSince: null,\n      };\n      if (\n        screenUploader.current &&\n        RESUMABLE_UPLOADER_STATUSES.has(screenUploader.current.status)\n      ) {\n        screenUploader.current.resume();\n      }\n      if (\n        cameraUploader.current &&\n        RESUMABLE_UPLOADER_STATUSES.has(cameraUploader.current.status)\n      ) {\n        cameraUploader.current.resume();\n      }\n      void emitUploadTelemetry(\"upload_resumed\", {\n        reason: \"network-online\",\n        uploaderType: \"cloud_recorder\",\n      });\n      persistSessionState({\n        networkOnlineAt: Date.now(),\n      });\n    };\n\n    window.addEventListener(\"offline\", onOffline);\n    window.addEventListener(\"online\", onOnline);\n    return () => {\n      window.removeEventListener(\"offline\", onOffline);\n      window.removeEventListener(\"online\", onOnline);\n    };\n  }, []);\n\n  useEffect(() => {\n    tryRecoverPreviousSession();\n  }, [tryRecoverPreviousSession]);\n\n  useEffect(() => {\n    void ensureTelemetryRuntimeContext();\n  }, []);\n\n  useEffect(() => {\n    recoverPendingScenes();\n  }, [recoverPendingScenes]);\n\n  useEffect(() => {\n    return () => {\n      stopAllIntervals();\n      stopTabKeepAlive(); // Clean up keep-alive on unmount\n    };\n  }, []);\n\n  useEffect(() => {\n    return () => {\n      stopAllIntervals();\n    };\n  }, []);\n\n  const sendEditorReady = ({\n    projectId,\n    sceneId,\n    recordingToScene,\n    multiMode,\n  }) => {\n    const localPlayback = localScreenPlaybackOfferRef.current;\n    const localPlaybackAvailable =\n      Boolean(localPlayback?.offerId) &&\n      localPlayback?.trackType === \"screen\" &&\n      Number(localPlayback?.expiresAt || 0) > Date.now();\n\n    const shouldNotifyEditor = !multiMode || recordingToScene;\n    if (!shouldNotifyEditor || !projectId || !sceneId) {\n      console.warn(\"[Screenity][CloudRecorder] Skipping editor-ready\", {\n        shouldNotifyEditor,\n        hasProjectId: Boolean(projectId),\n        hasSceneId: Boolean(sceneId),\n        recordingToScene: Boolean(recordingToScene),\n        multiMode: Boolean(multiMode),\n      });\n      return;\n    }\n\n    console.info(\"[Screenity][CloudRecorder] Sending editor-ready\", {\n      projectId,\n      sceneId,\n      recordingToScene: Boolean(recordingToScene),\n      multiMode: Boolean(multiMode),\n      instantMode: Boolean(instantMode.current),\n      localPlaybackAvailable,\n      localPlaybackOfferId: localPlayback?.offerId || null,\n    });\n    chrome.runtime.sendMessage({\n      type: \"editor-ready\",\n      publicUrl: !recordingToScene\n        ? `${process.env.SCREENITY_APP_BASE}/view/${projectId}/`\n        : undefined,\n      newProject: !recordingToScene && !multiMode,\n      multiMode: Boolean(multiMode),\n      instantMode: instantMode.current,\n      sceneId,\n      projectId,\n      editorUrl:\n        !recordingToScene && !multiMode\n          ? instantMode.current\n            ? `${process.env.SCREENITY_APP_BASE}/view/${projectId}?load=true`\n            : `${process.env.SCREENITY_APP_BASE}/editor/${projectId}/edit?load=true`\n          : undefined,\n      localPlayback: localPlaybackAvailable\n        ? {\n            available: true,\n            offerId: localPlayback.offerId,\n            trackType: \"screen\",\n            chunkCount: localPlayback.chunkCount || 0,\n            estimatedBytes: localPlayback.estimatedBytes || 0,\n            expiresAt: localPlayback.expiresAt || null,\n            mediaId: localPlayback.mediaId || null,\n            bunnyVideoId: localPlayback.bunnyVideoId || null,\n            source: localPlayback.source || \"indexeddb-screen-chunks\",\n          }\n        : {\n            available: false,\n            trackType: \"screen\",\n          },\n    });\n  };\n\n  const createSceneOrHandleMultiMode = async (\n    uploadMeta,\n    durations,\n    isSilent,\n  ) => {\n    const {\n      projectId,\n      clickEvents = [],\n      surface,\n      multiMode,\n      multiSceneCount = 0,\n      multiLastSceneId = null,\n      activeSceneId = null,\n      recordingToScene = false,\n      recordedTabDomain = null,\n      recordingType = null,\n    } = await chrome.storage.local.get([\n      \"projectId\",\n      \"clickEvents\",\n      \"surface\",\n      \"multiMode\",\n      \"multiSceneCount\",\n      \"multiLastSceneId\",\n      \"activeSceneId\",\n      \"recordingToScene\",\n      \"recordedTabDomain\",\n      \"recordingType\",\n    ]);\n\n    // Check if cameraFlipped is set in storage, then pass it in payload\n    const { cameraFlipped } = await chrome.storage.local.get([\"cameraFlipped\"]);\n\n    let insertAfterSceneId = null;\n    if (multiMode) {\n      insertAfterSceneId = multiLastSceneId || activeSceneId;\n    } else {\n      insertAfterSceneId = activeSceneId;\n    }\n\n    // Validate uploadMeta has required data\n    if (!uploadMeta.sceneId) {\n      throw new Error(\"Missing sceneId in uploadMeta\");\n    }\n\n    if (!uploadMeta.screen?.mediaId && !uploadMeta.camera?.mediaId) {\n      throw new Error(\n        \"No valid media uploaded - both screen and camera mediaId are missing\",\n      );\n    }\n\n    const sceneId = uploadMeta.sceneId;\n    const existingStatus = await getSceneCreateStatus(sceneId);\n    let sceneOutcome = null;\n    let shouldIncrementMultiSceneCount = false;\n\n    if (existingStatus?.status === \"created\") {\n      logDebugEvent(\"scene-create-skip\", {\n        projectId,\n        sceneId,\n      });\n      await ensureMediaLinked({\n        projectId,\n        sceneId,\n        mediaIds: [\n          uploadMeta.screen?.mediaId,\n          uploadMeta.camera?.mediaId,\n          uploadMeta.audio?.mediaId,\n        ],\n      });\n      await removePendingScene(sceneId);\n      await markSceneComplete(sceneId);\n      await setPipelineState(\"scene-reused\", {\n        projectId,\n        sceneId,\n      });\n      sceneOutcome = \"reused\";\n    } else {\n      await setSceneCreateStatus(sceneId, \"creating\", {\n        projectId,\n      });\n      await setPipelineState(\"scene-creating\", {\n        projectId,\n        sceneId,\n      });\n      logDebugEvent(\"scene-create-start\", {\n        projectId,\n        sceneId,\n      });\n\n      const payload = {\n        sceneId,\n        screenMediaId: uploadMeta.screen?.mediaId || null,\n        cameraMediaId: uploadMeta.camera?.mediaId || null,\n        screenVideoId: uploadMeta.screen?.videoId || null,\n        cameraVideoId: uploadMeta.camera?.videoId || null,\n        audioMediaId: uploadMeta.audio?.mediaId || null,\n        recordingSessionId: recorderSession.current?.id || null,\n        durations,\n        captionSource: uploadMeta.screen ? \"screen\" : \"camera\",\n        transcriptionSourceMediaId: !isSilent\n          ? uploadMeta.audio?.mediaId || null\n          : null,\n        thumbnail: uploadMeta.screen?.thumbnail || null,\n        dimensions: {\n          screen: {\n            width: uploadMeta.screen?.width || 1920,\n            height: uploadMeta.screen?.height || 1080,\n          },\n          camera: uploadMeta.camera\n            ? {\n                width: uploadMeta.camera?.width || 1920,\n                height: uploadMeta.camera?.height || 1080,\n                flip: cameraFlipped,\n              }\n            : null,\n        },\n        clickEvents,\n        surface,\n        instantMode: instantMode.current,\n        newProject: !recordingToScene && (!multiMode || multiSceneCount === 0),\n        insertAfterSceneId,\n        isTab: isTab.current && !regionRef.current,\n        domain: recordedTabDomain || null,\n      };\n\n      const res = await fetch(`${API_BASE}/videos/${projectId}/scenes`, {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application/json\" },\n        credentials: \"include\",\n        body: JSON.stringify(payload),\n      });\n\n      if (!res.ok) {\n        const errorText = await res.text();\n        const recoverResult = await recoverScene({\n          projectId,\n          sceneId,\n          screenMediaId: uploadMeta.screen?.mediaId || null,\n          cameraMediaId: uploadMeta.camera?.mediaId || null,\n          audioMediaId: uploadMeta.audio?.mediaId || null,\n        });\n\n        if (recoverResult.ok) {\n          await setSceneCreateStatus(sceneId, \"created\", {\n            recovered: true,\n          });\n          await ensureMediaLinked({\n            projectId,\n            sceneId,\n            mediaIds: [\n              uploadMeta.screen?.mediaId,\n              uploadMeta.camera?.mediaId,\n              uploadMeta.audio?.mediaId,\n            ],\n          });\n          await removePendingScene(sceneId);\n          await markSceneComplete(sceneId);\n          await setPipelineState(\"scene-recovered\", {\n            projectId,\n            sceneId,\n          });\n          logDebugEvent(\"scene-recovered\", {\n            projectId,\n            sceneId,\n          });\n          sceneOutcome = \"recovered\";\n          shouldIncrementMultiSceneCount = true;\n        } else {\n          logDebugEvent(\"scene-create-failed\", {\n            projectId,\n            sceneId,\n            error: errorText,\n          });\n          await setSceneCreateStatus(sceneId, \"failed\", {\n            error: errorText,\n          });\n          throw new Error(`Failed to create scene: ${errorText}`);\n        }\n      } else {\n        await setSceneCreateStatus(sceneId, \"created\");\n        await ensureMediaLinked({\n          projectId,\n          sceneId,\n          mediaIds: [\n            uploadMeta.screen?.mediaId,\n            uploadMeta.camera?.mediaId,\n            uploadMeta.audio?.mediaId,\n          ],\n        });\n        await removePendingScene(sceneId);\n        await markSceneComplete(sceneId);\n        await setPipelineState(\"scene-created\", {\n          projectId,\n          sceneId,\n        });\n        logDebugEvent(\"scene-create-complete\", {\n          projectId,\n          sceneId,\n        });\n        sceneOutcome = \"created\";\n        shouldIncrementMultiSceneCount = true;\n      }\n    }\n\n    if (multiMode && shouldIncrementMultiSceneCount) {\n      await chrome.storage.local.set({\n        multiSceneCount: multiSceneCount + 1,\n        multiLastSceneId: sceneId,\n      });\n      chrome.runtime.sendMessage({\n        type: \"reopen-popup-multi\",\n      });\n    }\n\n    sendEditorReady({\n      projectId,\n      sceneId,\n      recordingToScene,\n      multiMode,\n    });\n\n    await chrome.storage.local.remove(\"clickEvents\");\n    return { [sceneOutcome || \"created\"]: true };\n  };\n\n  // Check if audio is silent to skip transcription\n  const isAudioSilent = async (audioBlob, silenceThreshold = 0.01) => {\n    const arrayBuffer = await audioBlob.arrayBuffer();\n    const audioCtx = new OfflineAudioContext(1, 44100 * 40, 44100); // up to 40s\n    const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);\n\n    const channelData = audioBuffer.getChannelData(0);\n    let total = 0;\n\n    for (let i = 0; i < channelData.length; i++) {\n      total += Math.abs(channelData[i]);\n    }\n\n    const avg = total / channelData.length;\n    return avg < silenceThreshold; // true if basically silent\n  };\n\n  const stopRecording = async (shouldFinalize = true, reason = \"unknown\") => {\n    if (isFinishing.current || sentLast.current) return;\n    // Lock immediately to prevent duplicate stop/finalize races under unload pressure.\n    isFinishing.current = true;\n    clearPendingStart();\n\n    if (DEBUG_START_FLOW) {\n      console.debug(\"[Screenity] stopRecording invoked\", {\n        reason,\n        shouldFinalize,\n        screenState: screenRecorder.current?.state,\n        cameraState: cameraRecorder.current?.state,\n        audioState: audioRecorder.current?.state,\n        screenOffset: screenUploader.current?.offset || 0,\n        cameraOffset: cameraUploader.current?.offset || 0,\n        chunkCount:\n          (sessionTrackState.current.screen?.chunkCount || 0) +\n          (sessionTrackState.current.camera?.chunkCount || 0),\n      });\n    }\n    logStartFlow(\"recording_stop\", { reason, shouldFinalize });\n    chrome.storage.local.set({\n      lastStopRecordingEvent: {\n        reason,\n        screenOffset: screenUploader.current?.offset || 0,\n        cameraOffset: cameraUploader.current?.offset || 0,\n        chunkCount:\n          (sessionTrackState.current.screen?.chunkCount || 0) +\n          (sessionTrackState.current.camera?.chunkCount || 0),\n        ts: Date.now(),\n      },\n    });\n    const stage = reason || (shouldFinalize ? \"finalize\" : \"stop\");\n\n    if (keepAliveInterval.current) {\n      clearInterval(keepAliveInterval.current);\n      keepAliveInterval.current = null;\n    }\n\n    // Stop the silent audio keep-alive since we're done recording\n    stopTabKeepAlive();\n\n    const { projectId, recordingToScene, multiMode } =\n      await chrome.storage.local.get([\n        \"projectId\",\n        \"recordingToScene\",\n        \"multiMode\",\n      ]);\n\n    await persistSessionState({ status: \"stopping\" });\n\n    if (shouldFinalize && reason !== \"retry-finalize\") {\n      // Skip on retry-finalize: editor tab is already open from the first attempt.\n      // Re-sending would open a duplicate tab or reload the editor mid-session.\n      if (recordingToScene) {\n        chrome.runtime.sendMessage({\n          type: \"prepare-editor-existing\",\n          multiMode: multiMode,\n        });\n      } else if (!multiMode && !recordingToScene) {\n        chrome.runtime.sendMessage({\n          type: \"prepare-open-editor\",\n          projectId,\n          url: instantMode.current\n            ? `${process.env.SCREENITY_APP_BASE}/view/${projectId}?load=true`\n            : `${process.env.SCREENITY_APP_BASE}/editor/${projectId}/edit?load=true`,\n          publicUrl: `${process.env.SCREENITY_APP_BASE}/view/${projectId}/`,\n          instantMode: instantMode.current,\n        });\n      }\n    }\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    stopAllIntervals();\n\n    await stopAllRecorders();\n\n    const { sceneId } = await chrome.storage.local.get([\"sceneId\"]);\n    const uploadMeta = {\n      screen: screenUploader.current?.getMeta() || null,\n      camera: cameraUploader.current?.getMeta() || null,\n      audio: null,\n      // use sceneId as per the metadata in screenUploader or cameraUploader, whichever exists\n      sceneId:\n        screenUploader.current?.getMeta()?.sceneId ||\n        cameraUploader.current?.getMeta()?.sceneId ||\n        sceneId,\n    };\n    uploadMetaRef.current = uploadMeta;\n    cleanupTimers();\n\n    if (!shouldFinalize) {\n      console.info(\"[CloudRecorder] stopRecording: skipping uploader finalize\", {\n        reason,\n        shouldFinalize,\n        screenStatus: uploadMeta.screen?.status || \"none\",\n        cameraStatus: uploadMeta.camera?.status || \"none\",\n      });\n      void emitUploadTelemetry(\"upload_cancelled\", {\n        reason: reason || \"stop-without-finalize\",\n        uploaderType: \"cloud_recorder\",\n      });\n      try {\n        await chrome.storage.local.set({ sceneIdStatus: \"cancelled\" });\n      } catch {\n        // ignore\n      }\n      await finalizeRecorderSession(\"cancelled\");\n      await chunksStore.clear().catch((e) =>\n        console.warn(\"[CloudRecorder] chunksStore.clear failed (cancelled-stop):\", e),\n      );\n      await clearAudioChunkStore(\"cancelled-stop\");\n      await clearCameraChunkStore(\"cancelled-stop\");\n      await clearLocalScreenPlaybackOffer(\"cancelled-stop\");\n      return;\n    }\n\n    const durations = calculateDurations();\n\n    await flushPendingChunks();\n\n    let finalizeError = null;\n    const finalizeCalls = [\n      screenUploader.current?.finalize?.(),\n      cameraUploader.current?.finalize?.(),\n    ];\n\n    if (\n      shouldSimulateFinalizeFailure() &&\n      !simulateFinalizeFailureConsumedRef.current\n    ) {\n      simulateFinalizeFailureConsumedRef.current = true;\n      finalizeCalls.push(\n        Promise.reject(new Error(\"simulated-finalize-failure\")),\n      );\n    }\n\n    const settledResults = await Promise.allSettled(finalizeCalls);\n    const rejectedResults = settledResults.filter(\n      (result) => result.status === \"rejected\",\n    );\n\n    const screenMeta = screenUploader.current?.getMeta?.() || null;\n    const cameraMeta = cameraUploader.current?.getMeta?.() || null;\n    const incompleteUploaders = [];\n\n    if (screenUploader.current && screenMeta?.status !== \"completed\") {\n      incompleteUploaders.push({\n        uploader: \"screen\",\n        status: screenMeta?.status || \"unknown\",\n        offset: screenMeta?.offset || 0,\n        error: screenMeta?.error || null,\n      });\n    }\n\n    if (cameraUploader.current && cameraMeta?.status !== \"completed\") {\n      incompleteUploaders.push({\n        uploader: \"camera\",\n        status: cameraMeta?.status || \"unknown\",\n        offset: cameraMeta?.offset || 0,\n        error: cameraMeta?.error || null,\n      });\n    }\n\n    if (rejectedResults.length > 0 || incompleteUploaders.length > 0) {\n      const diagnostics = buildFinalizeDiagnostics({\n        stage,\n        settledResults,\n        rejectedResults,\n        incompleteUploaders,\n        reason: \"uploader-finalize-incomplete\",\n      });\n      await markFinalizeFailure(diagnostics);\n      logDebugEvent(\"finalize-failed\", {\n        stage,\n        diagnostics,\n      });\n      finalizeError = new Error(\"uploader-finalize-incomplete\");\n    } else {\n      if (DEBUG_START_FLOW) {\n        console.info(\"[CloudRecorder][Finalize] Finalization complete\", {\n          stage,\n          settledResults: settledResults.map((result, i) => ({\n            index: i,\n            status: result.status,\n          })),\n          screen: screenMeta,\n          camera: cameraMeta,\n        });\n      }\n      logDebugEvent(\"finalize-complete\", {\n        stage,\n        screen: screenMeta,\n        camera: cameraMeta,\n      });\n    }\n\n    if (finalizeError) {\n      void emitUploadTelemetry(\"upload_error\", {\n        reason: \"uploader-finalize-incomplete\",\n        uploaderType: \"cloud_recorder\",\n      });\n      await exportLocalRecovery(\"finalize-error\");\n      await chunksStore.clear().catch((e) =>\n        console.warn(\"[CloudRecorder] chunksStore.clear failed (finalize-error):\", e),\n      );\n      await finalizeRecorderSession(\"failed\");\n      await clearAudioChunkStore(\"finalize-error\");\n      await clearCameraChunkStore(\"finalize-error\");\n      await clearLocalScreenPlaybackOffer(\"finalize-error\");\n      sendRecordingError(\n        \"Upload failed to finalize. A recovery copy was downloaded.\",\n      );\n      return;\n    }\n\n    // Validate that at least one media source was successfully uploaded\n    const hasAnyScreenData = (uploadMeta.screen?.offset || 0) > 0;\n    const hasAnyCameraData = (uploadMeta.camera?.offset || 0) > 0;\n    const hasValidScreen =\n      hasAnyScreenData &&\n      uploadMeta.screen?.mediaId &&\n      uploadMeta.screen?.videoId;\n    const hasValidCamera =\n      hasAnyCameraData &&\n      uploadMeta.camera?.mediaId &&\n      uploadMeta.camera?.videoId;\n\n    // Warn if upload size seems suspiciously small for recording duration\n    const screenDuration = durations.screen || 0;\n    const cameraDuration = durations.camera || 0;\n    const fallbackSeconds = durations.fallbackMs\n      ? durations.fallbackMs / 1000\n      : 0;\n    const effectiveScreenDuration =\n      screenDuration < MIN_DURATION_SECONDS && fallbackSeconds > 0\n        ? fallbackSeconds\n        : screenDuration;\n    const effectiveCameraDuration =\n      cameraDuration < MIN_DURATION_SECONDS && fallbackSeconds > 0\n        ? fallbackSeconds\n        : cameraDuration;\n\n    const usedDurations = {\n      screen: effectiveScreenDuration || screenDuration,\n      camera: effectiveCameraDuration || cameraDuration,\n    };\n    if (hasValidScreen && effectiveScreenDuration > 5) {\n      const screenOffset = uploadMeta.screen?.offset || 0;\n      const minExpectedSize = effectiveScreenDuration * 50000; // ~50KB/sec minimum\n      if (screenOffset < minExpectedSize) {\n        console.warn(\n          `⚠️ Screen upload size (${screenOffset} bytes) seems small for ${effectiveScreenDuration}s recording. Expected at least ${minExpectedSize} bytes.`,\n        );\n      }\n    }\n\n    if (!hasValidScreen && !hasValidCamera) {\n      const screenStatus = uploadMeta.screen?.status || \"none\";\n      const cameraStatus = uploadMeta.camera?.status || \"none\";\n      const screenOffset = uploadMeta.screen?.offset || 0;\n      const cameraOffset = uploadMeta.camera?.offset || 0;\n      const screenError = uploadMeta.screen?.error || \"none\";\n      const cameraError = uploadMeta.camera?.error || \"none\";\n      // If we see bytes on Bunny (offset > 0) but status is not completed, treat as success with a warning.\n      if (hasAnyScreenData || hasAnyCameraData) {\n        console.warn(\n          \"Uploads have data but were not marked completed. Proceeding with scene creation.\",\n          { screenStatus, cameraStatus, screenOffset, cameraOffset },\n        );\n      } else {\n        await cleanupIfEmptyUploads(\"no-upload\");\n        await exportLocalRecovery(\"no-upload\");\n        await chunksStore.clear().catch((e) =>\n          console.warn(\"[CloudRecorder] chunksStore.clear failed (no-upload):\", e),\n        );\n        await finalizeRecorderSession(\"failed\");\n        await clearAudioChunkStore(\"no-upload\");\n        await clearCameraChunkStore(\"no-upload\");\n        await clearLocalScreenPlaybackOffer(\"no-upload\");\n        sendRecordingError(\n          `No media was successfully uploaded. Screen: ${screenStatus} (${screenOffset} bytes, error: ${screenError}), Camera: ${cameraStatus} (${cameraOffset} bytes, error: ${cameraError})`,\n        );\n        return;\n      }\n    }\n\n    // Create final audio blob from durable store when available.\n    const audioBlob = await buildAudioBlobFromDurableStore();\n    const silent = audioBlob ? await isAudioSilent(audioBlob) : true;\n\n    await upsertPendingScene(uploadMeta.sceneId, {\n      projectId,\n      screenMediaId: uploadMeta.screen?.mediaId || null,\n      cameraMediaId: uploadMeta.camera?.mediaId || null,\n      audioMediaId: null,\n      status: \"ready\",\n    });\n    await setPipelineState(\"scene-ready\", {\n      projectId,\n      sceneId: uploadMeta.sceneId,\n    });\n    logDebugEvent(\"scene-ready\", {\n      projectId,\n      sceneId: uploadMeta.sceneId,\n    });\n\n    localScreenPlaybackOfferRef.current = await registerLocalScreenPlaybackOffer(\n      {\n        projectId,\n        sceneId: uploadMeta.sceneId,\n        uploadMeta,\n      },\n    );\n\n    chrome.storage.local.set({ uploadMeta });\n\n    isInit.current = false;\n\n    if (!sentLast.current) {\n      sentLast.current = true;\n      try {\n        await createSceneOrHandleMultiMode(uploadMeta, usedDurations, silent);\n\n        const result = await handleAudioUpload(\n          audioBlob,\n          projectId,\n          uploadMeta,\n        );\n        uploadMeta.audio = result;\n        chrome.storage.local.set({ uploadMeta });\n\n        if (result?.mediaId) {\n          await ensureMediaLinked({\n            projectId,\n            sceneId: uploadMeta.sceneId,\n            mediaIds: [uploadMeta.audio?.mediaId],\n          });\n        }\n\n        if (!silent && audioBlob) {\n          await handleTranscription(uploadMeta, projectId);\n        }\n\n        chrome.runtime.sendMessage({ type: \"video-ready\", uploadMeta });\n        if (localScreenPlaybackOfferRef.current?.offerId) {\n          console.info(\n            \"[CloudRecorder] Retaining local screen chunks for editor local-first playback\",\n            {\n              offerId: localScreenPlaybackOfferRef.current.offerId,\n              projectId,\n              sceneId: uploadMeta.sceneId,\n              chunkCount: localScreenPlaybackOfferRef.current.chunkCount || 0,\n            },\n          );\n        } else {\n          await chunksStore.clear().catch(() => {});\n        }\n        await clearAudioChunkStore(\"success\");\n        await clearCameraChunkStore(\"success\");\n        await setPipelineState(\"completed\", {\n          projectId,\n          sceneId: uploadMeta.sceneId,\n        });\n        await finalizeRecorderSession(\"completed\");\n        void emitUploadTelemetry(\"upload_complete_client\", {\n          projectId,\n          sceneId: uploadMeta.sceneId,\n          uploaderType: \"cloud_recorder\",\n          mediaId: uploadMeta.screen?.mediaId || uploadMeta.camera?.mediaId || null,\n        });\n\n        if (!IS_IFRAME_CONTEXT) {\n          try {\n            window.close();\n          } catch {}\n        } else {\n          window.location.reload();\n        }\n      } catch (err) {\n        console.error(\"❌ Failed to create scene:\", err);\n\n        // Provide user-friendly error message based on the error type\n        let userMessage = \"Failed to save recording: \";\n        if (\n          err.message.includes(\"No data uploaded\") ||\n          err.message.includes(\"offset is 0\")\n        ) {\n          userMessage +=\n            \"No video data was uploaded. This may be due to network issues. Please check your connection and try again.\";\n        } else if (err.message.includes(\"No media was successfully uploaded\")) {\n          userMessage +=\n            \"Upload did not complete successfully. Please check your internet connection and try recording again.\";\n        } else {\n          userMessage += err.message;\n        }\n\n        void emitUploadTelemetry(\"upload_error\", {\n          reason: \"scene-create-failed\",\n          message: err?.message || String(err),\n          uploaderType: \"cloud_recorder\",\n          projectId,\n          sceneId: uploadMeta.sceneId,\n        });\n        sendRecordingError(userMessage);\n\n        chrome.storage.local.set({\n          failedRecording: {\n            uploadMeta,\n            durations,\n            timestamp: Date.now(),\n            error: err.message,\n          },\n        });\n        await setPipelineState(\"failed\", {\n          projectId,\n          sceneId: uploadMeta.sceneId,\n          status: err.message,\n        });\n\n        // Still close the window even on error to prevent stuck tabs\n        // Give user time to see the error message\n        setTimeout(() => {\n          if (!IS_IFRAME_CONTEXT) {\n            try {\n              window.close();\n            } catch {}\n          } else {\n            window.location.reload();\n          }\n        }, 3000);\n        await exportLocalRecovery(\"scene-error\");\n        await chunksStore.clear().catch((e) =>\n          console.warn(\"[CloudRecorder] chunksStore.clear failed (scene-error):\", e),\n        );\n        try {\n          await chrome.storage.local.set({ sceneIdStatus: \"failed\" });\n        } catch {\n          // ignore\n        }\n        await finalizeRecorderSession(\"failed\");\n        await clearAudioChunkStore(\"scene-error\");\n        await clearCameraChunkStore(\"scene-error\");\n        await clearLocalScreenPlaybackOffer(\"scene-error\");\n      }\n    }\n  };\n\n  const startAudioStream = async (id) => {\n    const useExact = id && id !== \"none\";\n    const audioStreamOptions = {\n      audio: useExact ? { deviceId: { exact: id } } : true,\n    };\n\n    try {\n      const { defaultAudioInputLabel, audioinput } =\n        await chrome.storage.local.get([\n          \"defaultAudioInputLabel\",\n          \"audioinput\",\n        ]);\n      const desiredLabel =\n        defaultAudioInputLabel ||\n        audioinput?.find((device) => device.deviceId === id)?.label ||\n        \"\";\n\n      return await getUserMediaWithFallback({\n        constraints: audioStreamOptions,\n        fallbacks:\n          useExact && desiredLabel\n            ? [\n                {\n                  kind: \"audioinput\",\n                  desiredDeviceId: id,\n                  desiredLabel,\n                  onResolved: (resolvedId) => {\n                    chrome.storage.local.set({\n                      defaultAudioInput: resolvedId,\n                      defaultAudioInputLabel: desiredLabel,\n                    });\n                  },\n                },\n              ]\n            : [],\n      });\n    } catch (err) {\n      console.warn(\n        \"⚠️ Failed to access audio with deviceId, trying fallback:\",\n        err,\n      );\n      try {\n        return await navigator.mediaDevices.getUserMedia({ audio: true });\n      } catch (err2) {\n        console.warn(\n          \"⚠️ Microphone blocked/unavailable; continuing without mic:\",\n          err2,\n        );\n\n        // Optional: small non-fatal UI signal\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message:\n            \"Microphone permission is blocked. Recording will be silent.\",\n        });\n\n        return null;\n      }\n    }\n  };\n\n  const getVideoStreamWithFallback = async (constraints, deviceId) => {\n    if (!deviceId || deviceId === \"none\") {\n      return navigator.mediaDevices.getUserMedia(constraints);\n    }\n    const { defaultVideoInputLabel, videoinput } =\n      await chrome.storage.local.get([\"defaultVideoInputLabel\", \"videoinput\"]);\n    const desiredLabel =\n      defaultVideoInputLabel ||\n      videoinput?.find((device) => device.deviceId === deviceId)?.label ||\n      \"\";\n\n    return getUserMediaWithFallback({\n      constraints,\n      fallbacks:\n        deviceId && desiredLabel\n          ? [\n              {\n                kind: \"videoinput\",\n                desiredDeviceId: deviceId,\n                desiredLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultVideoInput: resolvedId,\n                    defaultVideoInputLabel: desiredLabel,\n                  });\n                },\n              },\n            ]\n          : [],\n    });\n  };\n\n  const isUserCaptureCancel = (err) => {\n    const name = err?.name || \"\";\n    const message = (err?.message || \"\").toLowerCase();\n    return (\n      name === \"NotAllowedError\" ||\n      name === \"AbortError\" ||\n      message.includes(\"cancel\") ||\n      message.includes(\"permission denied\")\n    );\n  };\n\n  const startStream = async (data, id, permissions, permissions2) => {\n    // Defaulting quality for now\n    //const { qualityValue } = await chrome.storage.local.get([\"qualityValue\"]);\n    const { width = 1920, height = 1080 } = getResolutionForQuality() || {};\n\n    const { fpsValue } = await chrome.storage.local.get([\"fpsValue\"]);\n    const fps = parseInt(fpsValue) || 30;\n\n    const { instantMode: instant } = await chrome.storage.local.get([\n      \"instantMode\",\n    ]);\n    instantMode.current = instant || false;\n\n    const constraints = {\n      audio: false,\n      video: {\n        width: { ideal: width },\n        height: { ideal: height },\n        frameRate: { ideal: fps },\n      },\n    };\n\n    recordingType.current = data.recordingType || \"screen\";\n\n    try {\n      const { cameraActive } = await chrome.storage.local.get([\"cameraActive\"]);\n      const shouldUseCamera = cameraActive === true && !instantMode.current;\n\n      if (data.recordingType === \"camera\") {\n        try {\n          cameraStream.current = await navigator.mediaDevices.getUserMedia(\n            constraints,\n          );\n        } catch (err) {\n          console.warn(\"⚠️ Failed to access camera stream:\", err);\n          cameraStream.current = null;\n        }\n      } else if (data.recordingType === \"region\" && IS_IFRAME_CONTEXT) {\n        try {\n          const constraints = {\n            preferCurrentTab: true,\n            video: {\n              width: { max: 2560 }, // or use getResolutionForQuality()\n              height: { max: 1440 },\n              frameRate: { ideal: 30, max: 60 },\n            },\n            audio: data.systemAudio,\n          };\n          const stream = await navigator.mediaDevices.getDisplayMedia(\n            constraints,\n          );\n          screenStream.current = stream;\n          regionRef.current = true;\n          if (!screenStream.current?.getVideoTracks?.().length) {\n            sendRecordingError(\n              \"Failed to access region stream: no video track.\",\n            );\n            return;\n          }\n\n          if (isTab.current) {\n            const ctx = new AudioContext();\n            const src = ctx.createMediaStreamSource(screenStream.current);\n            src.connect(ctx.destination);\n          }\n\n          bindScreenTrack(screenStream.current.getVideoTracks()[0]);\n        } catch (err) {\n          if (isUserCaptureCancel(err)) {\n            sendRecordingError(\"User cancelled stream selection\", true);\n            return;\n          }\n          sendRecordingError(\"Failed to access region stream: \" + err.message);\n          return;\n        }\n\n        if (shouldUseCamera) {\n          const { defaultVideoInput } = await chrome.storage.local.get([\n            \"defaultVideoInput\",\n          ]);\n\n          const cameraConstraints = {\n            video: {\n              ...(defaultVideoInput && defaultVideoInput !== \"none\"\n                ? { deviceId: { exact: defaultVideoInput } }\n                : {}),\n              width: { ideal: width },\n              height: { ideal: height },\n              frameRate: { ideal: fps },\n            },\n            audio: false,\n          };\n\n          try {\n            cameraStream.current = await getVideoStreamWithFallback(\n              cameraConstraints,\n              defaultVideoInput,\n            );\n          } catch (err) {\n            console.warn(\n              \"⚠️ Camera permission denied — continuing without camera:\",\n              err,\n            );\n            cameraStream.current = null;\n\n            // keep UI consistent (optional)\n            await chrome.storage.local.set({ cameraActive: false });\n\n            // only fatal if the user is doing camera-only recording\n            if (data.recordingType === \"camera\") {\n              sendRecordingError(\n                \"Camera permission is blocked. Please allow camera access to record.\",\n              );\n              return;\n            }\n          }\n        }\n\n        try {\n          if (target.current) {\n            const track = screenStream.current.getVideoTracks()[0];\n            await track.cropTo(target.current);\n          } else {\n            sendRecordingError(\n              \"No crop target set for region capture. Please select a region.\",\n            );\n            return;\n          }\n        } catch (err) {\n          sendRecordingError(\"Failed to crop region stream: \" + err.message);\n          return;\n        }\n      } else {\n        const tabWidth = isTab.current ? window.innerWidth : width;\n        const tabHeight = isTab.current ? window.innerHeight : height;\n\n        const videoConstraints = isTab.current\n          ? {\n              // Tab recording - minimal constraints, let Chrome determine optimal capture\n              chromeMediaSource: \"tab\",\n              chromeMediaSourceId: id,\n              maxFrameRate: fps,\n              // No width/height constraints for tabs - Chrome will capture at the tab's natural size\n              maxWidth: tabWidth,\n              maxHeight: tabHeight,\n            }\n          : {\n              // Desktop/window recording - use quality settings as max bounds\n              chromeMediaSource: \"desktop\",\n              chromeMediaSourceId: id,\n              maxWidth: width,\n              maxHeight: height,\n              maxFrameRate: fps,\n            };\n\n        const desktopConstraints = {\n          audio: {\n            mandatory: {\n              chromeMediaSource: isTab.current ? \"tab\" : \"desktop\",\n              chromeMediaSourceId: id,\n            },\n          },\n          video: {\n            mandatory: { ...videoConstraints },\n          },\n        };\n\n        try {\n          screenStream.current = await navigator.mediaDevices.getUserMedia(\n            desktopConstraints,\n          );\n          if (!screenStream.current?.getVideoTracks?.().length) {\n            sendRecordingError(\n              \"Failed to access screen stream: no video track.\",\n            );\n            return;\n          }\n          const track = screenStream.current.getVideoTracks()[0];\n          const {\n            width,\n            height,\n            displaySurface: surface,\n          } = track.getSettings(); // Always use displaySurface from track\n\n          const settings = screenStream.current\n            .getVideoTracks()[0]\n            .getSettings();\n\n          traceStep(\"streamAcquired\", { surface: surface || null });\n\n          // Show \"Preparing...\" on the user's page while API calls run.\n          setTimeout(() => {\n            traceStep(\"preparingSent\");\n            chrome.runtime.sendMessage({ type: \"preparing-recording\" });\n          }, 200);\n\n          chrome.runtime.sendMessage(\n            { type: \"get-monitor-for-window\" },\n            (response) => {\n              if (!response || chrome.runtime.lastError || response.error) {\n                console.error(\n                  \"Failed to get monitor info:\",\n                  response?.error || chrome.runtime.lastError || \"No response\",\n                );\n                return;\n              }\n\n              const { monitorId, monitorBounds, displays } = response;\n\n              chrome.storage.local.set({\n                surface,\n                displays,\n                recordedMonitorId: monitorId,\n                monitorBounds,\n                recordedStreamDimensions: { width, height },\n              });\n            },\n          );\n\n          chrome.runtime.sendMessage({\n            type: \"set-surface\",\n            surface: surface,\n            subscribed: true,\n            instantMode: instantMode.current || false,\n          });\n\n          if (isTab.current) {\n            const ctx = new AudioContext();\n            const src = ctx.createMediaStreamSource(screenStream.current);\n            src.connect(ctx.destination);\n          }\n\n          bindScreenTrack(screenStream.current.getVideoTracks()[0]);\n        } catch (err) {\n          if (isUserCaptureCancel(err)) {\n            sendRecordingError(\"User cancelled stream selection\", true);\n            return;\n          }\n          sendRecordingError(\"Failed to access screen stream: \" + err.message);\n          return;\n        }\n\n        if (shouldUseCamera) {\n          const { defaultVideoInput } = await chrome.storage.local.get([\n            \"defaultVideoInput\",\n          ]);\n\n          const cameraConstraints = {\n            video: {\n              ...(defaultVideoInput && defaultVideoInput !== \"none\"\n                ? { deviceId: { exact: defaultVideoInput } }\n                : {}),\n              width: { ideal: width },\n              height: { ideal: height },\n              frameRate: { ideal: fps },\n            },\n            audio: false,\n          };\n\n          try {\n            cameraStream.current = await getVideoStreamWithFallback(\n              cameraConstraints,\n              defaultVideoInput,\n            );\n          } catch (err) {\n            console.warn(\n              \"⚠️ Camera permission denied — continuing without camera:\",\n              err,\n            );\n            cameraStream.current = null;\n\n            // keep UI consistent (optional)\n            await chrome.storage.local.set({ cameraActive: false });\n\n            // only fatal if the user is doing camera-only recording\n            if (data.recordingType === \"camera\") {\n              sendRecordingError(\n                \"Camera permission is blocked. Please allow camera access to record.\",\n              );\n              return;\n            }\n          }\n        }\n      }\n\n      setInitProject(true);\n\n      // Try to get microphone access\n      micStream.current = await startAudioStream(data.defaultAudioInput);\n      rawMicStream.current = micStream.current;\n\n      // Setup audio context and routing\n      aCtx.current = new AudioContext();\n      destination.current = aCtx.current.createMediaStreamDestination();\n\n      if (micStream.current?.getAudioTracks().length) {\n        audioInputGain.current = aCtx.current.createGain();\n        const micSource = aCtx.current.createMediaStreamSource(\n          micStream.current,\n        );\n        micSource.connect(audioInputGain.current).connect(destination.current);\n        micStream.current = destination.current.stream;\n\n        const { micActive } = await chrome.storage.local.get([\"micActive\"]);\n        if (micActive === false) {\n          audioInputGain.current.gain.value = 0;\n        }\n      }\n\n      if (screenStream.current?.getAudioTracks().length) {\n        audioOutputGain.current = aCtx.current.createGain();\n        const screenSource = aCtx.current.createMediaStreamSource(\n          screenStream.current,\n        );\n        screenSource\n          .connect(audioOutputGain.current)\n          .connect(destination.current);\n\n        // Set initial system audio volume based on preferences\n        const { systemAudioVolume } = await chrome.storage.local.get([\n          \"systemAudioVolume\",\n        ]);\n        if (systemAudioVolume !== undefined) {\n          audioOutputGain.current.gain.value = systemAudioVolume;\n        }\n      }\n\n      try {\n        // Try to get projectId from storage - this may have been set externally\n        const { projectId, multiMode, multiProjectId, multiSceneCount } =\n          await chrome.storage.local.get([\n            \"projectId\",\n            \"multiMode\",\n            \"multiProjectId\",\n            \"multiSceneCount\",\n          ]);\n\n        let videoId = projectId || multiProjectId;\n\n        if (videoId) {\n          if (multiMode) {\n            await chrome.storage.local.set({\n              multiSceneCount: multiSceneCount || 0,\n            });\n          }\n        } else {\n          const now = new Date();\n          const options = { day: \"2-digit\", month: \"short\", year: \"numeric\" };\n          const title = `Untitled video - ${now.toLocaleString(\n            \"en-GB\",\n            options,\n          )}`;\n\n          videoId = await createVideoProject({\n            title,\n            instantMode: instantMode.current,\n          });\n          if (!videoId) throw new Error(\"Failed to create video project\");\n\n          if (multiMode) {\n            await chrome.storage.local.set({\n              multiProjectId: videoId,\n              multiSceneCount: 0,\n            });\n          }\n        }\n\n        await chrome.storage.local.set({ projectId: videoId });\n        traceStep(\"apiProjectCreated\");\n        await setPipelineState(\"project-ready\", {\n          projectId: videoId,\n          status: multiMode ? \"multi\" : \"single\",\n        });\n        logDebugEvent(\"project-ready\", {\n          projectId: videoId,\n          multiMode: Boolean(multiMode),\n        });\n\n        uploadersInitialized.current = await initializeUploaders();\n        if (!uploadersInitialized.current) {\n          throw new Error(\"Failed to initialize uploaders\");\n        }\n        traceStep(\"apiUploadersReady\");\n\n        setStarted(true);\n        setInitProject(false);\n        traceStep(\"resetActiveTabSent\");\n        chrome.runtime.sendMessage({ type: \"reset-active-tab\" });\n        if (pendingStartRef.current) {\n          maybeStartRecording(\"uploaders-ready\");\n        }\n      } catch (err) {\n        sendRecordingError(\"Failed to initialize uploaders: \" + err.message);\n      }\n    } catch (err) {\n      setInitProject(false);\n      sendRecordingError(\"Failed to start stream: \" + err.message, true);\n    }\n  };\n\n  const startStreaming = async (data) => {\n    try {\n      const permissions = await navigator.permissions.query({ name: \"camera\" });\n      const permissions2 = await navigator.permissions.query({\n        name: \"microphone\",\n      });\n\n      if (isTab.current) {\n        // Wait for getStreamID to resolve regardless of recordingType — the\n        // previous guard (data.recordingType !== \"region\") caused startStream\n        // to be called with tabID.current = null for region tab-captures when\n        // getStreamID hadn't resolved yet.\n        let attempts = 0;\n        while (!tabID.current && attempts < 20) {\n          // wait for tab stream id to resolve\n          // eslint-disable-next-line no-await-in-loop\n          await new Promise((resolve) => setTimeout(resolve, 50));\n          attempts += 1;\n        }\n        if (!tabID.current) {\n          sendRecordingError(\"Failed to prepare tab capture.\");\n          return;\n        }\n      }\n\n      if (data.recordingType === \"camera\") {\n        startStream(data, null, permissions, permissions2);\n      } else if (!isTab.current && (data.recordingType != \"region\" || tabPreferred.current)) {\n        // Show the desktop picker when:\n        //   - not tab-capture mode AND not region recording, OR\n        //   - tabPreferred=true (playground) forced isTab=false even for a region\n        //     recording — in that case we still need the picker because there is\n        //     no pre-obtained stream ID to pass to startStream.\n        chrome.desktopCapture.chooseDesktopMedia(\n          [\"screen\", \"window\", \"tab\", \"audio\"],\n          null,\n          (streamId) => {\n            if (!streamId) {\n              sendRecordingError(\"User cancelled stream selection\", true);\n            } else {\n              startStream(data, streamId, permissions, permissions2);\n            }\n          },\n        );\n      } else {\n        startStream(data, tabID.current, permissions, permissions2);\n      }\n    } catch (err) {\n      sendRecordingError(\"Failed to setup streaming: \" + err.message);\n    }\n  };\n\n  const getStreamID = async (id) => {\n    try {\n      const streamId = await chrome.tabCapture.getMediaStreamId({\n        targetTabId: id,\n      });\n      tabID.current = streamId;\n    } catch (err) {\n      sendRecordingError(\"Failed to get stream ID: \" + err.message);\n    }\n  };\n\n  useEffect(() => {\n    if (!IS_IFRAME_CONTEXT) return;\n\n    // Notify parent that the region capture iframe has loaded\n    const sendReady = () => {\n      window.parent.postMessage(\n        { type: \"screenity-region-capture-loaded\" },\n        \"*\",\n      );\n    };\n\n    sendReady();\n  }, []);\n\n  useEffect(() => {\n    if (!IS_IFRAME_CONTEXT) return;\n\n    const onMessage = (event) => {\n      if (event.data.type === \"crop-target\") {\n        target.current = event.data.target;\n        regionRef.current = true;\n        regionWidth.current = event.data.width;\n        regionHeight.current = event.data.height;\n      } else if (event.data.type === \"restart-recording\") {\n        // Legacy path: restart is now orchestrated via runtime message.\n        void setCloudRestartPhase(\"restart-postmessage-ignored\");\n      }\n    };\n    window.addEventListener(\"message\", onMessage);\n\n    return () => {\n      window.removeEventListener(\"message\", onMessage);\n    };\n  }, []);\n\n  function getActiveVideoTime() {\n    const now = Date.now();\n    // Choose whichever timer is running\n    const timer = screenTimer.current.start\n      ? screenTimer\n      : cameraTimer.current.start\n      ? cameraTimer\n      : null;\n    if (!timer) return 0;\n\n    return timer.current.paused\n      ? timer.current.total\n      : timer.current.total + (now - timer.current.start);\n  }\n\n  const onMessage = useCallback((request, sender, sendResponse) => {\n    if (request.type === \"loaded\") {\n      setInitProject(false);\n      backupRef.current = request.backup;\n      if (IS_IFRAME_CONTEXT) {\n        // Only trigger if it's actually a region recording\n        if (request.region) {\n          isInit.current = true;\n          chrome.runtime.sendMessage({ type: \"get-streaming-data\" });\n        }\n      } else if (!request.region) {\n        // If the background included tabPreferred in the message, apply it\n        // synchronously before we use it — this eliminates a race where\n        // chrome.storage.local.get([\"tabPreferred\"]) hasn't completed yet when\n        // \"loaded\" arrives, causing isTab.current to be set incorrectly on the\n        // first attempt from playground.html.\n        if (typeof request.tabPreferred === \"boolean\") {\n          tabPreferred.current = request.tabPreferred;\n          console.info(\n            \"[CloudRecorder] tabPreferred from loaded message:\",\n            request.tabPreferred,\n          );\n        }\n        if (!tabPreferred.current) {\n          isTab.current = request.isTab;\n          recordingTabId.current = request.tabID || null;\n          if (request.isTab) getStreamID(request.tabID);\n        } else {\n          isTab.current = false;\n        }\n        isInit.current = true;\n        chrome.runtime.sendMessage({ type: \"get-streaming-data\" });\n      }\n    } else if (request.type === \"streaming-data\") {\n      if (!isInit.current) return;\n      if (IS_IFRAME_CONTEXT) {\n        if (regionRef.current) {\n          startStreaming(JSON.parse(request.data));\n        }\n      } else if (!regionRef.current) {\n        startStreaming(JSON.parse(request.data));\n      }\n    } else if (request.type === \"start-recording-tab\") {\n      if (!isInit.current) return;\n\n      chrome.storage.local.set({\n        lastStartRecordingTabMessage: {\n          ts: Date.now(),\n          region: Boolean(request.region),\n          isIframe: IS_IFRAME_CONTEXT,\n        },\n      });\n\n      if (IS_IFRAME_CONTEXT) {\n        if (request.region || target.current) {\n          if (target.current) {\n            regionRef.current = true;\n          }\n          pendingStartRef.current = true;\n          pendingStartAttempts.current = 0;\n          maybeStartRecording(\"start-message\");\n        }\n      } else if (!regionRef.current) {\n        pendingStartRef.current = true;\n        pendingStartAttempts.current = 0;\n        maybeStartRecording(\"start-message\");\n      }\n    } else if (request.type === \"restart-recording-tab\") {\n      if (!isInit.current) return;\n      Promise.resolve(restartRecording())\n        .then((restarted) => {\n          if (!restarted) {\n            sendResponse?.({\n              ok: false,\n              error: \"restart-teardown-failed\",\n            });\n            return;\n          }\n          sendResponse?.({ ok: true, restarted: true });\n        })\n        .catch((error) => {\n          sendResponse?.({\n            ok: false,\n            error: error?.message || String(error),\n          });\n        });\n      return true;\n    } else if (request.type === \"stop-recording-tab\") {\n      if (!isInit.current) return;\n      stopRecording(true, request.reason || \"message-stop\");\n      sendResponse?.({ ok: true });\n      return true;\n    } else if (request.type === \"set-mic-active-tab\") {\n      if (!isInit.current) return;\n      setMic(request);\n    } else if (request.type === \"set-audio-output-volume\") {\n      if (!isInit.current) return;\n      setAudioOutputVolume(request.volume);\n    } else if (request.type === \"get-video-time\") {\n      if (!isInit.current) return;\n      const videoTime = getActiveVideoTime() / 1000; // in seconds\n      sendResponse({ videoTime });\n      return true;\n    } else if (request.type === \"pause-recording-tab\") {\n      if (!isInit.current) return;\n      if (pausedStateRef.current) return;\n\n      // Pause all active recorders\n      if (\n        screenRecorder.current &&\n        screenRecorder.current.state === \"recording\"\n      ) {\n        screenRecorder.current.pause();\n      }\n      if (\n        cameraRecorder.current &&\n        cameraRecorder.current.state === \"recording\"\n      ) {\n        cameraRecorder.current.pause();\n      }\n      if (\n        audioRecorder.current &&\n        audioRecorder.current.state === \"recording\"\n      ) {\n        audioRecorder.current.pause();\n      }\n\n      const now = Date.now();\n      if (!screenTimer.current.paused && screenTimer.current.start) {\n        screenTimer.current.total += now - screenTimer.current.start;\n        screenTimer.current.paused = true;\n      }\n      if (!cameraTimer.current.paused && cameraTimer.current.start) {\n        cameraTimer.current.total += now - cameraTimer.current.start;\n        cameraTimer.current.paused = true;\n      }\n      pausedStateRef.current = true;\n      void setRecordingTimingState({\n        paused: true,\n        pausedAt: now,\n      });\n      void persistSessionState({\n        paused: true,\n        pausedAt: now,\n      });\n    } else if (request.type === \"resume-recording-tab\") {\n      if (!isInit.current) return;\n      if (!pausedStateRef.current) return;\n\n      // Resume all paused recorders\n      if (screenRecorder.current && screenRecorder.current.state === \"paused\") {\n        screenRecorder.current.resume();\n      }\n      if (cameraRecorder.current && cameraRecorder.current.state === \"paused\") {\n        cameraRecorder.current.resume();\n      }\n      if (audioRecorder.current && audioRecorder.current.state === \"paused\") {\n        audioRecorder.current.resume();\n      }\n\n      const now = Date.now();\n      if (screenTimer.current.paused) {\n        screenTimer.current.start = now;\n        screenTimer.current.paused = false;\n      }\n      if (cameraTimer.current.paused) {\n        cameraTimer.current.start = now;\n        cameraTimer.current.paused = false;\n      }\n      pausedStateRef.current = false;\n      void (async () => {\n        try {\n          const { pausedAt, totalPausedMs } = await chrome.storage.local.get([\n            \"pausedAt\",\n            \"totalPausedMs\",\n          ]);\n          const additional = pausedAt ? Math.max(0, now - pausedAt) : 0;\n          await setRecordingTimingState({\n            paused: false,\n            pausedAt: null,\n            totalPausedMs: (totalPausedMs || 0) + additional,\n          });\n          await persistSessionState({\n            paused: false,\n            resumedAt: now,\n          });\n        } catch (err) {\n          console.warn(\"Failed to update resume timing state:\", err);\n        }\n      })();\n    } else if (request.type === \"dismiss-recording\") {\n      if (!isInit.current) return;\n      dismissRecording();\n    }\n  }, []);\n\n  useEffect(() => {\n    chrome.storage.local.get([\"tabPreferred\"], (result) => {\n      tabPreferred.current = result.tabPreferred;\n    });\n\n    chrome.runtime.onMessage.addListener(onMessage);\n    return () => chrome.runtime.onMessage.removeListener(onMessage);\n  }, []);\n\n  return (\n    <RecorderUI\n      started={started}\n      isTab={isTab.current}\n      initProject={initProject}\n      finalizeFailure={finalizeFailure}\n      retryingFinalize={retryingFinalize}\n      onRetryFinalize={retryFinalize}\n      onExportDiagnostics={() =>\n        exportFinalizeDiagnostics(finalizeFailureRef.current)\n      }\n    />\n  );\n};\n\nexport default CloudRecorder;\n"
  },
  {
    "path": "src/pages/CloudRecorder/RecorderUI.jsx",
    "content": "import React from \"react\";\nimport Warning from \"./warning/Warning\";\n\nconst RecorderUI = ({\n  started,\n  initProject = false,\n  isTab,\n  finalizeFailure = null,\n  retryingFinalize = false,\n  onRetryFinalize = null,\n  onExportDiagnostics = null,\n  onExportDebugBundle = null,\n}) => {\n  const hasFinalizeFailure = Boolean(finalizeFailure);\n  return (\n    <div className=\"wrap\">\n      <img\n        className=\"logo\"\n        src={chrome.runtime.getURL(\"assets/logo-text.svg\")}\n        alt=\"Screenity logo\"\n      />\n      <div className=\"middle-area\">\n        <img\n          src={chrome.runtime.getURL(\"assets/record-tab-active.svg\")}\n          alt=\"Recording icon\"\n        />\n        <div className=\"title\">\n          {initProject\n            ? chrome.i18n.getMessage(\"recorderSetupTitle\")\n            : !started\n            ? chrome.i18n.getMessage(\"recorderSelectTitle\")\n            : chrome.i18n.getMessage(\"recorderSelectProgressTitle\")}\n        </div>\n        <div className=\"subtitle\">\n          {initProject\n            ? chrome.i18n.getMessage(\"recorderSetupDescription\")\n            : chrome.i18n.getMessage(\"recorderSelectDescription\")}\n        </div>\n        {hasFinalizeFailure && (\n          <>\n            <div className=\"button-strong\" onClick={onRetryFinalize}>\n              {retryingFinalize ? \"Retrying...\" : \"Retry finalize\"}\n            </div>\n            <div className=\"button-stop\" onClick={onExportDiagnostics}>\n              Export diagnostics\n            </div>\n          </>\n        )}\n        {/* Debug bundle generation button removed */}\n        {/* \n        Optionally: \n        <div className=\"button-stop\" onClick={() => chrome.runtime.sendMessage({ type: \"stop-recording-tab\" })}>\n          {chrome.i18n.getMessage(\"stopRecording\")}\n        </div> \n        */}\n      </div>\n\n      {!isTab && !started && <Warning />}\n\n      <div className=\"setupBackgroundSVG\"></div>\n\n      <style>\n        {`\n          body {\n            overflow: hidden;\n          }\n          .button-stop {\n            padding: 10px 20px;\n            background: #FFF;\n            border-radius: 30px;\n            color: #29292F;\n            font-size: 14px;\n            font-weight: 500;\n            cursor: pointer;\n            margin-top: 0px;\n            border: 1px solid #E8E8E8;\n            margin-left: auto;\n            margin-right: auto;\n            z-index: 999999;\n          }\n          .setupBackgroundSVG {\n            position: absolute;\n            top: 0px;\n            left: 0px;\n            width: 100%;\n            height:100%;\n            background: url('${chrome.runtime.getURL(\n              \"assets/helper/pattern-svg.svg\",\n            )}') repeat;\n            background-size: 62px 23.5px;\n            animation: moveBackground 138s linear infinite;\n            transform: rotate(0deg);\n          }\n          @keyframes moveBackground {\n            0% {\n              background-position: 0 0;\n            }\n            100% {\n              background-position: 100% 0;\n            }\n          }\n          .logo {\n            position: absolute;\n            bottom: 30px;\n            left: 0px;\n            right: 0px;\n            margin: auto;\n            width: 120px;\n          }\n          .wrap {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background-color: #F6F7FB;\n          }\n          .middle-area {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            height: 100%;\n            font-family: \"Satoshi Medium\", sans-serif;\n          }\n          .middle-area img {\n            width: 40px;\n            margin-bottom: 20px;\n          }\n          .title {\n            font-size: 24px;\n            font-weight: 700;\n            color: #1A1A1A;\n            margin-bottom: 14px;\n            font-family: Satoshi-Medium, sans-serif;\n            text-align: center;\n          }\n          .subtitle {\n            font-size: 14px;\n            font-weight: 400;\n            color: #6E7684;\n            margin-bottom: 24px;\n            font-family: Satoshi-Medium, sans-serif;\n            text-align: center;\n          }\n        `}\n      </style>\n    </div>\n  );\n};\n\nexport default RecorderUI;\n"
  },
  {
    "path": "src/pages/CloudRecorder/bunnyTusUploader.js",
    "content": "const API_BASE = process.env.SCREENITY_API_BASE_URL;\n\nexport async function getThumbnailFromBlob(blob, seekTo = 0.1) {\n  return new Promise((resolve, reject) => {\n    const video = document.createElement(\"video\");\n    video.preload = \"metadata\";\n    video.muted = true;\n    video.crossOrigin = \"anonymous\";\n\n    const url = URL.createObjectURL(blob);\n    video.src = url;\n\n    let timeoutId = setTimeout(() => {\n      URL.revokeObjectURL(url);\n      reject(new Error(\"Thumbnail timed out\"));\n    }, 2000);\n\n    const cleanup = () => {\n      clearTimeout(timeoutId);\n      URL.revokeObjectURL(url);\n    };\n\n    video.onerror = () => {\n      cleanup();\n      reject(new Error(\"Failed to load video for thumbnail\"));\n    };\n\n    video.onloadedmetadata = () => {\n      if (video.duration === Infinity) {\n        video.currentTime = 0;\n      }\n      const targetTime = Math.min(seekTo, video.duration - 0.01);\n      video.currentTime = targetTime;\n    };\n\n    video.onseeked = () => {\n      const canvas = document.createElement(\"canvas\");\n      canvas.width = video.videoWidth;\n      canvas.height = video.videoHeight;\n\n      const ctx = canvas.getContext(\"2d\");\n      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);\n\n      canvas.toBlob(\n        (thumbnailBlob) => {\n          cleanup();\n          if (thumbnailBlob) resolve(thumbnailBlob);\n          else reject(new Error(\"Failed to create thumbnail blob\"));\n        },\n        \"image/jpeg\",\n        0.8,\n      );\n    };\n  });\n}\n\nexport default class BunnyTusUploader {\n  constructor(options = {}) {\n    this.CHUNK_SIZE = options.chunkSize || 512 * 1024; // 512KB default\n    this.MAX_RETRIES = options.maxRetries || 5;\n    this.RETRY_DELAY = options.retryDelay || 1000;\n    this.UPLOAD_TIMEOUT_MS = options.uploadTimeoutMs || 20000;\n    this.HEARTBEAT_INTERVAL_MS = options.heartbeatIntervalMs || 10000;\n    this.HEARTBEAT_LAG_MS = options.heartbeatLagMs || 30000;\n    this.TOKEN_REFRESH_THRESHOLD = options.tokenRefreshThreshold || 300;\n    this.MAX_QUEUE_SIZE = options.maxQueueSize || 100;\n    this.onProgress = options.onProgress || null;\n    this.onStall = options.onStall || null;\n    this.onTelemetry = options.onTelemetry || null;\n    this.onStateChange = options.onStateChange || null;\n    this.trackType = options.trackType || null;\n\n    this.totalBytes = 0;\n    this.uploadUrl = null;\n    this.offset = 0;\n    this.projectId = null;\n    this.videoId = null;\n    this.mediaId = null;\n    this.signature = null;\n    this.expires = null;\n    this.libraryId = null;\n    this.status = \"idle\";\n    this.error = null;\n    this.isFinalizing = false;\n    this.isPaused = false;\n    this.metadata = {};\n    this.pendingUploads = [];\n    this.userToken = null;\n    this.heartbeatTimer = null;\n    this.lastProgressAt = Date.now();\n    this.stalled = false;\n\n    this.chunkQueue = [];\n    this.isProcessingQueue = false;\n    this.queueProcessingPromise = null;\n    this.queuedBytes = 0;\n    this._hasExtractedMeta = true; // Skip in-flight thumbnail extraction to avoid timeouts/noise\n    this.sessionId = options.sessionId || null;\n    this.journalKey = null;\n    this.journalLookupKey = null;\n    this.fingerprint = null;\n    this.metaWidth = null;\n    this.metaHeight = null;\n    this.lastJournalPersistAt = 0;\n    this.journalPersistTimer = null;\n    this.JOURNAL_WRITE_INTERVAL_MS = options.journalWriteIntervalMs || 4000;\n    this.PROGRESS_EVENT_INTERVAL_MS =\n      options.progressEventIntervalMs || 5000;\n    this.createdAt = null;\n    this.readyAt = null;\n    this.firstByteAt = null;\n    this.lastChunkQueuedAt = null;\n    this.lastErrorAt = null;\n    this.lastErrorCode = null;\n    this.finalizeStartedAt = null;\n    this.finalizedAt = null;\n    this.resumeCount = 0;\n    this.lastServerOffset = 0;\n    this.hasEmittedClientStarted = false;\n    this.hasEmittedFirstByte = false;\n    this.lastProgressEventAt = 0;\n    this.initializedFromResume = false;\n  }\n\n  debugLog(message, payload = null) {\n    if (!this.debug) return;\n    if (payload) {\n      console.info(`[BunnyTusUploader] ${message}`, payload);\n      return;\n    }\n    console.info(`[BunnyTusUploader] ${message}`);\n  }\n\n  getUploaderType() {\n    return this.trackType || this.metadata?.type || null;\n  }\n\n  emitTelemetry(event, payload = {}) {\n    if (typeof this.onTelemetry !== \"function\") return;\n    try {\n      this.onTelemetry(event, {\n        projectId: this.projectId || null,\n        sceneId: this.sceneId || null,\n        recordingSessionId: this.sessionId || null,\n        mediaId: this.mediaId || null,\n        bunnyVideoId: this.videoId || null,\n        trackType: this.getUploaderType(),\n        uploaderType: \"bunny_tus\",\n        status: this.status,\n        offset: this.offset || 0,\n        totalBytes: this.totalBytes || 0,\n        queuedBytes: this.queuedBytes || 0,\n        ...payload,\n      });\n    } catch (err) {\n      console.warn(\"Upload telemetry callback failed:\", err);\n    }\n  }\n\n  notifyStateChange(reason = null, extra = {}) {\n    if (typeof this.onStateChange !== \"function\") return;\n    try {\n      this.onStateChange({\n        reason: reason || null,\n        ...this.getResumeState(),\n        ...extra,\n      });\n    } catch (err) {\n      console.warn(\"Upload state callback failed:\", err);\n    }\n  }\n\n  setUploaderError(errorCode, err = null) {\n    this.status = \"error\";\n    this.error = errorCode || err?.message || \"upload-error\";\n    this.lastErrorAt = Date.now();\n    this.lastErrorCode = errorCode || null;\n    this.emitTelemetry(\"upload_error\", {\n      errorCode: errorCode || null,\n      message: err?.message || this.error || \"upload-error\",\n    });\n    this.scheduleJournalPersist({ force: true });\n  }\n\n  async setSessionId(sessionId) {\n    this.sessionId = sessionId || null;\n    await this.persistUploadJournal({ force: true });\n    this.notifyStateChange(\"session-updated\");\n  }\n\n  getResumeState() {\n    return {\n      projectId: this.projectId || null,\n      sceneId: this.sceneId || null,\n      type: this.metadata?.type || null,\n      trackType: this.getUploaderType(),\n      sessionId: this.sessionId || null,\n      videoId: this.videoId || null,\n      mediaId: this.mediaId || null,\n      uploadUrl: this.uploadUrl || null,\n      offset: this.offset || 0,\n      totalBytes: this.totalBytes || 0,\n      status: this.status,\n      error: this.error || null,\n      stalled: this.stalled,\n      queueLength: this.chunkQueue.length,\n      queuedBytes: this.queuedBytes,\n      lastProgressAt: this.lastProgressAt || null,\n      journalKey: this.journalKey || null,\n      journalLookupKey: this.journalLookupKey || null,\n      resumeCount: this.resumeCount || 0,\n      firstByteAt: this.firstByteAt || null,\n      readyAt: this.readyAt || null,\n      createdAt: this.createdAt || null,\n      finalizedAt: this.finalizedAt || null,\n      lastErrorAt: this.lastErrorAt || null,\n      lastErrorCode: this.lastErrorCode || null,\n      updatedAt: Date.now(),\n    };\n  }\n\n  scheduleJournalPersist({ force = false } = {}) {\n    if (force) {\n      if (this.journalPersistTimer) {\n        clearTimeout(this.journalPersistTimer);\n        this.journalPersistTimer = null;\n      }\n      void this.persistUploadJournal({ force: true });\n      return;\n    }\n\n    const sinceLastPersist = Date.now() - this.lastJournalPersistAt;\n    if (sinceLastPersist >= this.JOURNAL_WRITE_INTERVAL_MS) {\n      void this.persistUploadJournal({ force: false });\n      return;\n    }\n\n    if (this.journalPersistTimer) return;\n    const waitMs = this.JOURNAL_WRITE_INTERVAL_MS - sinceLastPersist;\n    this.journalPersistTimer = setTimeout(() => {\n      this.journalPersistTimer = null;\n      void this.persistUploadJournal({ force: false });\n    }, Math.max(250, waitMs));\n  }\n\n  getJournalKey(mediaId) {\n    return mediaId ? `uploadJournal-${mediaId}` : null;\n  }\n\n  getJournalLookupKey(projectId, sceneId, type) {\n    return `uploadJournalLookup-${projectId}-${sceneId || \"none\"}-${type}`;\n  }\n\n  getVideoMapKey(projectId, sceneId, type) {\n    return `bunnyVideoMap-${projectId}-${sceneId || \"none\"}-${type || \"none\"}`;\n  }\n\n  async getVideoMap(projectId, sceneId, type) {\n    if (typeof chrome === \"undefined\" || !chrome.storage?.local) return null;\n    const key = this.getVideoMapKey(projectId, sceneId, type);\n    const result = await chrome.storage.local.get([key]);\n    const entry = result?.[key];\n    if (!entry || !entry.videoId || !entry.mediaId) return null;\n    return { ...entry, key };\n  }\n\n  async persistVideoMap({\n    projectId,\n    sceneId,\n    type,\n    videoId,\n    mediaId,\n    sessionId,\n  }) {\n    if (typeof chrome === \"undefined\" || !chrome.storage?.local) return;\n    if (!projectId || !videoId || !mediaId) return;\n    const key = this.getVideoMapKey(projectId, sceneId, type);\n    await chrome.storage.local.set({\n      [key]: {\n        projectId,\n        sceneId: sceneId || null,\n        type: type || null,\n        trackType: this.getUploaderType(),\n        videoId,\n        mediaId,\n        sessionId: sessionId || null,\n        updatedAt: Date.now(),\n      },\n    });\n  }\n\n  buildFingerprint({ projectId, sceneId, type, width, height, fingerprint }) {\n    if (fingerprint) return fingerprint;\n    return `${projectId || \"none\"}:${sceneId || \"none\"}:${type || \"none\"}:${\n      width || \"na\"\n    }x${height || \"na\"}`;\n  }\n\n  async clearUploadJournal(journal = null) {\n    if (typeof chrome === \"undefined\" || !chrome.storage?.local) return;\n    const keysToRemove = [];\n    const mediaId = journal?.mediaId || this.mediaId || null;\n    const journalKey = journal?.key || this.getJournalKey(mediaId);\n    const lookupKey = journal?.lookupKey || this.journalLookupKey;\n\n    if (journalKey) keysToRemove.push(journalKey);\n    if (lookupKey) keysToRemove.push(lookupKey);\n    if (!keysToRemove.length) return;\n\n    try {\n      await chrome.storage.local.remove(keysToRemove);\n      this.debugLog(\"Cleared upload journal\", {\n        keys: keysToRemove,\n      });\n      if (!journal || !journal.key || journal.key === this.journalKey) {\n        this.journalKey = null;\n      }\n      this.notifyStateChange(\"journal-cleared\");\n    } catch (err) {\n      this.debugLog(\"Failed to clear upload journal\", {\n        error: err?.message || err,\n        keys: keysToRemove,\n      });\n    }\n  }\n\n  async persistUploadJournal({ force = false } = {}) {\n    if (\n      typeof chrome === \"undefined\" ||\n      !chrome.storage?.local ||\n      !this.mediaId\n    )\n      return;\n    if (this.journalPersistTimer && force) {\n      clearTimeout(this.journalPersistTimer);\n      this.journalPersistTimer = null;\n    }\n\n    if (\n      !force &&\n      Date.now() - this.lastJournalPersistAt < this.JOURNAL_WRITE_INTERVAL_MS\n    ) {\n      return;\n    }\n\n    const journalKey = this.journalKey || this.getJournalKey(this.mediaId);\n    this.journalKey = journalKey;\n\n    const inferredLookupKey =\n      this.projectId && this.metadata?.type\n        ? this.getJournalLookupKey(\n            this.projectId,\n            this.sceneId,\n            this.metadata.type,\n          )\n        : null;\n    const lookupKey = this.journalLookupKey || inferredLookupKey;\n    this.journalLookupKey = lookupKey;\n\n    const updatedAt = Date.now();\n    const payload = {\n      journalVersion: 2,\n      key: journalKey,\n      projectId: this.projectId || null,\n      sceneId: this.sceneId || null,\n      type: this.metadata.type || null,\n      trackType: this.getUploaderType(),\n      width: this.metaWidth || null,\n      height: this.metaHeight || null,\n      fingerprint: this.fingerprint || null,\n      sessionId: this.sessionId || null,\n      uploadUrl: this.uploadUrl || null,\n      signature: this.signature || null,\n      expires: this.expires || null,\n      libraryId: this.libraryId || null,\n      offset: this.offset,\n      totalBytes: this.totalBytes,\n      updatedAt,\n      videoId: this.videoId,\n      mediaId: this.mediaId,\n      stalled: this.stalled,\n      status: this.status,\n      error: this.error || null,\n      queueLength: this.chunkQueue.length,\n      queuedBytes: this.queuedBytes,\n      lastProgressAt: this.lastProgressAt || null,\n      createdAt: this.createdAt || null,\n      readyAt: this.readyAt || null,\n      firstByteAt: this.firstByteAt || null,\n      lastChunkQueuedAt: this.lastChunkQueuedAt || null,\n      finalizeStartedAt: this.finalizeStartedAt || null,\n      finalizedAt: this.finalizedAt || null,\n      lastErrorAt: this.lastErrorAt || null,\n      lastErrorCode: this.lastErrorCode || null,\n      lastServerOffset: this.lastServerOffset || 0,\n      resumeCount: this.resumeCount || 0,\n    };\n\n    const toStore = {\n      [journalKey]: payload,\n    };\n    if (lookupKey) {\n      toStore[lookupKey] = {\n        mediaId: this.mediaId,\n        key: journalKey,\n        updatedAt: payload.updatedAt,\n      };\n    }\n    try {\n      await chrome.storage.local.set(toStore);\n      this.lastJournalPersistAt = updatedAt;\n      this.notifyStateChange(\"journal-persisted\");\n    } catch (err) {\n      this.debugLog(\"Failed to persist upload journal\", {\n        error: err?.message || err,\n        mediaId: this.mediaId,\n      });\n    }\n  }\n\n  isResumeJournalStale(updatedAt) {\n    if (!updatedAt) return true;\n    const MAX_AGE_MS = 24 * 60 * 60 * 1000;\n    return Date.now() - updatedAt > MAX_AGE_MS;\n  }\n\n  validateResumeJournal(\n    journal,\n    { projectId, sceneId, type, fingerprint, allowSceneMismatch = false },\n  ) {\n    if (\n      !journal ||\n      !journal.mediaId ||\n      !journal.videoId ||\n      !journal.uploadUrl\n    ) {\n      return { valid: false, reason: \"missing-required-fields\" };\n    }\n    if (journal.projectId !== projectId) {\n      return { valid: false, reason: \"project-mismatch\" };\n    }\n    if (journal.type !== type) {\n      return { valid: false, reason: \"type-mismatch\" };\n    }\n    if (\n      !allowSceneMismatch &&\n      sceneId &&\n      journal.sceneId &&\n      journal.sceneId !== sceneId\n    ) {\n      return { valid: false, reason: \"scene-mismatch\" };\n    }\n    if (\n      fingerprint &&\n      journal.fingerprint &&\n      journal.fingerprint !== fingerprint\n    ) {\n      return { valid: false, reason: \"fingerprint-mismatch\" };\n    }\n    if (this.isResumeJournalStale(journal.updatedAt)) {\n      return { valid: false, reason: \"stale-journal\" };\n    }\n    return { valid: true, reason: \"ok\" };\n  }\n\n  async getResumeJournal({\n    projectId,\n    sceneId,\n    type,\n    fingerprint,\n    reuse = null,\n  }) {\n    if (typeof chrome === \"undefined\" || !chrome.storage?.local) return null;\n    const lookupKey = this.getJournalLookupKey(projectId, sceneId, type);\n    const candidates = [];\n\n    if (reuse?.mediaId) {\n      const reuseKey = this.getJournalKey(reuse.mediaId);\n      const reuseResult = await chrome.storage.local.get([reuseKey]);\n      if (reuseResult[reuseKey]) {\n        candidates.push({\n          journal: reuseResult[reuseKey],\n          key: reuseKey,\n          lookupKey,\n          allowSceneMismatch: true,\n        });\n      }\n    }\n\n    const lookupResult = await chrome.storage.local.get([lookupKey]);\n    const mappedMediaId = lookupResult?.[lookupKey]?.mediaId || null;\n    if (mappedMediaId) {\n      const mappedKey = this.getJournalKey(mappedMediaId);\n      const mappedResult = await chrome.storage.local.get([mappedKey]);\n      if (mappedResult[mappedKey]) {\n        candidates.push({\n          journal: mappedResult[mappedKey],\n          key: mappedKey,\n          lookupKey,\n          allowSceneMismatch: false,\n        });\n      }\n    }\n\n    if (!candidates.length) return null;\n\n    for (const candidate of candidates) {\n      const validation = this.validateResumeJournal(candidate.journal, {\n        projectId,\n        sceneId,\n        type,\n        fingerprint,\n        allowSceneMismatch: candidate.allowSceneMismatch,\n      });\n      if (validation.valid) {\n        this.debugLog(\"Found valid upload journal for resume\", {\n          mediaId: candidate.journal.mediaId,\n          projectId,\n          sceneId,\n          type,\n          offset: candidate.journal.offset || 0,\n        });\n        return {\n          ...candidate.journal,\n          key: candidate.key,\n          lookupKey: candidate.lookupKey,\n        };\n      }\n\n      this.debugLog(\"Discarding invalid upload journal\", {\n        reason: validation.reason,\n        mediaId: candidate.journal?.mediaId || null,\n        projectId,\n        sceneId,\n        type,\n      });\n      await this.clearUploadJournal({\n        key: candidate.key,\n        lookupKey: candidate.lookupKey,\n        mediaId: candidate.journal?.mediaId || null,\n      });\n    }\n\n    return null;\n  }\n\n  async getServerOffset() {\n    if (!this.uploadUrl) return null;\n    try {\n      const headRes = await fetch(this.uploadUrl, {\n        method: \"HEAD\",\n        headers: {\n          \"Tus-Resumable\": \"1.0.0\",\n          AuthorizationSignature: this.signature,\n          AuthorizationExpire: String(this.expires),\n          LibraryId: String(this.libraryId),\n          VideoId: this.videoId,\n        },\n      });\n\n      if (!headRes.ok) {\n        this.debugLog(\"HEAD offset check failed\", {\n          status: headRes.status,\n        });\n        return null;\n      }\n\n      const serverOffset = parseInt(\n        headRes.headers.get(\"Upload-Offset\") || \"0\",\n        10,\n      );\n      return Number.isFinite(serverOffset) ? serverOffset : null;\n    } catch (err) {\n      this.debugLog(\"HEAD offset check threw\", {\n        error: err?.message || err,\n      });\n      return null;\n    }\n  }\n  async initialize(\n    projectId,\n    {\n      title,\n      type,\n      width = null,\n      height = null,\n      linkedMediaId = null,\n      reuse = null,\n      sceneId = null,\n      sessionId = null,\n    },\n  ) {\n    if (this.status !== \"idle\" && this.status !== \"error\") {\n      throw new Error(\"Uploader has already been initialized\");\n    }\n\n    try {\n      this.projectId = projectId;\n      this.metadata = { title, type, linkedMediaId, sceneId };\n      this.trackType = this.trackType || type || null;\n      this.sceneId = sceneId;\n      this.metaWidth = width;\n      this.metaHeight = height;\n      this.sessionId = sessionId || this.sessionId || null;\n      this.journalLookupKey = this.getJournalLookupKey(projectId, sceneId, type);\n      this.createdAt = this.createdAt || Date.now();\n\n      this.status = \"initializing\";\n      this.error = null;\n      this.offset = 0;\n      this.totalBytes = 0;\n      this.lastErrorAt = null;\n      this.lastErrorCode = null;\n      this.initializedFromResume = false;\n\n      // Build fingerprint for resume journal matching\n      const fingerprint = this.buildFingerprint({\n        projectId,\n        sceneId,\n        type,\n        width,\n        height,\n      });\n      this.fingerprint = fingerprint;\n\n      // Attempt to locate a resume journal candidate from storage\n      const resumeJournal = await this.getResumeJournal({\n        projectId,\n        sceneId,\n        type,\n        fingerprint,\n        reuse,\n      });\n\n      // Validate reuse object if provided\n      if (reuse) {\n        if (!reuse.videoId || !reuse.mediaId) {\n          throw new Error(\n            \"Invalid reuse object: must have both videoId and mediaId\",\n          );\n        }\n        this.videoId = reuse.videoId;\n        this.mediaId = reuse.mediaId;\n      } else if (resumeJournal?.videoId && resumeJournal?.mediaId) {\n        this.initializedFromResume = true;\n        this.videoId = resumeJournal.videoId;\n        this.mediaId = resumeJournal.mediaId;\n        this.uploadUrl = resumeJournal.uploadUrl || null;\n        this.offset = resumeJournal.offset || 0;\n        this.totalBytes = resumeJournal.totalBytes || 0;\n        this.journalKey =\n          resumeJournal.key || this.journalKey || this.getJournalKey(this.mediaId);\n        this.journalLookupKey = resumeJournal.lookupKey || this.journalLookupKey;\n        if (!this.sessionId && resumeJournal.sessionId) {\n          this.sessionId = resumeJournal.sessionId;\n        }\n        this.resumeCount = (resumeJournal.resumeCount || 0) + 1;\n        this.debugLog(\"Resuming upload from journal candidate\", {\n          projectId,\n          sceneId,\n          type,\n          mediaId: this.mediaId,\n          offset: this.offset,\n        });\n        await this.persistVideoMap({\n          projectId,\n          sceneId,\n          type,\n          videoId: this.videoId,\n          mediaId: this.mediaId,\n          sessionId: this.sessionId,\n        });\n      } else {\n        const existingMap = await this.getVideoMap(projectId, sceneId, type);\n        if (existingMap?.videoId && existingMap?.mediaId) {\n          this.videoId = existingMap.videoId;\n          this.mediaId = existingMap.mediaId;\n          this.journalKey = this.getJournalKey(this.mediaId);\n          this.debugLog(\"Reusing Bunny video from map\", {\n            projectId,\n            sceneId,\n            type,\n            mediaId: this.mediaId,\n          });\n        }\n      }\n\n      if (!this.videoId || !this.mediaId) {\n        const { authenticated, user } = await new Promise((resolve) => {\n          chrome.runtime.sendMessage({ type: \"check-auth-status\" }, resolve);\n        });\n\n        if (!authenticated) throw new Error(\"Not authenticated with Screenity\");\n\n        const { screenityToken } = await chrome.storage.local.get([\n          \"screenityToken\",\n        ]);\n\n        this.userToken = screenityToken;\n\n        if (!this.userToken) {\n          throw new Error(\"Missing user token for saving upload metadata\");\n        }\n\n        const res = await fetch(`${API_BASE}/bunny/videos`, {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n            Authorization: `Bearer ${screenityToken}`,\n          },\n          body: JSON.stringify({\n            title,\n            projectId,\n            type,\n            linkedMediaId,\n            sceneId,\n            recordingSessionId: this.sessionId || null,\n          }),\n        });\n\n        if (!res.ok) throw new Error(\"Failed to create Bunny video\");\n        const data = await res.json();\n        this.videoId = data.videoId;\n        this.mediaId = data.mediaId;\n        await this.persistVideoMap({\n          projectId,\n          sceneId,\n          type,\n          videoId: this.videoId,\n          mediaId: this.mediaId,\n          sessionId: this.sessionId,\n        });\n      } else {\n        await this.persistVideoMap({\n          projectId,\n          sceneId,\n          type,\n          videoId: this.videoId,\n          mediaId: this.mediaId,\n          sessionId: this.sessionId,\n        });\n      }\n\n      this.journalKey = this.journalKey || this.getJournalKey(this.mediaId);\n      await this.refreshTusAuth();\n\n      if (this.uploadUrl) {\n        const serverOffset = await this.getServerOffset();\n        if (Number.isFinite(serverOffset) && serverOffset >= 0) {\n          this.lastServerOffset = serverOffset;\n          this.offset = serverOffset;\n          this.totalBytes = Math.max(this.totalBytes || 0, serverOffset);\n          if (this.initializedFromResume || serverOffset > 0) {\n            this.emitTelemetry(\"upload_resumed\", {\n              resumedOffset: serverOffset,\n              resumeCount: this.resumeCount || 1,\n            });\n          }\n        } else {\n          this.setUploaderError(\"resume-offset-unverified\");\n          throw new Error(\"Could not verify server offset during resume.\");\n        }\n      } else {\n        await this.initTusUpload();\n      }\n\n      await this.persistUploadJournal({ force: true });\n      this.startHeartbeat();\n      this.status = \"ready\";\n      this.readyAt = Date.now();\n      this.emitTelemetry(\"upload_started\", {\n        resumed: this.initializedFromResume,\n        resumeCount: this.resumeCount || 0,\n      });\n      this.scheduleJournalPersist({ force: true });\n      return { videoId: this.videoId, mediaId: this.mediaId };\n    } catch (err) {\n      if (this.status !== \"error\") {\n        this.setUploaderError(\"initialize-failed\", err);\n      } else {\n        this.scheduleJournalPersist({ force: true });\n      }\n      throw err;\n    }\n  }\n\n  async refreshTusAuth() {\n    // Prefer stored screenityToken for auth\n    const token = this.userToken || (await chrome.storage.local.get([\"screenityToken\"]).then(r => r.screenityToken));\n    const headers = token ? { Authorization: `Bearer ${token}` } : {};\n    const res = await fetch(\n      `${API_BASE}/bunny/videos/tus-auth?videoId=${this.videoId}`,\n      { headers },\n    );\n    if (!res.ok) throw new Error(\"Failed to refresh TUS auth\");\n    const { signature, expires, libraryId } = await res.json();\n    this.signature = signature;\n    this.expires = expires;\n    this.libraryId = libraryId;\n    this.scheduleJournalPersist();\n  }\n\n  async initTusUpload() {\n    const res = await fetch(\"https://video.bunnycdn.com/tusupload\", {\n      method: \"POST\",\n      headers: {\n        \"Tus-Resumable\": \"1.0.0\",\n        \"Upload-Defer-Length\": \"1\",\n        AuthorizationSignature: this.signature,\n        AuthorizationExpire: String(this.expires),\n        LibraryId: String(this.libraryId),\n        VideoId: this.videoId,\n        \"Upload-Metadata\": `filetype ${btoa(\"video/webm\")},title ${btoa(\n          this.metadata.title,\n        )}`,\n      },\n    });\n\n    if (!res.ok) throw new Error(\"Failed to start TUS upload session\");\n    const location = res.headers.get(\"location\");\n    this.uploadUrl = location.startsWith(\"/\")\n      ? `https://video.bunnycdn.com${location}`\n      : location;\n\n    if (this.userToken) {\n      fetch(`${API_BASE}/bunny/videos/save-upload-meta`, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n          Authorization: `Bearer ${this.userToken}`,\n        },\n        body: JSON.stringify({\n          mediaId: this.mediaId,\n          uploadUrl: this.uploadUrl,\n          signature: this.signature,\n          expires: this.expires,\n          projectId: this.projectId,\n          sceneId: this.sceneId || null,\n          recordingSessionId: this.sessionId || null,\n          type: this.metadata?.type || null,\n        }),\n      }).catch((err) =>\n        this.debugLog(\"save-upload-meta failed (non-blocking)\", { error: String(err) }),\n      );\n    } else {\n      this.debugLog(\"Skipping save-upload-meta because user token is missing\", {\n        mediaId: this.mediaId,\n        uploadUrl: this.uploadUrl,\n        signature: this.signature,\n        expires: this.expires,\n      });\n    }\n    await this.persistUploadJournal({ force: true });\n  }\n\n  async write(chunk) {\n    if (this.isFinalizing) throw new Error(\"Cannot write during finalization\");\n    if (this.isPaused) throw new Error(\"Uploader paused\");\n    if (!this.uploadUrl) throw new Error(\"Uploader not initialized\");\n\n    // Check if we're in error state\n    if (this.status === \"error\") {\n      throw new Error(`Uploader in error state: ${this.error}`);\n    }\n\n    await this.checkAuthExpiration();\n    this.status = \"uploading\";\n    if (!this.hasEmittedClientStarted) {\n      this.hasEmittedClientStarted = true;\n      this.emitTelemetry(\"upload_client_started\");\n    }\n\n    for (let i = 0; i < chunk.size; i += this.CHUNK_SIZE) {\n      const subChunk = chunk.slice(i, i + this.CHUNK_SIZE);\n      this.chunkQueue.push(subChunk);\n      this.queuedBytes += subChunk.size;\n      this.totalBytes += subChunk.size;\n    }\n    this.lastChunkQueuedAt = Date.now();\n    this.scheduleJournalPersist();\n\n    // Always ensure queue is processing\n    if (!this.isProcessingQueue) {\n      this.queueProcessingPromise = this.processQueue();\n    }\n\n    // Wait for current queue to finish processing these chunks\n    if (this.queuedBytes > 10 * 1024 * 1024) {\n      await this.waitForPendingUploads();\n    }\n\n    // Thumbnail extraction disabled in-flight to reduce timeouts/noise.\n    this._hasExtractedMeta = true;\n  }\n\n  async checkAuthExpiration() {\n    if (!this.expires) return;\n    const now = Math.floor(Date.now() / 1000);\n    if (this.expires - now < this.TOKEN_REFRESH_THRESHOLD) {\n      await this.refreshTusAuth();\n\n      if (this.isProcessingQueue) {\n        this.pause();\n        this.resume();\n      }\n    }\n  }\n  async uploadChunk(chunk) {\n    if (this.isFinalizing) return;\n    const data = new Uint8Array(await chunk.arrayBuffer());\n\n    for (let attempt = 0; attempt <= this.MAX_RETRIES; attempt++) {\n      try {\n        const currentOffset = this.offset;\n\n        const controller = new AbortController();\n        const timeout = setTimeout(\n          () => controller.abort(\"upload-timeout\"),\n          this.UPLOAD_TIMEOUT_MS,\n        );\n\n        const res = await fetch(this.uploadUrl, {\n          method: \"PATCH\",\n          headers: {\n            \"Tus-Resumable\": \"1.0.0\",\n            \"Content-Type\": \"application/offset+octet-stream\",\n            \"Upload-Offset\": String(currentOffset),\n            AuthorizationSignature: this.signature,\n            AuthorizationExpire: String(this.expires),\n            LibraryId: String(this.libraryId),\n            VideoId: this.videoId,\n          },\n          body: data,\n          signal: controller.signal,\n        });\n        clearTimeout(timeout);\n\n        if (res.ok || res.status === 204) {\n          // Server is the source of truth\n          const serverOffsetHeader = res.headers.get(\"Upload-Offset\");\n          if (serverOffsetHeader) {\n            this.offset = parseInt(serverOffsetHeader, 10);\n          } else {\n            // Fallback if Bunny doesn't return it for some reason\n            this.offset = currentOffset + data.length;\n          }\n          this.lastServerOffset = this.offset;\n\n          this.recordProgress(data.length);\n          return;\n        } else {\n          const errorText = await res.text();\n\n          // If the TUS session was invalidated (404), flag as fatal so callers can fall back.\n          if (res.status === 404) {\n            this.status = \"error\";\n            this.error = \"tus-session-missing\";\n            this.stalled = true;\n            this.lastErrorAt = Date.now();\n            this.lastErrorCode = \"tus-session-missing\";\n            this.emitTelemetry(\"upload_error\", {\n              errorCode: \"tus-session-missing\",\n              httpStatus: 404,\n            });\n            this.scheduleJournalPersist({ force: true });\n            if (typeof this.onStall === \"function\") {\n              this.onStall({\n                mediaId: this.mediaId,\n                videoId: this.videoId,\n                offset: this.offset,\n                diff: Date.now() - this.lastProgressAt,\n                reason: \"tus-404\",\n              });\n            }\n            throw new Error(\"TUS session missing (404).\");\n          }\n\n          // Handle offset mismatch errors (409 Conflict)\n          if (\n            res.status === 409 ||\n            errorText.toLowerCase().includes(\"offset\")\n          ) {\n            console.warn(\n              `⚠️ Offset conflict detected (status ${res.status}), fetching current offset from server`,\n            );\n\n            // Query server for current offset using HEAD request\n            try {\n              const serverOffset = await this.getServerOffset();\n              if (Number.isFinite(serverOffset) && serverOffset >= 0) {\n                this.lastServerOffset = serverOffset;\n                this.offset = serverOffset;\n                this.totalBytes = Math.max(this.totalBytes || 0, serverOffset);\n                this.emitTelemetry(\"upload_resumed\", {\n                  resumedOffset: serverOffset,\n                  reason: \"offset-conflict\",\n                });\n                this.scheduleJournalPersist({ force: true });\n\n                // Retry with corrected offset\n                continue;\n              }\n            } catch (headErr) {\n              console.error(\"Failed to fetch server offset:\", headErr);\n            }\n          }\n\n          throw new Error(`Upload failed (${res.status}): ${errorText}`);\n        }\n      } catch (err) {\n        if (err?.name === \"AbortError\") {\n          console.warn(\"⚠️ Upload chunk timed out\");\n        }\n        if (attempt === this.MAX_RETRIES) {\n          console.error(\n            `❌ Failed to upload chunk after ${this.MAX_RETRIES} retries:`,\n            err,\n          );\n          this.setUploaderError(\"chunk-upload-retries-exhausted\", err);\n          throw err;\n        }\n        console.warn(\n          `⚠️ Upload attempt ${attempt + 1}/${\n            this.MAX_RETRIES + 1\n          } failed, retrying...`,\n          err.message,\n        );\n        const jitter = Math.random() * 300;\n        await new Promise((r) =>\n          setTimeout(r, this.RETRY_DELAY * Math.pow(2, attempt) + jitter),\n        );\n      }\n    }\n  }\n\n  async processQueue() {\n    if (this.isProcessingQueue) return;\n    this.isProcessingQueue = true;\n\n    try {\n      while (this.chunkQueue.length && !this.isPaused && !this.isFinalizing) {\n        const chunk = this.chunkQueue.shift();\n        this.queuedBytes -= chunk.size;\n\n        // Serialize uploads: wait for each chunk to complete before starting next\n        try {\n          await this.uploadChunk(chunk);\n        } catch (err) {\n          console.error(\"❌ Chunk upload failed in queue:\", err);\n\n          // Put chunk back at front of queue for potential retry\n          this.chunkQueue.unshift(chunk);\n          this.queuedBytes += chunk.size;\n\n          // Set error state\n          this.setUploaderError(\"queue-upload-failed\", err);\n\n          // Stop processing queue on error\n          break;\n        }\n      }\n    } finally {\n      this.isProcessingQueue = false;\n      this.queueProcessingPromise = null;\n    }\n  }\n\n  async waitForPendingUploads() {\n    // Temporarily unpause to ensure all chunks are processed\n    const wasPaused = this.isPaused;\n    if (wasPaused) {\n      this.isPaused = false;\n    }\n\n    while (\n      this.chunkQueue.length > 0 ||\n      this.isProcessingQueue ||\n      this.pendingUploads.length > 0\n    ) {\n      if (this.stalled && this.lastProgressAt) {\n        const diff = Date.now() - this.lastProgressAt;\n        if (diff > this.HEARTBEAT_LAG_MS * 2) {\n          break;\n        }\n      }\n      if (this.chunkQueue.length && !this.isProcessingQueue) {\n        await this.processQueue();\n      }\n      if (this.queueProcessingPromise) {\n        await this.queueProcessingPromise;\n      }\n      if (this.pendingUploads.length) {\n        await Promise.all(this.pendingUploads);\n      }\n      // Small delay to catch any race conditions\n      await new Promise((r) => setTimeout(r, 100));\n    }\n\n    // Restore pause state if it was paused\n    if (wasPaused) {\n      this.isPaused = true;\n    }\n  }\n\n  async finalize() {\n    if (this.isFinalizing) throw new Error(\"Already finalizing\");\n    this.isFinalizing = true;\n    this.status = \"finalizing\";\n    this.finalizeStartedAt = Date.now();\n    this.emitTelemetry(\"upload_finalize_started\");\n    this.scheduleJournalPersist({ force: true });\n    try {\n      await this.waitForPendingUploads();\n      await this.checkAuthExpiration();\n\n      // Re-validate offset with server to avoid partial finalization\n      let serverOffset = null;\n      try {\n        const headRes = await fetch(this.uploadUrl, {\n          method: \"HEAD\",\n          headers: {\n            \"Tus-Resumable\": \"1.0.0\",\n            AuthorizationSignature: this.signature,\n            AuthorizationExpire: String(this.expires),\n            LibraryId: String(this.libraryId),\n            VideoId: this.videoId,\n          },\n        });\n\n        if (headRes.ok) {\n          serverOffset = parseInt(\n            headRes.headers.get(\"Upload-Offset\") || \"0\",\n            10,\n          );\n          this.offset = serverOffset;\n        }\n      } catch (err) {\n        console.warn(\"⚠️ Failed HEAD before finalize:\", err);\n      }\n\n      // If server didn't receive everything, don't \"finalize\" into a ghost upload.\n      if (serverOffset === null) {\n        this.setUploaderError(\"finalize-offset-unverified\");\n        throw new Error(\"Finalize failed: could not verify server offset.\");\n      }\n\n      if (serverOffset === 0) {\n        this.status = \"error\";\n        this.error = \"server-offset-0\";\n        this.lastErrorAt = Date.now();\n        this.lastErrorCode = \"server-offset-0\";\n        this.emitTelemetry(\"upload_error\", {\n          errorCode: \"server-offset-0\",\n        });\n        this.scheduleJournalPersist({ force: true });\n        throw new Error(\"Finalize failed: server has 0 bytes.\");\n      }\n\n      if (serverOffset < this.totalBytes) {\n        this.status = \"error\";\n        this.error = `incomplete-upload server=${serverOffset} expected=${this.totalBytes}`;\n        this.lastErrorAt = Date.now();\n        this.lastErrorCode = \"finalize-incomplete-upload\";\n        this.emitTelemetry(\"upload_error\", {\n          errorCode: \"finalize-incomplete-upload\",\n          serverOffset,\n          expectedBytes: this.totalBytes,\n        });\n        this.scheduleJournalPersist({ force: true });\n        throw new Error(\n          `Finalize blocked: upload incomplete (server ${serverOffset} / expected ${this.totalBytes}).`,\n        );\n      }\n\n      if (serverOffset > this.totalBytes) {\n        this.status = \"error\";\n        this.error = `invalid-length server=${serverOffset} expected=${this.totalBytes}`;\n        this.lastErrorAt = Date.now();\n        this.lastErrorCode = \"finalize-invalid-length\";\n        this.emitTelemetry(\"upload_error\", {\n          errorCode: \"finalize-invalid-length\",\n          serverOffset,\n          expectedBytes: this.totalBytes,\n        });\n        this.scheduleJournalPersist({ force: true });\n        throw new Error(\n          `Finalize blocked: serverOffset (${serverOffset}) exceeds expected totalBytes (${this.totalBytes}).`,\n        );\n      }\n\n      // Now we can safely complete the tus upload by declaring the final length\n      const res = await fetch(this.uploadUrl, {\n        method: \"PATCH\",\n        headers: {\n          \"Tus-Resumable\": \"1.0.0\",\n          \"Content-Type\": \"application/offset+octet-stream\",\n          \"Upload-Offset\": String(serverOffset),\n          \"Upload-Length\": String(this.totalBytes),\n          AuthorizationSignature: this.signature,\n          AuthorizationExpire: String(this.expires),\n          LibraryId: String(this.libraryId),\n          VideoId: this.videoId,\n        },\n      });\n\n      if (!res.ok && res.status !== 204) {\n        this.setUploaderError(\"finalize-patch-failed\");\n        throw new Error(\"Finalization failed\");\n      }\n      this.status = \"completed\";\n      this.finalizedAt = Date.now();\n      this.emitTelemetry(\"upload_finalize_completed\", {\n        finalizedBytes: this.totalBytes,\n      });\n      this.emitTelemetry(\"upload_complete_client\", {\n        finalizedBytes: this.totalBytes,\n      });\n      this.stopHeartbeat();\n      await this.clearUploadJournal();\n      this.notifyStateChange(\"finalize-completed\");\n    } catch (err) {\n      // Reset the lock so a subsequent retry call can attempt finalize again.\n      // Without this reset, isFinalizing stays true permanently after any error,\n      // and every retry immediately throws \"Already finalizing\".\n      this.isFinalizing = false;\n      console.warn(\"[BunnyTusUploader] finalize failed — isFinalizing reset for retry\", {\n        trackType: this.trackType,\n        error: err?.message || String(err),\n        offset: this.offset,\n        totalBytes: this.totalBytes,\n      });\n      throw err;\n    }\n  }\n\n  getMeta() {\n    return {\n      videoId: this.videoId,\n      mediaId: this.mediaId,\n      offset: this.offset,\n      status: this.status,\n      error: this.error,\n      isPaused: this.isPaused,\n      isFinalizing: this.isFinalizing,\n      metadata: this.metadata,\n      expiresAt: this.expires ? new Date(this.expires * 1000) : null,\n      queueLength: this.chunkQueue.length,\n      queuedBytes: this.queuedBytes,\n      width: this.metaWidth || null,\n      height: this.metaHeight || null,\n      thumbnail: this.metaThumbnail || null,\n      sceneId: this.sceneId || null,\n      lastProgressAt: this.lastProgressAt,\n      stalled: this.stalled,\n    };\n  }\n\n  pause() {\n    this.isPaused = true;\n    if (this.status !== \"completed\" && this.status !== \"error\") {\n      this.status = \"paused\";\n    }\n    this.scheduleJournalPersist();\n  }\n\n  resume() {\n    if (this.isPaused) {\n      this.isPaused = false;\n      if (\n        this.status !== \"completed\" &&\n        this.status !== \"error\" &&\n        this.status !== \"finalizing\"\n      ) {\n        this.status = \"uploading\";\n      }\n      if (!this.isProcessingQueue && this.chunkQueue.length > 0) {\n        this.queueProcessingPromise = this.processQueue();\n      }\n      this.emitTelemetry(\"upload_resumed\", {\n        reason: \"client-resume\",\n      });\n      this.scheduleJournalPersist();\n    }\n  }\n\n  async abort() {\n    this.pause();\n    this.status = \"aborted\";\n    this.uploadUrl = null;\n    this.chunkQueue = [];\n    this.queuedBytes = 0;\n    this.totalBytes = 0;\n    this.pendingUploads = [];\n    this.stopHeartbeat();\n    this.emitTelemetry(\"upload_cancelled\");\n    if (this.journalPersistTimer) {\n      clearTimeout(this.journalPersistTimer);\n      this.journalPersistTimer = null;\n    }\n    await this.clearUploadJournal();\n    this.notifyStateChange(\"aborted\");\n  }\n\n  startHeartbeat() {\n    this.stopHeartbeat();\n    this.lastProgressAt = Date.now();\n    this.stalled = false;\n    this.heartbeatTimer = setInterval(() => {\n      const now = Date.now();\n      const diff = now - this.lastProgressAt;\n      if (diff > this.HEARTBEAT_LAG_MS) {\n        this.stalled = true;\n        this.emitTelemetry(\"upload_stalled\", {\n          stallMs: diff,\n        });\n        this.scheduleJournalPersist({ force: true });\n        if (typeof this.onStall === \"function\") {\n          this.onStall({\n            mediaId: this.mediaId,\n            videoId: this.videoId,\n            offset: this.offset,\n            diff,\n          });\n        }\n      }\n    }, this.HEARTBEAT_INTERVAL_MS);\n  }\n\n  stopHeartbeat() {\n    if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);\n    this.heartbeatTimer = null;\n  }\n\n  recordProgress(bytes) {\n    this.lastProgressAt = Date.now();\n    this.stalled = false;\n    if (!this.firstByteAt) {\n      this.firstByteAt = this.lastProgressAt;\n    }\n    if (typeof this.onProgress === \"function\") {\n      try {\n        this.onProgress({\n          bytes,\n          offset: this.offset,\n          videoId: this.videoId,\n          mediaId: this.mediaId,\n          at: this.lastProgressAt,\n        });\n      } catch (err) {\n        console.warn(\"Progress callback failed:\", err);\n      }\n    }\n    if (!this.hasEmittedFirstByte) {\n      this.hasEmittedFirstByte = true;\n      this.emitTelemetry(\"upload_first_byte\", {\n        bytes,\n      });\n    }\n    if (\n      this.lastProgressEventAt === 0 ||\n      this.lastProgressAt - this.lastProgressEventAt >=\n        this.PROGRESS_EVENT_INTERVAL_MS\n    ) {\n      this.lastProgressEventAt = this.lastProgressAt;\n      this.emitTelemetry(\"upload_progress\", {\n        bytes,\n      });\n    }\n    this.scheduleJournalPersist();\n  }\n}\n\nif (typeof module !== \"undefined\" && module.exports) {\n  module.exports = BunnyTusUploader;\n} else {\n  window.BunnyTusUploader = BunnyTusUploader;\n}\n"
  },
  {
    "path": "src/pages/CloudRecorder/createVideoProject.js",
    "content": "const VIDEO_INIT = {\n  aspectRatio: {\n    width: 16,\n    height: 9,\n    source: \"preset\",\n    preset: \"Youtube (16:9)\",\n  },\n  hasRecordingSession: true,\n  baseResolution: 720,\n  duration: 12,\n  fps: 30,\n  lastUsedBackdrop: { categoryId: 5, backdropId: 8 },\n  lastUsedStylePrefs: {\n    screen: {\n      padding: 0.8,\n      cornerRadius: 0,\n      darkMode: false,\n      statusBar: false,\n      url: \"\",\n      shadow: 0,\n      borderThickness: 0,\n    },\n    camera: {\n      padding: 0.8,\n      roundness: 1,\n    },\n    shadowStrength: 0.5,\n  },\n  lastUsedLayout: {\n    type: \"cameraVideo\",\n    layout: \"bubble1\",\n  },\n  lastUsedMockup: {\n    id: \"macbookPro16\",\n    category: \"laptop\",\n    model: \"16-inch\",\n    color: \"silver\",\n  },\n  lastUsedKeyframe: {\n    cameraZoom: true,\n    cameraScale: 0.8,\n    preset: \"CINEMATIC\",\n  },\n  captionSettings: {\n    enabled: false,\n    size: 100,\n    position: 82,\n    style: \"modern-glass\",\n    defaultSource: \"screen\",\n    textDirection: \"ltr\",\n    wordByWord: false,\n  },\n  audioSettings: {\n    enabled: false,\n    track: \"none\",\n    volume: 0.5,\n    loop: true,\n  },\n  sceneOrder: [],\n  scenes: {},\n};\n\nexport const createVideoProject = async ({\n  title = \"Untitled Recording\",\n  instantMode = false,\n} = {}) => {\n  return new Promise((resolve, reject) => {\n    if (typeof chrome === \"undefined\" || !chrome.runtime?.sendMessage) {\n      return reject(new Error(\"Chrome extension runtime is not available\"));\n    }\n\n    let data = VIDEO_INIT;\n\n    if (instantMode) {\n      data = {\n        ...data,\n        instantMode: true,\n      };\n    }\n\n    chrome.runtime.sendMessage(\n      {\n        type: \"create-video-project\",\n        title,\n        data,\n        instantMode,\n      },\n      (response) => {\n        if (chrome.runtime.lastError) {\n          return reject(\n            new Error(\n              \"Extension runtime error: \" + chrome.runtime.lastError.message\n            )\n          );\n        }\n\n        if (!response || !response.success) {\n          if (response?.message === \"User not authenticated\") {\n            chrome.runtime.sendMessage({ type: \"handle-login\" });\n            return reject(new Error(\"User not authenticated — login required\"));\n          }\n\n          return reject(\n            new Error(response?.error || \"Failed to create video project\")\n          );\n        }\n\n        resolve(response.videoId);\n      }\n    );\n  });\n};\n"
  },
  {
    "path": "src/pages/CloudRecorder/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n    <title>Screenity - Recorder</title>\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n      body {\n        font-family: Satoshi-Medium;\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/CloudRecorder/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport CloudRecorder from \"./CloudRecorder\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<CloudRecorder />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/CloudRecorder/messaging.js",
    "content": "import { classifyError } from \"../utils/errorCodes\";\n\nconst urlParams = new URLSearchParams(window.location.search);\nconst IS_INJECTED_IFRAME = urlParams.has(\"injected\");\nconst IS_IFRAME_CONTEXT =\n  IS_INJECTED_IFRAME ||\n  (window.top !== window.self &&\n    !document.referrer.startsWith(\"chrome-extension://\"));\n\nexport function sendRecordingError(why, cancel = false) {\n  const errorType = !cancel ? \"stream-error\" : \"cancel-modal\";\n  const whyStr = typeof why === \"string\" ? why : JSON.stringify(why);\n  const errorCode = classifyError(whyStr, errorType);\n\n  chrome.runtime.sendMessage({\n    type: \"recording-error\",\n    error: errorType,\n    why: whyStr,\n    errorCode,\n  });\n\n  if (IS_IFRAME_CONTEXT) {\n    // Reload the iframe\n    window.location.reload();\n  }\n}\n\nexport function sendStopRecording(reason = \"generic\") {\n  const payload =\n    typeof reason === \"string\"\n      ? { reason }\n      : { ...(reason || {}), reason: reason?.reason || \"generic\" };\n\n  chrome.runtime.sendMessage({\n    type: \"stop-recording-tab\",\n    ...payload,\n  });\n}\n"
  },
  {
    "path": "src/pages/CloudRecorder/recorderConfig.js",
    "content": "export const MIME_TYPES = [\n  \"video/webm;codecs=vp9,opus\",\n  \"video/webm;codecs=vp8,opus\",\n  \"video/webm;codecs=h264\",\n  \"video/webm\",\n  \"video/mp4\",\n  \"video/webm;codecs=avc1\",\n];\n\nexport function getBitrates(quality) {\n  switch (quality) {\n    case \"4k\":\n      return { audio: 192000, video: 40000000 };\n    case \"1080p\":\n      return { audio: 192000, video: 8000000 };\n    case \"720p\":\n      return { audio: 128000, video: 5000000 };\n    case \"480p\":\n      return { audio: 96000, video: 2500000 };\n    case \"360p\":\n      return { audio: 96000, video: 1000000 };\n    case \"240p\":\n      return { audio: 64000, video: 500000 };\n    default:\n      return { audio: 128000, video: 5000000 };\n  }\n}\n\nexport const VIDEO_QUALITIES = {\n  \"4k\": { width: 4096, height: 2160 },\n  \"1080p\": { width: 1920, height: 1080 },\n  \"720p\": { width: 1280, height: 720 },\n  \"480p\": { width: 854, height: 480 },\n  \"360p\": { width: 640, height: 360 },\n  \"240p\": { width: 426, height: 240 },\n  default: { width: 2560, height: 1440 }, // 2.5K as default\n};\n\nexport function getResolutionForQuality(qualityValue = \"default\") {\n  return VIDEO_QUALITIES[qualityValue] || VIDEO_QUALITIES.default;\n}\n"
  },
  {
    "path": "src/pages/CloudRecorder/warning/Warning.jsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  useContext,\n  useCallback,\n  useRef,\n} from \"react\";\n\nimport { ReactSVG } from \"react-svg\";\n\nimport * as ToastEl from \"@radix-ui/react-toast\";\n\nconst Warning = () => {\n  const [open, setOpen] = useState(false);\n  const [title, setTitle] = useState(\"Record computer audio\");\n  const [description, setDescription] = useState(\"\");\n  const [duration, setDuration] = useState(10000);\n\n  const openWarning = useCallback((title, description, duration) => {\n    setTitle(title);\n    setDescription(description);\n    setDuration(duration);\n    setOpen(true);\n  }, []);\n\n  useEffect(() => {\n    // Check if macOS\n    const isMac = navigator.userAgent.indexOf(\"Mac\") !== -1;\n    if (isMac) {\n      openWarning(\n        chrome.i18n.getMessage(\"recordAudioWarningMacTitle\"),\n        chrome.i18n.getMessage(\"recordAudioWarningMacDescription\"),\n        10000\n      );\n    } else {\n      openWarning(\n        chrome.i18n.getMessage(\"recordAudioWarningOtherTitle\"),\n        chrome.i18n.getMessage(\"recordAudioWarningOtherDescription\"),\n        10000\n      );\n    }\n  }, []);\n\n  return (\n    <ToastEl.Provider swipeDirection=\"down\" duration={duration}>\n      <ToastEl.Root\n        className=\"warning-root\"\n        open={open}\n        onOpenChange={setOpen}\n        onSwipeEnd={() => {\n          setOpen(false);\n        }}\n      >\n        <div className=\"warning-icon\">\n          <ReactSVG\n            src={chrome.runtime.getURL(\"assets/tool-icons/audio-icon.svg\")}\n            width={20}\n            height={20}\n          />\n        </div>\n        <div className=\"warning-content\">\n          <ToastEl.Title className=\"warning-title\">{title}</ToastEl.Title>\n          <ToastEl.Description className=\"warning-description\">\n            {description}\n          </ToastEl.Description>\n        </div>\n        <ToastEl.Close\n          className=\"warning-close\"\n          onClick={() => {\n            setOpen(false);\n          }}\n        >\n          <ReactSVG\n            src={chrome.runtime.getURL(\"assets/camera-icons/close.svg\")}\n            width={20}\n            height={20}\n          />\n        </ToastEl.Close>\n      </ToastEl.Root>\n      <ToastEl.Viewport className=\"WarningViewport\" />\n      <style>\n        {`\n\n\t\t\t\tbutton {\n\t\t\t\t\tall: unset;\n\t\t\t\t}\n\t\t\t\t.WarningViewport {\n\t\t\t\t\t--viewport-padding: 25px;\n\t\t\t\t\tposition: fixed;\n\t\t\t\t\tbottom: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\tmargin: auto !important;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tpadding: var(--viewport-padding);\n\t\t\t\t\tgap: 14px;\n\t\t\t\t\tmax-width: 100vw;\n\t\t\t\t\twidth: fit-content;\n\t\t\t\t\tlist-style: none;\n\t\t\t\t\tz-index: 2147483647;\n\t\t\t\t\toutline: none;\n\t\t\t\t\tpointer-events: all !important;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.warning-root {\n\t\t\t\t\tbackground-color: #29292F;\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tbox-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n\t\t\t\t\t\thsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n\t\t\t\t\tpadding: 14px 20px;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: row;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tgap: 8px;\n\t\t\t\t\tfont-size: 15px;\n\t\t\t\t\tline-height: 1.5;\n\t\t\t\t\tmax-width: 500px;\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\ttext-align: left;\n\t\t\t\t\talign-items: flex-start;\n\t\t\t\t}\n\t\t\t\t.warning-content {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: flex-start;\n\t\t\t\t\tgap: 8px;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-state=\"open\"] {\n\t\t\t\t\tanimation: slideIn2 150ms cubic-bezier(0.16, 1, 0.3, 1);\n\t\t\t\t}\n\t\t\t\t.warning-root[data-state=\"closed\"] {\n\t\t\t\t\tanimation: hide 100ms ease-in;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"move\"] {\n\t\t\t\t\ttransform: translateY(var(--radix-toast-swipe-move-y));\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"cancel\"] {\n\t\t\t\t\ttransform: translateY(0);\n\t\t\t\t\ttransition: transform 200ms ease-out;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"end\"] {\n\t\t\t\t\tanimation: swipeOut2 100ms ease-out;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes hide {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\topacity: 1;\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\topacity: 0 !important;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes slideIn2 {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\ttransform: translateY(calc(100% + var(--viewport-padding)));\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\ttransform: translateY(0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes swipeOut2 {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\ttransform: translateY(var(--radix-toast-swipe-end-y));\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\ttransform: translateY(calc(100% + var(--viewport-padding)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.warning-title {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.warning-description {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\topacity: 0.8;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.ToastAction {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t\ttext-align: right;\n\t\t\t\t\tbackground-color: #51515f;\n\t\t\t\t\tpadding: 0px 12px !important;\n\t\t\t\t\theight: 24px !important;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t\t.warning-close {\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t.warning-close:hover {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t\t`}\n      </style>\n    </ToastEl.Provider>\n  );\n};\n\nexport default Warning;\n"
  },
  {
    "path": "src/pages/Content/Content.jsx",
    "content": "import React, { useState, useContext, useEffect, useRef } from \"react\";\n\nimport Wrapper from \"./Wrapper\";\n\nimport ContentState from \"./context/ContentState\";\n\nconst Content = () => {\n  return (\n    <div className=\"screenity-shadow-dom\">\n      <ContentState>\n        <Wrapper />\n      </ContentState>\n      <style type=\"text/css\">{`\n\t\t\t#screenity-ui, #screenity-ui div {\n\t\t\t\tbackground-color: unset;\n\t\t\t\tpadding: unset;\n\t\t\t\twidth: unset;\n\t\t\t\tbox-shadow: unset;\n\t\t\t\tdisplay: unset;\n\t\t\t\tmargin: unset;\n\t\t\t\tborder-radius: unset;\n\t\t\t}\n\t\t\t.screenity-outline {\n\t\t\t\tposition: absolute;\n\t\t\t\tz-index: 99999999999;\n\t\t\t\tborder: 2px solid #3080F8;\n\t\t\t\toutline-offset: -2px;\n\t\t\t\tpointer-events: none;\n\t\t\t\tborder-radius: 5px!important;\n\t\t\t}\n\t\t.screenity-blur {\n\t\t\tfilter: blur(10px)!important;\n\t\t}\n\t\t\t.screenity-shadow-dom * {\n\t\t\t\ttransition: unset;\n\t\t\t}\n\t\t\t.screenity-shadow-dom .TooltipContent {\n  border-radius: 30px!important;\n\tbackground-color: #29292F!important;\n  padding: 10px 15px!important;\n  font-size: 12px;\n\tmargin-bottom: 10px!important;\n\tbottom: 100px;\n  line-height: 1;\n\tfont-family: 'Satoshi-Medium', sans-serif;\n\tz-index: 99999999!important;\n  color: #FFF;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px!important;\n  user-select: none;\n\ttransition: opacity 0.3 ease-in-out;\n  will-change: transform, opacity;\n\tanimation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.screenity-shadow-dom .hide-tooltip {\n\tdisplay: none!important;\n}\n\n.screenity-shadow-dom .tooltip-tall {\n\tmargin-bottom: 20px;\n}\n\n.screenity-shadow-dom .tooltip-small {\n\tmargin-bottom: 5px;\n}\n\n.screenity-shadow-dom .TooltipContent[data-state='delayed-open'][data-side='top'] {\n\tanimation-name: screenity-slideDownAndFade;\n}\n.screenity-shadow-dom .TooltipContent[data-state='delayed-open'][data-side='right'] {\n  animation-name: screenity-slideLeftAndFade;\n}\n.screenity-shadow-dom.TooltipContent[data-state='delayed-open'][data-side='bottom'] {\n  animation-name: screenity-slideUpAndFade;\n}\n.screenity-shadow-dom.TooltipContent[data-state='delayed-open'][data-side='left'] {\n  animation-name: screenity-slideRightAndFade;\n}\n\n@keyframes screenity-slideUpAndFade {\n  from { opacity: 0; transform: translateY(2px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n@keyframes screenity-slideRightAndFade {\n  from { opacity: 0; transform: translateX(-2px); }\n  to   { opacity: 1; transform: translateX(0); }\n}\n@keyframes screenity-slideDownAndFade {\n  from { opacity: 0; transform: translateY(-2px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n@keyframes screenity-slideLeftAndFade {\n  from { opacity: 0; transform: translateX(2px); }\n  to   { opacity: 1; transform: translateX(0); }\n}\n\n#screenity-ui [data-radix-popper-content-wrapper] { z-index: 999999999999!important; }\n\n.screenity-shadow-dom .CanvasContainer {\n\tposition: fixed;\n\tpointer-events: all!important;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999!important;\n}\n.screenity-shadow-dom .canvas {\n\tposition: fixed;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999!important;\n\tbackground: transparent!important;\n}\n.screenity-shadow-dom .canvas-container {\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999;\n\tposition: fixed!important;\n\tbackground: transparent!important;\n}\n\n.ScreenityDropdownMenuContent {\n\tz-index: 99999999999!important;\n  min-width: 200px;\n  background-color: white;\n  margin-top: 4px;\n  margin-right: 8px;\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-radius: 15px;\n  font-family: 'Satoshi-Medium', sans-serif;\n  color: #29292F;\n  box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),\n    0px 10px 20px -15px rgba(22, 23, 24, 0.2);\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n.ScreenityDropdownMenuContent[data-side=\"top\"] {\n  animation-name: screenity-slideDownAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"right\"] {\n  animation-name: screenity-slideLeftAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"bottom\"] {\n  animation-name: screenity-slideUpAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"left\"] {\n  animation-name: screenity-slideRightAndFade;\n}\n.ScreenityItemIndicator {\n  position: absolute;\n  right: 12px;\n  width: 18px;\n  height: 18px;\n  background: #3080F8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n.ScreenityDropdownMenuItem,\n.ScreenityDropdownMenuRadioItem {\n  font-size: 14px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  height: 40px;\n  padding: 0 5px;\n  position: relative;\n  padding-left: 22px;\n  padding-right: 22px;\n  user-select: none;\n  outline: none;\n}\n.ScreenityDropdownMenuItem:hover {\n    background-color: #F6F7FB !important;\n    cursor: pointer;\n}\n.ScreenityDropdownMenuItem[data-disabled] {\n  color: #6E7684 !important;\n  cursor: not-allowed;\n  background-color: #F6F7FB !important;\n}\n\n`}</style>\n    </div>\n  );\n};\n\nexport default Content;\n"
  },
  {
    "path": "src/pages/Content/DevHUD.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nconst Z = 2147483647;\n\nconst BTN = {\n  padding: \"4px 8px\",\n  fontSize: \"11px\",\n  border: \"1px solid rgba(255,255,255,0.2)\",\n  borderRadius: \"4px\",\n  background: \"rgba(0,0,0,0.6)\",\n  color: \"#fff\",\n  cursor: \"pointer\",\n  whiteSpace: \"nowrap\",\n  fontFamily: \"monospace\",\n  lineHeight: \"1.4\",\n  boxSizing: \"border-box\",\n};\n\nconst DevHUD = ({ contentStateRef, setContentState }) => {\n  const [collapsed, setCollapsed] = useState(true);\n  const [portalEl, setPortalEl] = useState(null);\n\n  useEffect(() => {\n    const el = document.createElement(\"div\");\n    el.id = \"screenity-dev-hud-portal\";\n    // Make the container itself invisible/non-interfering but ensure its\n    // children (position:fixed) are not clipped by host-page CSS.\n    el.style.cssText = [\n      \"position:fixed !important\",\n      \"top:0 !important\",\n      \"left:0 !important\",\n      \"width:0 !important\",\n      \"height:0 !important\",\n      \"overflow:visible !important\",\n      \"pointer-events:none !important\",\n      `z-index:${Z} !important`,\n      \"display:block !important\",\n      \"visibility:visible !important\",\n      \"opacity:1 !important\",\n    ].join(\";\");\n    document.documentElement.appendChild(el);\n    setPortalEl(el);\n    return () => el.remove();\n  }, []);\n\n  const toast = (msg, ms = 8000) => {\n    contentStateRef.current?.openToast?.(msg, () => {}, ms);\n  };\n\n  const actions = [\n    {\n      label: \"Stream error modal\",\n      fn: () => {\n        const s = contentStateRef.current;\n        s?.openModal?.(\n          chrome.i18n.getMessage(\"streamErrorModalTitle\"),\n          chrome.i18n.getMessage(\"streamErrorModalDescription\"),\n          chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n          null,\n          () => {},\n          () => {},\n          null,\n          null,\n          null,\n          false,\n          chrome.i18n.getMessage(\"getHelpButton\"),\n          () => {\n            chrome.runtime.sendMessage({\n              type: \"report-error\",\n              errorCode: \"DEBUG_TEST\",\n              source: \"stream-error\",\n            });\n          },\n        );\n      },\n    },\n    {\n      label: \"Low storage warning\",\n      fn: () => toast(chrome.i18n.getMessage(\"toastStorageLow\")),\n    },\n    {\n      label: \"Low storage critical\",\n      fn: () => toast(chrome.i18n.getMessage(\"toastStorageCritical\")),\n    },\n    {\n      label: \"Stream ended toast\",\n      fn: () => toast(chrome.i18n.getMessage(\"streamEndedWarningToast\"), 10000),\n    },\n    {\n      label: \"Video track ended\",\n      fn: () => toast(chrome.i18n.getMessage(\"videoTrackEndedToast\")),\n    },\n    {\n      label: \"Audio track ended\",\n      fn: () => toast(chrome.i18n.getMessage(\"audioTrackEndedToast\")),\n    },\n    {\n      label: \"Stop-ack timeout\",\n      fn: () => toast(chrome.i18n.getMessage(\"stopAckTimeoutToast\")),\n    },\n    {\n      label: \"Editor recovery\",\n      fn: () => toast(chrome.i18n.getMessage(\"editorRecoveryToast\"), 12000),\n    },\n    {\n      label: \"Memory limit modal\",\n      fn: () => {\n        const s = contentStateRef.current;\n        s?.openModal?.(\n          chrome.i18n.getMessage(\"memoryLimitTitle\"),\n          chrome.i18n.getMessage(\"memoryLimitDescription\"),\n          chrome.i18n.getMessage(\"understoodButton\"),\n          null,\n          () => {},\n          () => {}\n        );\n      },\n    },\n    {\n      label: \"Preparing overlay\",\n      fn: () => {\n        setContentState?.((p) => ({\n          ...p,\n          preparingRecording: true,\n          processingProgress: 42,\n        }));\n      },\n    },\n  ];\n\n  if (!portalEl) return null;\n\n  // Shared wrapper style — lives inside the zero-size fixed container,\n  // so it needs its own position:fixed to escape.\n  const wrapBase = {\n    position: \"fixed\",\n    bottom: 6,\n    right: 6,\n    zIndex: Z,\n    pointerEvents: \"auto\",\n    fontFamily: \"monospace\",\n    boxSizing: \"border-box\",\n  };\n\n  const ui = collapsed ? (\n    <div\n      onClick={() => setCollapsed(false)}\n      style={{\n        ...wrapBase,\n        background: \"rgba(0,0,0,0.65)\",\n        color: \"#0f0\",\n        fontSize: \"10px\",\n        padding: \"3px 7px\",\n        borderRadius: \"4px\",\n        cursor: \"pointer\",\n      }}\n    >\n      DEV\n    </div>\n  ) : (\n    <div\n      style={{\n        ...wrapBase,\n        background: \"rgba(20,20,20,0.9)\",\n        borderRadius: \"6px\",\n        padding: \"8px\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: \"4px\",\n        maxWidth: \"170px\",\n        maxHeight: \"80vh\",\n        overflowY: \"auto\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          alignItems: \"center\",\n          marginBottom: \"2px\",\n        }}\n      >\n        <span style={{ color: \"#0f0\", fontSize: \"10px\" }}>DEV HUD</span>\n        <span\n          onClick={() => setCollapsed(true)}\n          style={{ color: \"#888\", fontSize: \"12px\", cursor: \"pointer\" }}\n        >\n          x\n        </span>\n      </div>\n      {actions.map((a) => (\n        <button key={a.label} style={BTN} onClick={a.fn}>\n          {a.label}\n        </button>\n      ))}\n    </div>\n  );\n\n  return createPortal(ui, portalEl);\n};\n\nexport default DevHUD;\n"
  },
  {
    "path": "src/pages/Content/Wrapper.jsx",
    "content": "import React, { useContext, useRef, useEffect } from \"react\";\n\nimport PopupContainer from \"./popup/PopupContainer\";\nimport Toolbar from \"./toolbar/Toolbar\";\nimport Camera from \"./camera/Camera\";\nimport CameraOnly from \"./camera-only/CameraOnly\";\nimport Canvas from \"./canvas/Canvas\";\nimport Countdown from \"./countdown/Countdown\";\nimport Modal from \"./modal/Modal\";\nimport Warning from \"./warning/Warning\";\n\nimport Region from \"./region/Region\";\n\n// Using ShadowDOM\nimport root from \"react-shadow\";\n\n// Import styles raw to add into the ShadowDOM\nimport styles from \"!raw-loader!./styles/app.css\";\n\nimport ZoomContainer from \"./utils/ZoomContainer\";\nimport BlurTool from \"./utils/BlurTool\";\nimport CursorModes from \"./utils/CursorModes\";\n\nimport { contentStateContext } from \"./context/ContentState\";\n\nimport { startClickTracking } from \"./cursor/trackClicks\";\n\nconst RecordingLoader = () => {\n  const label = chrome.i18n.getMessage(\"preparingLabel\") || \"Preparing...\";\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        inset: 0,\n        background: \"rgba(0, 0, 0, 0.5)\",\n        backdropFilter: \"blur(20px) saturate(180%)\",\n        WebkitBackdropFilter: \"blur(20px) saturate(180%)\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        zIndex: 99999999999,\n      }}\n      aria-label=\"Loading overlay\"\n      role=\"alert\"\n    >\n      <div\n        style={{\n          background: \"rgba(255, 255, 255, 0.15)\",\n          border: \"1px solid rgba(255, 255, 255, 0.2)\",\n          borderRadius: 20,\n          padding: 40,\n          width: 160,\n          height: 160,\n          boxShadow: `\n        0 8px 32px 0 rgba(0, 0, 0, 0.1),\n        0 0 0 1px rgba(255, 255, 255, 0.05),\n        inset 0 1px 0 rgba(255, 255, 255, 0.1)\n      `,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily:\n            'Satoshi-Medium, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif',\n          userSelect: \"none\",\n          animation: \"fadeIn 0.3s ease-out\",\n        }}\n      >\n        <div\n          style={{\n            width: 60,\n            height: 60,\n            border: \"3px solid rgba(255, 255, 255, 0.2)\",\n            borderTop: \"3px solid rgba(255, 255, 255, 0.8)\",\n            borderRadius: \"50%\",\n            animation: \"spin 1s linear infinite\",\n          }}\n        />\n        <div\n          style={{\n            marginTop: 20,\n            fontSize: 15,\n            fontWeight: 500,\n            color: \"#FFFFFF\",\n            textAlign: \"center\",\n            letterSpacing: \"-0.01em\",\n          }}\n        >\n          {label}\n        </div>\n        <style>\n          {`\n        @keyframes spin {\n          0% { transform: rotate(0deg); }\n          100% { transform: rotate(360deg); }\n        }\n        @keyframes fadeIn {\n          0% { opacity: 0; transform: scale(0.95); }\n          100% { opacity: 1; transform: scale(1); }\n        }\n        @keyframes pulse {\n          0%, 100% { opacity: 0.8; }\n          50% { opacity: 1; }\n        }\n      `}\n        </style>\n      </div>\n    </div>\n  );\n};\n\nconst Wrapper = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const shadowRef = useRef(null);\n  const parentRef = useRef(null);\n  const permissionsRef = useRef(null);\n  const regionCaptureRef = useRef(null);\n  const contentStateRef = useRef(contentState);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  useEffect(() => {\n    if (!parentRef.current) return;\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      parentRef: parentRef.current,\n    }));\n  }, [parentRef.current]);\n\n  useEffect(() => {\n    if (!shadowRef.current) return;\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      shadowRef: shadowRef.current,\n    }));\n  }, [shadowRef.current]);\n\n  useEffect(() => {\n    if (!regionCaptureRef.current) return;\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionCaptureRef: regionCaptureRef.current,\n    }));\n  }, [regionCaptureRef.current]);\n\n  useEffect(() => {\n    if (contentState.permissionsChecked) return;\n    if (!permissionsRef.current) return;\n    if (!contentState.showExtension) return;\n    if (!contentState.permissionsLoaded) return;\n\n    permissionsRef.current.contentWindow.postMessage(\n      {\n        type: \"screenity-get-permissions\",\n      },\n      \"*\"\n    );\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      permissionsChecked: true,\n    }));\n  }, [\n    permissionsRef.current,\n    contentState.showExtension,\n    contentState.permissionsLoaded,\n  ]);\n\n  useEffect(() => {\n    let stopTracking = null;\n\n    // Start tracking clicks only when recording starts\n    if (contentState.recording) {\n      stopTracking = startClickTracking(\n        contentState.customRegion,\n        contentState.regionWidth,\n        contentState.regionHeight,\n        contentState.regionX,\n        contentState.regionY,\n        contentStateRef\n      );\n    }\n\n    return () => {\n      stopTracking?.();\n    };\n  }, [\n    contentState.recording,\n    contentState.customRegion,\n    contentState.regionWidth,\n    contentState.regionHeight,\n    contentState.regionX,\n    contentState.regionY,\n  ]);\n\n  return (\n    <div ref={parentRef}>\n      {contentState.showExtension && (\n        <iframe\n          style={{\n            // all: \"unset\",\n            display: \"none\",\n            visibility: \"hidden\",\n          }}\n          ref={permissionsRef}\n          src={chrome.runtime.getURL(\"permissions.html\")}\n          allow=\"camera *; microphone *\"\n        ></iframe>\n      )}\n      {contentState.hasOpenedBefore && (\n        <iframe\n          style={{\n            // all: \"unset\",\n            display: \"none\",\n            visibility: \"hidden\",\n          }}\n          ref={regionCaptureRef}\n          src={\n            contentState.isSubscribed\n              ? chrome.runtime.getURL(\"cloudrecorder.html?injected=true\")\n              : chrome.runtime.getURL(\"region.html\")\n          }\n          allow=\"camera *; microphone *; display-capture *\"\n        ></iframe>\n      )}\n\n      {contentState.zoomEnabled && <ZoomContainer />}\n      <BlurTool />\n      {contentState.showExtension || contentState.recording ? (\n        <div>\n          {!contentState.recording &&\n            !contentState.drawingMode &&\n            !contentState.blurMode && (\n              <div\n                style={{\n                  // all: \"unset\",\n                  width: \"100%\",\n                  height: \"100%\",\n                  zIndex: 999999999,\n                  pointerEvents:\n                    contentState.pendingRecording ||\n                    contentState.preparingRecording\n                      ? \"none\"\n                      : \"all\",\n                  position: \"fixed\",\n                  background:\n                    window.location.href.indexOf(\n                      chrome.runtime.getURL(\"setup.html\")\n                    ) === -1 &&\n                    window.location.href.indexOf(\n                      chrome.runtime.getURL(\"playground.html\")\n                    ) === -1 &&\n                    !contentState.pendingRecording &&\n                    !contentState.preparingRecording\n                      ? \"rgba(0,0,0,0.15)\"\n                      : \"rgba(0,0,0,0)\",\n                  top: 0,\n                  left: 0,\n                }}\n                onClick={() => {\n                  const onboardingActive =\n                    document.documentElement.classList.contains(\n                      \"screenity-driver-active\"\n                    ) || Boolean(document.querySelector(\".driver-overlay\"));\n                  if (onboardingActive) return;\n\n                  if (\n                    window.location.href.indexOf(\n                      chrome.runtime.getURL(\"setup.html\")\n                    ) === -1 &&\n                    window.location.href.indexOf(\n                      chrome.runtime.getURL(\"playground.html\")\n                    ) === -1 &&\n                    !contentState.pendingRecording &&\n                    !contentState.customRegion\n                  ) {\n                    setContentState((prevContentState) => ({\n                      ...prevContentState,\n                      showExtension: false,\n                      showPopup: false,\n                    }));\n                  }\n                }}\n              ></div>\n            )}\n          <Canvas />\n          <CursorModes />\n          <root.div\n            className=\"root-container\"\n            id=\"screenity-root-container\"\n            style={{\n              display: \"block\",\n              width: \"100%\",\n              height: \"100%\",\n              position: \"absolute\",\n              pointerEvents: \"none\",\n              left: \"0px\",\n              top: \"0px\",\n              zIndex: 9999999999,\n              // Isolation: prevent host-page inherited typography from\n              // leaking through the shadow-DOM boundary.\n              fontFamily: \"'Satoshi-Medium', sans-serif\",\n              fontSize: \"16px\",\n              lineHeight: \"normal\",\n              letterSpacing: \"normal\",\n              wordSpacing: \"normal\",\n              textTransform: \"none\",\n              textIndent: \"0\",\n              textAlign: \"left\",\n              color: \"#29292F\",\n              direction: \"ltr\",\n              whiteSpace: \"normal\",\n              fontStyle: \"normal\",\n              fontVariant: \"normal\",\n              fontWeight: \"normal\",\n            }}\n            ref={shadowRef}\n          >\n            <div className=\"container\">\n              <Warning />\n              {contentState.recordingType === \"region\" &&\n                contentState.customRegion && <Region />}\n              {shadowRef.current && <Modal shadowRef={shadowRef} />}\n              {contentState.preparingRecording && (\n                <RecordingLoader />\n              )}\n              <Countdown />\n              {contentState.recordingType != \"camera\" &&\n                !contentState.onboarding &&\n                !(\n                  contentState.isSubscribed === false &&\n                  contentState.isLoggedIn === true\n                ) &&\n                !(!contentState.isLoggedIn && contentState.wasLoggedIn) && (\n                  <Camera shadowRef={shadowRef} />\n                )}\n              {contentState.recordingType === \"camera\" && (\n                <CameraOnly shadowRef={shadowRef} />\n              )}\n              {!(contentState.hideToolbar && contentState.hideUI) &&\n                !contentState.onboarding &&\n                !(\n                  contentState.isSubscribed === false &&\n                  contentState.isLoggedIn === true\n                ) &&\n                !(!contentState.isLoggedIn && contentState.wasLoggedIn) && (\n                  <Toolbar />\n                )}\n              {contentState.showPopup && (\n                <PopupContainer shadowRef={shadowRef} />\n              )}\n            </div>\n            <style type=\"text/css\">{styles}</style>\n          </root.div>\n        </div>\n      ) : (\n        <div></div>\n      )}\n    </div>\n  );\n};\n\nexport default Wrapper;\n"
  },
  {
    "path": "src/pages/Content/camera/Camera.jsx",
    "content": "import React, { useContext, useEffect } from \"react\";\n\nimport CameraWrap from \"./layout/CameraWrap\";\n\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst Camera = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <div\n      className=\"camera-page\"\n      style={{\n        visibility:\n          (contentState.isSubscribed &&\n            (!contentState.instantMode || contentState.multiMode)) ||\n          !contentState.recording ||\n          contentState.onboarding\n            ? \"hidden\"\n            : \"visible\",\n        pointerEvents:\n          (contentState.isSubscribed &&\n            (!contentState.instantMode || contentState.multiMode)) ||\n          !contentState.recording ||\n          contentState.onboarding\n            ? \"none\"\n            : \"auto\",\n      }}\n    >\n      {contentState.defaultVideoInput != \"none\" &&\n        contentState.cameraActive && <CameraWrap shadowRef={props.shadowRef} />}\n    </div>\n  );\n};\n\nexport default Camera;\n"
  },
  {
    "path": "src/pages/Content/camera/components/ResizeHandle.jsx",
    "content": "import React from \"react\";\n\nimport { CameraResizeIcon } from \"../../toolbar/components/SVG\";\n\nconst ResizeHandle = ({ position }) => {\n  return (\n    <div className=\"camera-resize\">\n      <CameraResizeIcon />\n    </div>\n  );\n};\n\nexport default ResizeHandle;\n"
  },
  {
    "path": "src/pages/Content/camera/layout/CameraToolbar.jsx",
    "content": "import React, { useState, useEffect, useContext } from \"react\";\n\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\nimport TooltipWrap from \"../../toolbar/components/TooltipWrap\";\n\nimport { CameraCloseIcon, Pip } from \"../../toolbar/components/SVG\";\n\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst CameraToolbar = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <Toolbar.Root className=\"camera-toolbar\">\n      <Toolbar.Button\n        className=\"CameraToolbarButton\"\n        onClick={() => {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            cameraActive: false,\n          }));\n          chrome.storage.local.set({ cameraActive: false });\n        }}\n      >\n        <CameraCloseIcon />\n      </Toolbar.Button>\n      {contentState.recording && contentState.surface === \"monitor\" && (\n        <TooltipWrap\n          content={chrome.i18n.getMessage(\"togglePictureinPictureModeTooltip\")}\n        >\n          <Toolbar.Button\n            className=\"CameraToolbarButton CameraMore\"\n            onClick={() => {\n              chrome.runtime.sendMessage({ type: \"toggle-pip\" });\n            }}\n          >\n            <Pip />\n          </Toolbar.Button>\n        </TooltipWrap>\n      )}\n    </Toolbar.Root>\n  );\n};\n\nexport default CameraToolbar;\n"
  },
  {
    "path": "src/pages/Content/camera/layout/CameraWrap.jsx",
    "content": "import React, {\n  useEffect,\n  useContext,\n  useRef,\n  useState,\n  useLayoutEffect,\n} from \"react\";\n\nimport { Rnd } from \"react-rnd\";\n\nimport { contentStateContext } from \"../../context/ContentState\";\n\nimport CameraToolbar from \"./CameraToolbar\";\nimport ResizeHandle from \"../components/ResizeHandle\";\n\nconst CameraWrap = (props) => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n  const cameraRef = React.useRef();\n  const [cx, setCx] = useState(200);\n  const [cy, setCy] = useState(200);\n  const [w, setW] = useState(200);\n  const [h, setH] = useState(200);\n\n  const updateUIPosition = () => {\n    const ref =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\");\n    const circleCenterX =\n      ref.getBoundingClientRect().left + ref.getBoundingClientRect().width / 2;\n    const circleCenterY =\n      ref.getBoundingClientRect().top + ref.getBoundingClientRect().height / 2;\n    const circleRadius = ref.getBoundingClientRect().width / 2;\n    const squareBottomRightX =\n      ref.getBoundingClientRect().left + ref.getBoundingClientRect().width;\n    const squareBottomRightY =\n      ref.getBoundingClientRect().top + ref.getBoundingClientRect().height;\n    const handle =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-resize\");\n    const toolbar =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-toolbar\");\n\n    const c = Math.sqrt(\n      Math.pow(circleCenterX - squareBottomRightX, 2) +\n        Math.pow(circleCenterY - squareBottomRightY, 2)\n    );\n    const a = circleRadius / Math.sqrt(2);\n    const r = (c + Math.sqrt(c ** 2 + 16 * a ** 2)) / 4;\n\n    const x = r - r / Math.sqrt(2);\n    const y = r - r / Math.sqrt(2);\n\n    handle.style.bottom = `${y - handle.getBoundingClientRect().width / 2}px`;\n    handle.style.right = `${x - handle.getBoundingClientRect().height / 2}px`;\n    toolbar.style.top = `${y - toolbar.getBoundingClientRect().width / 2}px`;\n    toolbar.style.left = `${x - toolbar.getBoundingClientRect().height / 2}px`;\n  };\n\n  const saveDimensions = () => {\n    const ref =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\");\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      cameraDimensions: {\n        size: ref.getBoundingClientRect().width,\n        x: ref.getBoundingClientRect().x,\n        y: ref.getBoundingClientRect().y,\n      },\n    }));\n    chrome.storage.local.set({\n      cameraDimensions: {\n        size: ref.getBoundingClientRect().width,\n        x: ref.getBoundingClientRect().x,\n        y: ref.getBoundingClientRect().y,\n      },\n    });\n  };\n\n  useEffect(() => {\n    if (!cameraRef.current) return;\n    if (!props.shadowRef.current.shadowRoot.querySelector(\".camera-resize\"))\n      return;\n    if (!props.shadowRef.current.shadowRoot.querySelector(\".camera-toolbar\"))\n      return;\n\n    updateUIPosition();\n  }, [cameraRef.current]);\n\n  // I need to make sure the camera is never offscreen (if the user resizes the window)\n  useLayoutEffect(() => {\n    const updateCameraPosition = () => {\n      if (\n        !props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\")\n      )\n        return;\n      const ref =\n        props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\");\n      let xpos = cameraRef.current.getDraggablePosition().x;\n      let ypos = cameraRef.current.getDraggablePosition().y;\n\n      // Width and height of camera\n      const width = ref.getBoundingClientRect().width;\n      const height = ref.getBoundingClientRect().height;\n\n      const { innerWidth, innerHeight } = window;\n\n      // Keep camera positioned relative to the bottom and right of the screen, proportionally\n      if (xpos + width > innerWidth) {\n        xpos = innerWidth - width;\n      }\n      if (ypos + height > innerHeight) {\n        ypos = innerHeight - height;\n      }\n\n      cameraRef.current.updatePosition({ x: xpos, y: ypos });\n\n      saveDimensions();\n    };\n\n    updateCameraPosition();\n\n    window.addEventListener(\"resize\", updateCameraPosition);\n\n    return () => {\n      window.removeEventListener(\"resize\", updateCameraPosition);\n    };\n  }, []);\n\n  return (\n    <div\n      style={{\n        visibility:\n          !contentState.recording && !contentState.pendingRecording\n            ? \"visible\"\n            : (!contentState.pipEnded &&\n                contentState.surface === \"monitor\" &&\n                (contentState.pendingRecording || contentState.recording)) ||\n              (contentState.isSubscribed &&\n                (!contentState.instantMode || contentState.multiMode)) ||\n              contentState.onboarding\n            ? \"hidden\"\n            : \"visible\",\n        pointerEvents:\n          !contentState.recording && !contentState.pendingRecording\n            ? \"auto\"\n            : (!contentState.pipEnded &&\n                contentState.surface === \"monitor\" &&\n                (contentState.pendingRecording || contentState.recording)) ||\n              (contentState.isSubscribed &&\n                (!contentState.instantMode || contentState.multiMode)) ||\n              contentState.onboarding\n            ? \"none\"\n            : \"auto\",\n      }}\n    >\n      <Rnd\n        default={{\n          x: contentState.cameraDimensions.x,\n          y: contentState.cameraDimensions.y,\n          width: contentState.cameraDimensions.size,\n          height: contentState.cameraDimensions.size,\n        }}\n        ref={cameraRef}\n        className=\"camera-draggable\"\n        dragHandleClassName=\"camera-grab\"\n        resizeHandleComponent={{\n          bottomRight: <ResizeHandle />,\n        }}\n        minHeight={150}\n        minWidth={150}\n        enableResizing={{\n          bottom: false,\n          bottomRight: true,\n          bottomLeft: false,\n          left: false,\n          right: false,\n          top: false,\n          topRight: false,\n          topLeft: false,\n        }}\n        onResize={(e, direction, ref, delta, position) => {\n          updateUIPosition();\n        }}\n        onResizeStop={(e, direction, ref, delta, position) => {\n          saveDimensions();\n        }}\n        onDragStop={(node, x, y) => {\n          saveDimensions();\n        }}\n        lockAspectRatio={1}\n        bounds={\"window\"}\n      >\n        <div className=\"camera-grab\"></div>\n        <CameraToolbar />\n        <iframe\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            borderRadius: \"50%\",\n            outline: \"none\",\n            border: \"none\",\n            pointerEvents: \"none\",\n          }}\n          className={contentState.cameraFlipped ? \"camera-flipped\" : \"\"}\n          src={chrome.runtime.getURL(\"camera.html\")}\n          allow=\"camera; microphone\"\n        ></iframe>\n      </Rnd>\n    </div>\n  );\n};\n\nexport default CameraWrap;\n"
  },
  {
    "path": "src/pages/Content/camera/styles/_Camera.scss",
    "content": "@use '../../styles/_variables' as *;\n@use './layout/CameraWrap';\n@use './layout/CameraToolbar';\n@use './components/ResizeHandle';"
  },
  {
    "path": "src/pages/Content/camera/styles/components/_ResizeHandle.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.camera-resize {\n\tposition: absolute;\n\tbottom: 20px;\n\tright: 20px;\n\tz-index: $z-index-max;\n\theight: 28px;\n\twidth: 28px;\n\tborder-radius: 50%;\n\tbackground-color: rgba(30, 30, 30, .8);\n\tbox-shadow: 0 2px 10px rgba(0, 0, 0, .15);\n\tborder: 3px solid rgba(255, 255, 255, .2);\n\tbackdrop-filter: blur(10px);\n\talign-items: center;\n\tbox-sizing: border-box;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\topacity: 0;\n\n\tsvg {\n\t\tcolor: #9797A4;\n\t\ttext-align: center;\n\t\tmargin: auto;\n\t\tdisplay: block;\n\t}\n}"
  },
  {
    "path": "src/pages/Content/camera/styles/layout/_CameraToolbar.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.camera-toolbar {\n\tdisplay: flex;\n\talign-items: center;\n  padding-left: 4px;\n\tpadding-right: 4px;\n\ttransition: opacity .25s cubic-bezier(.61,.11,.08,.96);\n\tmin-width: max-content;\n\tbackground-color: rgba(30, 30, 30, .8);\n\tbox-shadow: 0 2px 10px rgba(0, 0, 0, .15);\n\theight: 28px;\n\tposition: absolute;\n\tleft: 10px;\n\ttop: 10px;\n\tborder-radius: $container-border-radius;\n\tbackdrop-filter: blur(10px);\n\tz-index: $z-index-max;\n\topacity: 0;\n\tborder: 3px solid rgba(255, 255, 255, .2);\n}\n.camera-draggable:hover {\n\t.camera-toolbar, .camera-resize {\n\t\topacity: 1!important;\n\t}\n}\n.camera-toolbar:hover, .camera-resize:hover {\n\topacity: 1!important;\n}\n\n.CameraToolbarSeparator {\n  width: 1px;\n\theight: 18px;\n  background-color: rgba(255, 255, 255, .3);\n  margin: 0 4px;\n}\n.CameraToggleItem, .CameraToolbarButton {\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tcolor: #000;\n  height: 22px;\n\twidth: 22px;\n\ttext-align: center;\n  font-size: 13px;\n  line-height: 1;\n\tborder-radius: 50%;\n\ttransition: background-color .25s ease-in-out;\n\tbackground-color: rgba(124, 139, 165, 0);\n\n\tsvg {\n\t\tcolor: $color-icon;\n\t}\n\n\t&:hover {\n\t\tbackground-color: rgba(124, 139, 165, 0.2)!important;\n\t\tcursor: pointer;\n\t}\n\n\t&:disabled {\n\t\topacity: 0.5;\n\t\tpointer-events: none;\n\t}\n\n\t&[data-state='on'] {\n\t\tcolor: #FFF;\n\n\t\tsvg {\n\t\t\tcolor: #FFF;\n\t\t}\n\n\t}\n}\n\n\n.CameraToggleItem:hover, .CameraToggleButton:hover {\n\tcursor: pointer;\n}\n.CameraToggleItem:focus-visible, .CameraToggleButton:focus-visible {\n  position: relative;\n  box-shadow: $focus-border;\n}\n\n.CameraToggleGroup {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.CameraToggleGroup, .CameraToolbarSeparator {\n\tdisplay: none;\n}"
  },
  {
    "path": "src/pages/Content/camera/styles/layout/_CameraWrap.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.camera-draggable {\n\twidth: 100%;\n\theight: 100%;\n\ttransform-origin: left top;\n\tborder-radius: 50%;\n}\n.camera-grab {\n\twidth: 100%;\n\theight: 100%;\n\tposition: absolute;\n\tborder-radius: 50%;\n\tz-index: 99999999!important;\n\tcursor: grab;\n}\n.camera-flipped {\n\ttransform: scaleX(-1);\n}"
  },
  {
    "path": "src/pages/Content/camera-only/CameraOnly.jsx",
    "content": "import React, { useContext } from \"react\";\n\nimport CameraWrap from \"./layout/CameraWrap\";\n\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst CameraOnly = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <div className=\"camera-page\">\n      {contentState.defaultVideoInput != \"none\" &&\n        contentState.cameraActive &&\n        contentState.recordingType === \"camera\" && (\n          <CameraWrap shadowRef={props.shadowRef} />\n        )}\n    </div>\n  );\n};\n\nexport default CameraOnly;\n"
  },
  {
    "path": "src/pages/Content/camera-only/layout/CameraWrap.jsx",
    "content": "import React, { useEffect, useContext, useRef, useState } from \"react\";\n\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst CameraWrap = (props) => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n\n  return (\n    <div>\n      <iframe\n        style={{\n          width: \"80vw\",\n          outline: \"none\",\n          border: \"none\",\n          pointerEvents: \"none\",\n          zIndex: 0,\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          right: 0,\n          bottom: 0,\n          margin: \"auto\",\n        }}\n        className={contentState.cameraFlipped ? \"camera-flipped\" : \"\"}\n        src={chrome.runtime.getURL(\"camera.html\")}\n        allow=\"camera; microphone\"\n      ></iframe>\n    </div>\n  );\n};\n\nexport default CameraWrap;\n"
  },
  {
    "path": "src/pages/Content/camera-only/styles/_CameraOnly.scss",
    "content": "@use '../../styles/_variables' as *;"
  },
  {
    "path": "src/pages/Content/canvas/Canvas.jsx",
    "content": "import React, { useContext } from \"react\";\n\nimport CanvasWrap from \"./layout/CanvasWrap\";\n\nconst Canvas = () => {\n  return (\n    <div className=\"canvas-page\">\n      <CanvasWrap />\n    </div>\n  );\n};\n\nexport default Canvas;\n"
  },
  {
    "path": "src/pages/Content/canvas/layout/CanvasWrap.jsx",
    "content": "import React, { useEffect, useRef, useContext } from \"react\";\nimport { fabric } from \"fabric\";\n\nimport { contentStateContext } from \"../../context/ContentState\";\n\nimport CustomControls from \"../modules/CustomControls\";\n\nimport ArrowTool from \"../modules/ArrowTool\";\nimport EraserTool from \"../modules/EraserTool\";\nimport ShapeTool from \"../modules/ShapeTool\";\nimport TextTool from \"../modules/TextTool\";\nimport PenTool from \"../modules/PenTool\";\nimport SelectTool from \"../modules/SelectTool\";\n\nimport {\n  undoCanvas,\n  redoCanvas,\n  saveCanvas,\n  checkChanges,\n} from \"../modules/History\";\n\nconst CanvasWrap = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const contentStateRef = useRef(null);\n  const canvasContainer = useRef();\n  const canvasRef = useRef();\n  const fabricRef = useRef();\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  // INIT\n  useEffect(() => {\n    if (!canvasRef.current) return;\n    if (fabricRef.current) return;\n\n    const canvas = new fabric.Canvas(\"canvas-screenity\", {\n      perPixelTargetFind: true,\n    });\n    fabricRef.current = canvas;\n\n    canvas.getContext(\"2d\", { willReadFrequently: true });\n\n    // Set width and height of canvas to full size of document\n    canvas.setWidth(window.document.body.offsetWidth);\n\n    // set max height of 2000px\n    //canvas.setHeight(Math.min(window.document.body.offsetHeight, 2500));\n    // set height to viewport\n    canvas.setHeight(window.innerHeight);\n\n    canvas.renderAll();\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      canvas: canvas,\n    }));\n\n    CustomControls(canvas);\n    saveCanvas(\n      {\n        ...contentState,\n        canvas: canvas,\n      },\n      setContentState,\n    );\n  }, []);\n\n  useEffect(() => {\n    if (!fabricRef.current) return;\n\n    const resizeCanvas = () => {\n      fabricRef.current.setWidth(window.document.body.offsetWidth);\n      fabricRef.current.setHeight(window.innerHeight);\n      fabricRef.current.renderAll();\n    };\n\n    window.addEventListener(\"resize\", resizeCanvas);\n\n    return () => {\n      window.removeEventListener(\"resize\", resizeCanvas);\n    };\n  }, []);\n\n  useEffect(() => {\n    const canvas = fabricRef.current;\n    if (!canvas) return;\n\n    const tool = contentState.tool;\n\n    const shouldDraw =\n      contentState.drawingMode && (tool === \"pen\" || tool === \"highlighter\");\n\n    canvas.isDrawingMode = shouldDraw;\n\n    // Important: when leaving pen/highlighter, clear any in-progress brush stroke\n    if (!shouldDraw && canvas.freeDrawingBrush) {\n      const brush = canvas.freeDrawingBrush;\n      if (Array.isArray(brush._points)) brush._points.length = 0;\n      if (Array.isArray(brush.points)) brush.points.length = 0;\n      if (typeof brush._reset === \"function\") brush._reset();\n    }\n\n    canvas.requestRenderAll();\n  }, [contentState.tool, contentState.drawingMode]);\n\n  useEffect(() => {\n    const canvas = fabricRef.current;\n    if (!canvas) return;\n\n    // Only discard selection when leaving select (or when entering a drawing tool)\n    if (contentState.tool !== \"select\") {\n      canvas.discardActiveObject();\n      canvas.requestRenderAll();\n    }\n  }, [contentState.tool]);\n\n  const panXRef = useRef(window.scrollX);\n  const panYRef = useRef(window.scrollY);\n\n  useEffect(() => {\n    if (!fabricRef.current) return;\n\n    panXRef.current = window.scrollX;\n    panYRef.current = window.scrollY;\n\n    // Function to update canvas panning\n    const updateCanvasPan = () => {\n      fabricRef.current.setZoom(fabricRef.current.getZoom());\n      fabricRef.current.absolutePan({\n        x: panXRef.current,\n        y: panYRef.current,\n      });\n    };\n\n    // Event listener for window scroll\n    const handleScroll = () => {\n      // Update canvas pan position based on scroll position\n      panXRef.current = window.scrollX;\n      panYRef.current = window.scrollY;\n      updateCanvasPan();\n    };\n\n    updateCanvasPan();\n\n    // Attach scroll event listener\n    window.addEventListener(\"scroll\", handleScroll, { passive: true });\n\n    // Cleanup function\n    return () => {\n      // Remove scroll event listener\n      window.removeEventListener(\"scroll\", handleScroll);\n\n      // Reset canvas pan position\n      panXRef.current = 0;\n      panYRef.current = 0;\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!fabricRef.current) return;\n\n    const canvas = fabricRef.current;\n\n    // Pass a ref getter instead of snapshot state\n    const arrowDrawing = ArrowTool(\n      canvas,\n      contentStateRef,\n      setContentState,\n      saveCanvas,\n    );\n    const eraserDrawing = EraserTool(canvas, contentStateRef, setContentState);\n    const shapeDrawing = ShapeTool(\n      canvas,\n      contentStateRef,\n      setContentState,\n      saveCanvas,\n    );\n    const textDrawing = TextTool(\n      canvas,\n      contentStateRef,\n      setContentState,\n      saveCanvas,\n    );\n    const penDrawing = PenTool(\n      canvas,\n      contentStateRef,\n      setContentState,\n      saveCanvas,\n    );\n\n    return () => {\n      arrowDrawing.removeEventListeners();\n      eraserDrawing.removeEventListeners();\n      shapeDrawing.removeEventListeners();\n      textDrawing.removeEventListeners();\n      penDrawing.removeEventListeners();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!fabricRef.current) return;\n\n    const canvas = fabricRef.current;\n\n    const selection = SelectTool(canvas, contentStateRef, setContentState);\n    const objectChanges = checkChanges(\n      canvas,\n      contentStateRef,\n      setContentState,\n    );\n\n    return () => {\n      selection.removeEventListeners();\n      objectChanges.removeEventListeners();\n    };\n  }, []);\n\n  useEffect(() => {\n    // Prevent selecting elements unless in select mode\n    if (!fabricRef.current) return;\n    if (contentState.tool !== \"select\") {\n      // De-select all objects on canvas\n      fabricRef.current.discardActiveObject();\n\n      fabricRef.current.selection = false;\n      fabricRef.current.forEachObject((obj) => {\n        obj.selectable = false;\n      });\n      fabricRef.current.renderAll();\n    } else {\n      fabricRef.current.selection = true;\n      fabricRef.current.forEachObject((obj) => {\n        obj.selectable = true;\n      });\n      fabricRef.current.renderAll();\n    }\n  }, [contentState.tool]);\n\n  useEffect(() => {\n    const onKeyDown = (event) => {\n      const state = contentStateRef.current;\n      if (!state) return;\n\n      if (\n        (event.ctrlKey || event.metaKey) &&\n        event.key.toLowerCase() === \"z\" &&\n        !event.shiftKey\n      ) {\n        undoCanvas(state, setContentState);\n      }\n      if (\n        (event.ctrlKey || event.metaKey) &&\n        (event.key.toLowerCase() === \"y\" ||\n          (event.shiftKey && event.key.toLowerCase() === \"z\"))\n      ) {\n        redoCanvas(state, setContentState);\n      }\n    };\n\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, []);\n\n  useEffect(() => {\n    if (!fabricRef.current) return;\n\n    // De-select all objects on canvas\n    fabricRef.current.discardActiveObject();\n    fabricRef.current.requestRenderAll();\n  }, [contentState.drawingMode]);\n\n  return (\n    <div\n      style={\n        !contentState.drawingMode ||\n        (contentState.hideToolbar && contentState.hideUI)\n          ? { all: \"unset\", pointerEvents: \"none\" }\n          : { all: \"unset\", pointerEvents: \"all\" }\n      }\n    >\n      <div\n        className=\"canvas-container\"\n        ref={canvasContainer}\n        id=\"canvas-wrapper-screenity\"\n        style={{\n          height: \"100vh\",\n          width: \"100vw\",\n          zIndex:\n            contentState.drawingMode && !contentState.recording\n              ? 99999999999\n              : 99999999,\n        }}\n      >\n        <canvas id=\"canvas-screenity\" ref={canvasRef} className=\"canvas\" />\n      </div>\n    </div>\n  );\n};\n\nexport default CanvasWrap;\n"
  },
  {
    "path": "src/pages/Content/canvas/layout/TextToolbar.jsx",
    "content": "import React, { useRef, useContext, useEffect, useState } from \"react\";\n\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\nimport * as Select from \"@radix-ui/react-select\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst TextToolbar = (props) => {\n  const toolbarRef = useRef(null);\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [left, setLeft] = useState(0);\n  const [top, setTop] = useState(0);\n\n  useEffect(() => {\n    if (!contentState.canvas) return;\n\n    const canvas = contentState.canvas;\n    const toolbar = toolbarRef.current;\n\n    function positionToolbarOnTextbox(textbox) {\n      // place the toolbar on top of the textbox, centered, with a vertical offset\n      const textCoords = textbox.getBoundingRect();\n      const textOffset = textCoords.left;\n      const textWidth = textCoords.width;\n      const textHeight = textCoords.height;\n      const textTop = textCoords.top;\n      const textLeft = textCoords.left;\n      const textCenter = textLeft + textWidth / 2;\n      const textCenterOffset = textCenter;\n      const toolbarWidth = toolbar.clientWidth;\n      const toolbarHeight = toolbar.clientHeight;\n      const toolbarLeft = textCenterOffset - toolbarWidth / 2;\n      const toolbarTop = textTop - toolbarHeight - 10;\n      setLeft(toolbarLeft);\n      setTop(toolbarTop);\n    }\n\n    function hideToolbar() {\n      toolbar.style.display = \"none\";\n    }\n\n    canvas.on(\"selection:created\", function (e) {\n      const selectedObject = canvas.getActiveObject();\n      if (selectedObject === null || selectedObject === undefined) return;\n\n      if (selectedObject.type === \"textbox\") {\n        positionToolbarOnTextbox(selectedObject);\n      }\n    });\n\n    canvas.on(\"selection:cleared\", function () {\n      hideToolbar();\n    });\n\n    canvas.on(\"object:moving\", function (e) {\n      const movedObject = e.target;\n\n      if (\n        movedObject.type === \"textbox\" &&\n        canvas.getActiveObject() === movedObject\n      ) {\n        positionToolbarOnTextbox(movedObject);\n      }\n    });\n\n    canvas.on(\"object:scaling\", function (e) {\n      const resizedObject = e.target;\n\n      if (\n        resizedObject.type === \"textbox\" &&\n        canvas.getActiveObject() === resizedObject\n      ) {\n        positionToolbarOnTextbox(resizedObject);\n      }\n    });\n  }, [contentState.canvas]);\n\n  return (\n    <div\n      className=\"text-toolbar\"\n      ref={toolbarRef}\n      style={{\n        left: left + \"px\",\n        top: top + \"px\",\n        position: \"absolute\",\n        backgroundColor: \"black\",\n        borderRadius: \"10px\",\n        padding: \"10px\",\n        color: \"white\",\n        display: \"block\",\n      }}\n    >\n      oioi\n    </div>\n  );\n};\n\nexport default TextToolbar;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/ArrowTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst createArrowLine = (x, y, color, strokeWidth) => {\n  return new fabric.Line([x, y, x, y], {\n    strokeWidth: (strokeWidth || 2) * 6,\n    stroke: color,\n    originX: \"center\",\n    originY: \"center\",\n    selectable: false,\n    evented: false,\n    id: \"arrowLine\",\n    objectCaching: false,\n  });\n};\n\nconst createArrowHead = (x, y, color, strokeWidth) => {\n  const size = (strokeWidth || 2) * 16;\n  return new fabric.Triangle({\n    width: size,\n    height: size,\n    left: x,\n    top: y,\n    fill: color,\n    originX: \"center\",\n    originY: \"center\",\n    selectable: false,\n    evented: false,\n    id: \"arrowHead\",\n    objectCaching: false,\n  });\n};\n\nconst createArrowCircle = (x, y, id) => {\n  return new fabric.Circle({\n    radius: 5,\n    fill: \"white\",\n    stroke: \"#0D99FF\",\n    strokeWidth: 2,\n    left: x,\n    top: y,\n    selectable: false,\n    evented: true,\n    id,\n    opacity: 0,\n    objectCaching: false,\n  });\n};\n\nconst createArrowLineControl = (x, y) => {\n  return new fabric.Line([x, y, x, y], {\n    strokeWidth: 2,\n    stroke: \"#0D99FF\",\n    originX: \"center\",\n    originY: \"center\",\n    selectable: false,\n    evented: false,\n    id: \"arrowLineControl\",\n    opacity: 0,\n    objectCaching: false,\n  });\n};\n\nconst ArrowTool = (canvas, contentStateRef, setContentState, saveCanvas) => {\n  const getState = () => contentStateRef.current;\n\n  let arrowPoints = [];\n  let arrowLine = null;\n  let arrowHead = null;\n  let arrowCircle1 = null;\n  let arrowCircle2 = null;\n  let arrowLineControl = null;\n\n  // --- endpoint drag handler (store ref so we can remove safely)\n  const onEndpointMouseDown = (e) => {\n    // Only when selecting / interacting with existing arrows\n    if (!e?.subTargets?.length) return;\n\n    const state = getState();\n    // Allow endpoint dragging even if tool != arrow, but only in drawingMode\n    if (!state?.drawingMode) return;\n\n    const hit = e.subTargets.find(\n      (obj) => obj?.id === \"arrowCircle1\" || obj?.id === \"arrowCircle2\",\n    );\n    if (!hit) return;\n\n    moveArrowCircle(canvas, hit, getState, saveCanvas, setContentState);\n  };\n\n  const moveArrowCircle = (\n    canvas,\n    arrowCircle,\n    getState,\n    saveCanvas,\n    setContentState,\n  ) => {\n    let isDown = true;\n\n    const group = arrowCircle.group;\n    const items = group._objects;\n\n    const arrowCircle1 = group._objects.find(\n      (item) => item.id === \"arrowCircle1\",\n    );\n    const arrowCircle2 = group._objects.find(\n      (item) => item.id === \"arrowCircle2\",\n    );\n    const arrowLine = group._objects.find((item) => item.id === \"arrowLine\");\n    const arrowHead = group._objects.find((item) => item.id === \"arrowHead\");\n    const arrowLineControl = group._objects.find(\n      (item) => item.id === \"arrowLineControl\",\n    );\n\n    arrowLineControl.set({ opacity: 0 });\n\n    // Ungroup temporarily so we can edit in canvas coords\n    group._restoreObjectsState();\n    canvas.remove(group);\n    items.forEach((item) => canvas.add(item));\n    canvas.requestRenderAll();\n\n    const updateGeometry = () => {\n      const x1 = arrowCircle1.left + 5;\n      const y1 = arrowCircle1.top + 5;\n      const x2 = arrowCircle2.left + 5;\n      const y2 = arrowCircle2.top + 5;\n\n      arrowLine.set({ x1, y1, x2, y2 });\n      arrowLineControl.set({ x1, y1, x2, y2 });\n\n      arrowLine.setCoords();\n      arrowLineControl.setCoords();\n\n      const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;\n      arrowHead.set({ angle: angle + 90, left: x2, top: y2 });\n      arrowHead.setCoords();\n    };\n\n    const onMove = (o) => {\n      if (!isDown) return;\n\n      const pointer = canvas.getPointer(o.e);\n      arrowCircle.set({ left: pointer.x - 5, top: pointer.y - 5 });\n      arrowCircle.setCoords();\n\n      updateGeometry();\n      canvas.requestRenderAll();\n    };\n\n    const onUp = () => {\n      if (!isDown) return;\n      isDown = false;\n\n      canvas.off(\"mouse:move\", onMove);\n      canvas.off(\"mouse:up\", onUp);\n\n      arrowLineControl.set({ opacity: 1 });\n\n      const newGroup = new fabric.Group(\n        [arrowLine, arrowHead, arrowLineControl, arrowCircle1, arrowCircle2],\n        {\n          selectable: true,\n          evented: true,\n          id: \"arrowGroup\",\n          hasControls: false,\n          hasBorders: false,\n          hasRotatingPoint: false,\n          subTargetCheck: true,\n          originX: \"left\",\n          originY: \"top\",\n          perPixelTargetFind: true,\n        },\n      );\n\n      canvas.add(newGroup);\n      canvas.remove(arrowLine);\n      canvas.remove(arrowHead);\n      canvas.remove(arrowCircle1);\n      canvas.remove(arrowCircle2);\n      canvas.remove(arrowLineControl);\n      canvas.requestRenderAll();\n\n      canvas.discardActiveObject();\n      canvas.requestRenderAll();\n\n      const state = getState();\n      saveCanvas({ ...state, tool: \"select\" }, setContentState);\n\n      canvas.setActiveObject(newGroup);\n      canvas.requestRenderAll();\n    };\n\n    canvas.on(\"mouse:move\", onMove);\n    canvas.on(\"mouse:up\", onUp);\n  };\n\n  // --- draw handlers\n  const onMouseDown = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"arrow\") return;\n    if (arrowPoints.length) return;\n\n    canvas.selection = false;\n    canvas.requestRenderAll();\n\n    const { x, y } = canvas.getPointer(o.e);\n    arrowPoints = [{ x, y }];\n\n    arrowLine = createArrowLine(x, y, state.color, state.strokeWidth);\n    arrowHead = createArrowHead(x, y, state.color, state.strokeWidth);\n    arrowCircle1 = createArrowCircle(x - 5, y - 5, \"arrowCircle1\");\n    arrowCircle2 = createArrowCircle(x - 5, y - 5, \"arrowCircle2\");\n    arrowLineControl = createArrowLineControl(x, y);\n\n    canvas.add(arrowLine);\n    canvas.add(arrowHead);\n    canvas.add(arrowLineControl);\n    canvas.add(arrowCircle1);\n    canvas.add(arrowCircle2);\n\n    canvas.requestRenderAll();\n  };\n\n  const onMouseMove = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"arrow\") return;\n    if (!arrowPoints.length) return;\n    if (\n      !arrowLine ||\n      !arrowHead ||\n      !arrowCircle1 ||\n      !arrowCircle2 ||\n      !arrowLineControl\n    )\n      return;\n\n    const { x, y } = canvas.getPointer(o.e);\n    const startX = arrowPoints[0].x;\n    const startY = arrowPoints[0].y;\n\n    arrowLine.set({ x1: startX, y1: startY, x2: x, y2: y });\n    arrowLineControl.set({ x1: startX, y1: startY, x2: x, y2: y });\n    arrowLine.setCoords();\n    arrowLineControl.setCoords();\n\n    const angle = (Math.atan2(y - startY, x - startX) * 180) / Math.PI;\n    arrowHead.set({ left: x, top: y, angle: angle + 90 });\n    arrowHead.setCoords();\n\n    arrowCircle1.set({ left: startX - 5, top: startY - 5 });\n\n    // keep your “slight offset” if you want, but simplest is:\n    arrowCircle2.set({ left: x - 5, top: y - 5 });\n\n    arrowCircle1.setCoords();\n    arrowCircle2.setCoords();\n\n    canvas.requestRenderAll();\n  };\n\n  const onMouseUp = () => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"arrow\") return;\n    if (!arrowPoints.length) return;\n\n    canvas.selection = true;\n    arrowPoints = [];\n\n    arrowCircle1.set({ opacity: 1 });\n    arrowCircle2.set({ opacity: 1 });\n    arrowLineControl.set({ opacity: 1 });\n\n    const group = new fabric.Group(\n      [arrowLine, arrowHead, arrowLineControl, arrowCircle1, arrowCircle2],\n      {\n        selectable: true,\n        evented: true,\n        id: \"arrowGroup\",\n        hasControls: false,\n        hasBorders: false,\n        hasRotatingPoint: false,\n        subTargetCheck: true,\n        originX: \"left\",\n        originY: \"top\",\n        perPixelTargetFind: true,\n      },\n    );\n\n    canvas.add(group);\n    canvas.remove(arrowLine);\n    canvas.remove(arrowHead);\n    canvas.remove(arrowCircle1);\n    canvas.remove(arrowCircle2);\n    canvas.remove(arrowLineControl);\n\n    canvas.requestRenderAll();\n\n    saveCanvas({ ...state, tool: \"select\" }, setContentState);\n    canvas.setActiveObject(group);\n    canvas.requestRenderAll();\n  };\n\n  // --- selection visibility handlers (keep refs for proper off)\n  const onBeforeSelectionCleared = () => {\n    const activeObject = canvas.getActiveObject();\n    if (activeObject && activeObject.id === \"arrowGroup\") {\n      activeObject._objects.forEach((obj) => {\n        if (\n          obj.id === \"arrowCircle1\" ||\n          obj.id === \"arrowCircle2\" ||\n          obj.id === \"arrowLineControl\"\n        ) {\n          obj.set({ opacity: 0 });\n        }\n      });\n      canvas.requestRenderAll();\n    }\n  };\n\n  const onSelectionCleared = () => {\n    canvas.getObjects().forEach((obj) => {\n      if (obj.type === \"group\") {\n        obj._objects.forEach((obj2) => {\n          if (\n            obj2.id === \"arrowCircle1\" ||\n            obj2.id === \"arrowCircle2\" ||\n            obj2.id === \"arrowLineControl\"\n          ) {\n            obj2.set({ opacity: 0 });\n          }\n        });\n      }\n    });\n    canvas.requestRenderAll();\n  };\n\n  const onSelectionChanged = () => {\n    const activeObject = canvas.getActiveObject();\n\n    if (activeObject && activeObject.id === \"arrowGroup\") {\n      activeObject._objects.forEach((obj) => {\n        if (\n          obj.id === \"arrowCircle1\" ||\n          obj.id === \"arrowCircle2\" ||\n          obj.id === \"arrowLineControl\"\n        ) {\n          obj.set({ opacity: 1 });\n        }\n      });\n      canvas.requestRenderAll();\n    }\n\n    if (activeObject && activeObject.type === \"activeSelection\") {\n      activeObject._objects.forEach((obj) => {\n        if (obj.id === \"arrowGroup\") {\n          obj._objects.forEach((obj2) => {\n            if (obj2.id === \"arrowLineControl\") obj2.set({ opacity: 1 });\n          });\n        }\n      });\n      canvas.requestRenderAll();\n    }\n  };\n\n  // attach\n  canvas.on(\"mouse:down\", onEndpointMouseDown); // endpoint dragging\n  canvas.on(\"mouse:down\", onMouseDown);\n  canvas.on(\"mouse:move\", onMouseMove);\n  canvas.on(\"mouse:up\", onMouseUp);\n\n  canvas.on(\"before:selection:cleared\", onBeforeSelectionCleared);\n  canvas.on(\"selection:cleared\", onSelectionCleared);\n  canvas.on(\"selection:created\", onSelectionChanged);\n  canvas.on(\"selection:updated\", onSelectionChanged);\n\n  return {\n    removeEventListeners: function () {\n      canvas.off(\"mouse:down\", onEndpointMouseDown);\n      canvas.off(\"mouse:down\", onMouseDown);\n      canvas.off(\"mouse:move\", onMouseMove);\n      canvas.off(\"mouse:up\", onMouseUp);\n\n      canvas.off(\"before:selection:cleared\", onBeforeSelectionCleared);\n      canvas.off(\"selection:cleared\", onSelectionCleared);\n      canvas.off(\"selection:created\", onSelectionChanged);\n      canvas.off(\"selection:updated\", onSelectionChanged);\n    },\n  };\n};\n\nexport default ArrowTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/CustomControls.jsx",
    "content": "import React from \"react\";\nimport { fabric } from \"fabric\";\n\nimport {\n  HandleControl,\n  RotateControl,\n  MiddleHandleControl,\n  MiddleHandleControlV,\n} from \"./../../images/popup/images.js\";\n\n// Custom controls for the canvas, with rounded square handles and a circular rotate handle\nconst CustomControls = (canvas) => {\n  fabric.Object.prototype.set({\n    transparentCorners: false,\n    borderColor: \"#0D99FF\",\n    cornerColor: \"#FFF\",\n    borderScaleFactor: 2,\n    cornerStyle: \"circle\",\n    cornerStrokeColor: \"#0D99FF\",\n    borderOpacityWhenMoving: 1,\n  });\n\n  fabric.Textbox.prototype.set({\n    transparentCorners: false,\n    borderColor: \"#0D99FF\",\n    cornerColor: \"#FFF\",\n    borderScaleFactor: 2,\n    cornerStyle: \"circle\",\n    cornerStrokeColor: \"#0D99FF\",\n    borderOpacityWhenMoving: 1,\n  });\n\n  canvas.selectionColor = \"rgba(46, 115, 252, 0.11)\";\n  canvas.selectionBorderColor = \"rgba(98, 155, 255, 0.81)\";\n  canvas.selectionLineWidth = 1.5;\n\n  // Handle control\n  var img = new Image();\n  img.src = HandleControl;\n\n  function renderIcon(ctx, left, top, styleOverride, fabricObject) {\n    const size = 25;\n    ctx.save();\n    ctx.translate(left, top);\n    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n    ctx.drawImage(img, -size / 2, -size / 2, size, size);\n    ctx.restore();\n  }\n\n  // Rotate control\n  var img2 = new Image();\n  img2.src = RotateControl;\n\n  function renderIconRotate(ctx, left, top, styleOverride, fabricObject) {\n    const wsize = 30;\n    const hsize = 30;\n    ctx.save();\n    ctx.translate(left, top);\n    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n    ctx.drawImage(img2, -wsize / 2, -hsize / 2, wsize, hsize);\n    ctx.restore();\n  }\n\n  // Middle handle control\n  var img3 = new Image();\n  img3.src = MiddleHandleControl;\n\n  function renderIconMiddle(ctx, left, top, styleOverride, fabricObject) {\n    const size = 18;\n    ctx.save();\n    ctx.translate(left, top);\n    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n    ctx.drawImage(img3, -(size + 14) / 2, -size / 2, size + 14, size);\n    ctx.restore();\n  }\n\n  // Middle handle control vertical\n  var img4 = new Image();\n  img4.src = MiddleHandleControlV;\n\n  function renderIconMiddleV(ctx, left, top, styleOverride, fabricObject) {\n    const size = 18;\n    ctx.save();\n    ctx.translate(left, top);\n    ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n    ctx.drawImage(img4, -size / 2, -(size + 14) / 2, size, size + 14);\n    ctx.restore();\n  }\n\n  fabric.Object.prototype.controls.tl = new fabric.Control({\n    x: -0.5,\n    y: -0.5,\n    offsetX: -1,\n    offsetY: -1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.tr = new fabric.Control({\n    x: 0.5,\n    y: -0.5,\n    offsetX: 1,\n    offsetY: -1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.bl = new fabric.Control({\n    x: -0.5,\n    y: 0.5,\n    offsetX: -1,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.br = new fabric.Control({\n    x: 0.5,\n    y: 0.5,\n    offsetX: 1,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.mr = new fabric.Control({\n    x: 0.5,\n    y: 0,\n    offsetX: 1,\n    offsetY: 0,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,\n    render: renderIconMiddleV,\n  });\n\n  fabric.Object.prototype.controls.ml = new fabric.Control({\n    x: -0.5,\n    y: 0,\n    offsetX: -1,\n    offsetY: 0,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,\n    render: renderIconMiddleV,\n  });\n\n  fabric.Object.prototype.controls.mb = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    offsetX: 0,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,\n    render: renderIconMiddle,\n  });\n\n  fabric.Object.prototype.controls.mt = new fabric.Control({\n    x: 0,\n    y: -0.5,\n    offsetX: 0,\n    offsetY: -1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,\n    render: renderIconMiddle,\n  });\n\n  fabric.Object.prototype.controls.mtr = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,\n    actionHandler: fabric.controlsUtils.rotationWithSnapping,\n    offsetY: 26,\n    withConnecton: false,\n    actionName: \"rotate\",\n    render: renderIconRotate,\n  });\n\n  // Also use same controls for Textbox\n  fabric.Textbox.prototype.controls.tl = new fabric.Control({\n    x: -0.5,\n    y: -0.5,\n    offsetX: -1,\n    offsetY: -1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Textbox.prototype.controls.tr = new fabric.Control({\n    x: 0.5,\n    y: -0.5,\n    offsetX: 1,\n    offsetY: -1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Textbox.prototype.controls.bl = new fabric.Control({\n    x: -0.5,\n    y: 0.5,\n    offsetX: -1,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  fabric.Textbox.prototype.controls.br = new fabric.Control({\n    x: 0.5,\n    y: 0.5,\n    offsetX: 1,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIcon,\n  });\n\n  // Ml and mr controls for Textbox. The actionhandler should be for resizing the textbox horizontally, not skewing it\n  fabric.Textbox.prototype.controls.ml = new fabric.Control({\n    x: -0.5,\n    y: 0,\n    offsetX: -1,\n    offsetY: 0,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.changeWidth,\n    render: renderIconMiddleV,\n  });\n\n  fabric.Textbox.prototype.controls.mr = new fabric.Control({\n    x: 0.5,\n    y: 0,\n    offsetX: 1,\n    offsetY: 0,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.changeWidth,\n    render: renderIconMiddleV,\n  });\n\n  fabric.Textbox.prototype.controls.mb = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    offsetX: 0,\n    visible: false,\n    offsetY: 1,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.changeHeight,\n    render: renderIconMiddle,\n  });\n\n  fabric.Textbox.prototype.controls.mt = new fabric.Control({\n    x: 0,\n    y: -0.5,\n    offsetX: 0,\n    offsetY: -1,\n    visible: false,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.changeHeight,\n    render: renderIconMiddle,\n  });\n\n  fabric.Textbox.prototype.controls.mtr = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,\n    actionHandler: fabric.controlsUtils.rotationWithSnapping,\n    offsetY: 26,\n    withConnecton: false,\n    actionName: \"rotate\",\n    render: renderIconRotate,\n  });\n\n  // Update canvas rendering\n  canvas.renderAll();\n};\n\nexport default CustomControls;\n\n/*\nfabric.Object.prototype.set({\n  transparentCorners: false,\n  borderColor: '#51B9F9',\n  cornerColor: '#FFF',\n  borderScaleFactor: 2.5,\n  cornerStyle: 'circle',\n  cornerStrokeColor: '#0E98FC',\n  borderOpacityWhenMoving: 1,\n});\n\ncanvas.selectionColor = 'rgba(46, 115, 252, 0.11)';\ncanvas.selectionBorderColor = 'rgba(98, 155, 255, 0.81)';\ncanvas.selectionLineWidth = 1.5;\n\nvar img = document.createElement('img');\nimg.src = 'assets/middlecontrol.svg';\n\nvar img2 = document.createElement('img');\nimg2.src = 'assets/middlecontrolhoz.svg';\n\nvar img3 = document.createElement('img');\nimg3.src = 'assets/edgecontrol.svg';\n\nvar img4 = document.createElement('img');\nimg4.src = 'assets/rotateicon.svg';\n\nfunction renderIcon(ctx, left, top, styleOverride, fabricObject) {\n  const wsize = 20;\n  const hsize = 25;\n  ctx.save();\n  ctx.translate(left, top);\n  ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n  ctx.drawImage(img, -wsize / 2, -hsize / 2, wsize, hsize);\n  ctx.restore();\n}\nfunction renderIconHoz(ctx, left, top, styleOverride, fabricObject) {\n  const wsize = 25;\n  const hsize = 20;\n  ctx.save();\n  ctx.translate(left, top);\n  ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n  ctx.drawImage(img2, -wsize / 2, -hsize / 2, wsize, hsize);\n  ctx.restore();\n}\nfunction renderIconEdge(ctx, left, top, styleOverride, fabricObject) {\n  const wsize = 25;\n  const hsize = 25;\n  ctx.save();\n  ctx.translate(left, top);\n  ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n  ctx.drawImage(img3, -wsize / 2, -hsize / 2, wsize, hsize);\n  ctx.restore();\n}\n\nfunction renderIconRotate(\n  ctx,\n  left,\n  top,\n  styleOverride,\n  fabricObject\n) {\n  const wsize = 40;\n  const hsize = 40;\n  ctx.save();\n  ctx.translate(left, top);\n  ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle));\n  ctx.drawImage(img4, -wsize / 2, -hsize / 2, wsize, hsize);\n  ctx.restore();\n}\nfunction resetControls() {\n  fabric.Object.prototype.controls.ml = new fabric.Control({\n    x: -0.5,\n    y: 0,\n    offsetX: -1,\n    cursorStyleHandler:\n      fabric.controlsUtils.scaleSkewCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,\n    getActionName: fabric.controlsUtils.scaleOrSkewActionName,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.mr = new fabric.Control({\n    x: 0.5,\n    y: 0,\n    offsetX: 1,\n    cursorStyleHandler:\n      fabric.controlsUtils.scaleSkewCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingXOrSkewingY,\n    getActionName: fabric.controlsUtils.scaleOrSkewActionName,\n    render: renderIcon,\n  });\n\n  fabric.Object.prototype.controls.mb = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    offsetY: 1,\n    cursorStyleHandler:\n      fabric.controlsUtils.scaleSkewCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,\n    getActionName: fabric.controlsUtils.scaleOrSkewActionName,\n    render: renderIconHoz,\n  });\n\n  fabric.Object.prototype.controls.mt = new fabric.Control({\n    x: 0,\n    y: -0.5,\n    offsetY: -1,\n    cursorStyleHandler:\n      fabric.controlsUtils.scaleSkewCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingYOrSkewingX,\n    getActionName: fabric.controlsUtils.scaleOrSkewActionName,\n    render: renderIconHoz,\n  });\n\n  fabric.Object.prototype.controls.tl = new fabric.Control({\n    x: -0.5,\n    y: -0.5,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIconEdge,\n  });\n\n  fabric.Object.prototype.controls.tr = new fabric.Control({\n    x: 0.5,\n    y: -0.5,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIconEdge,\n  });\n\n  fabric.Object.prototype.controls.bl = new fabric.Control({\n    x: -0.5,\n    y: 0.5,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIconEdge,\n  });\n\n  fabric.Object.prototype.controls.br = new fabric.Control({\n    x: 0.5,\n    y: 0.5,\n    cursorStyleHandler: fabric.controlsUtils.scaleCursorStyleHandler,\n    actionHandler: fabric.controlsUtils.scalingEqually,\n    render: renderIconEdge,\n  });\n\n  fabric.Object.prototype.controls.mtr = new fabric.Control({\n    x: 0,\n    y: 0.5,\n    cursorStyleHandler: fabric.controlsUtils.rotationStyleHandler,\n    actionHandler: fabric.controlsUtils.rotationWithSnapping,\n    offsetY: 30,\n    withConnecton: false,\n    actionName: 'rotate',\n    render: renderIconRotate,\n  });\n}\nresetControls();\n*/\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/EraserTool.jsx",
    "content": "import { saveCanvas } from \"./History\";\n\nconst EraserTool = (canvas, contentStateRef, setContentState) => {\n  const getState = () => contentStateRef.current;\n\n  let objectsToDelete = [];\n  let isDown = false;\n\n  const setEraserHitTestMode = (enabled) => {\n    // enabled: perPixelTargetFind true, selectable false\n    canvas.forEachObject((object) => {\n      object.set({\n        selectable: false,\n        perPixelTargetFind: enabled ? true : false,\n      });\n    });\n  };\n\n  const onMouseDown = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"eraser\") return;\n\n    objectsToDelete = [];\n    isDown = true;\n\n    setEraserHitTestMode(true);\n\n    if (!o.target) return;\n\n    objectsToDelete.push(o.target);\n    o.target.set({ opacity: 0.5 });\n\n    canvas.requestRenderAll();\n  };\n\n  const onMouseMove = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"eraser\") return;\n\n    if (!isDown) return;\n    if (!o.target) return;\n\n    // avoid duplicates (optional but nice)\n    if (!objectsToDelete.includes(o.target)) {\n      objectsToDelete.push(o.target);\n      o.target.set({ opacity: 0.5 });\n    }\n\n    canvas.requestRenderAll();\n  };\n\n  const onMouseUp = () => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"eraser\") return;\n\n    // delete collected\n    objectsToDelete.forEach((object) => {\n      if (!object) return;\n\n      if (object.type === \"group\") {\n        // remove group children then group\n        object._objects?.forEach((child) => canvas.remove(child));\n        canvas.remove(object);\n      } else {\n        canvas.remove(object);\n      }\n    });\n\n    // restore opacities if any remained (in case duplicates / removed objects)\n    canvas.getObjects().forEach((obj) => {\n      if (obj?.opacity === 0.5) obj.set({ opacity: 1 });\n    });\n\n    objectsToDelete = [];\n    isDown = false;\n\n    // Selection logic is handled by your CanvasWrap effect (based on tool),\n    // so we don't force selectable=true here.\n    canvas.requestRenderAll();\n\n    saveCanvas(contentStateRef, setContentState);\n  };\n\n  const onKeyDown = (e) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"eraser\") return;\n\n    if (e.key === \"Backspace\" || e.key === \"Delete\") {\n      const activeObjects = canvas.getActiveObjects();\n      const activeObject = canvas.getActiveObject();\n\n      // If text editing, don't delete\n      if (activeObject?.isEditing) return;\n\n      if (activeObjects && activeObjects.length > 1 && !activeObject) {\n        canvas.discardActiveObject();\n        activeObjects.forEach((obj) => canvas.remove(obj));\n        saveCanvas(contentStateRef, setContentState);\n        canvas.requestRenderAll();\n        return;\n      }\n\n      if (activeObject) {\n        canvas.remove(activeObject);\n        saveCanvas(contentStateRef, setContentState);\n        canvas.requestRenderAll();\n      }\n    }\n  };\n\n  // Attach listeners once\n  document.addEventListener(\"keydown\", onKeyDown);\n  canvas.on(\"mouse:down\", onMouseDown);\n  canvas.on(\"mouse:move\", onMouseMove);\n  canvas.on(\"mouse:up\", onMouseUp);\n\n  return {\n    removeEventListeners: () => {\n      canvas.off(\"mouse:down\", onMouseDown);\n      canvas.off(\"mouse:move\", onMouseMove);\n      canvas.off(\"mouse:up\", onMouseUp);\n      document.removeEventListener(\"keydown\", onKeyDown);\n    },\n  };\n};\n\nexport default EraserTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/History.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst getState = (stateOrRef) =>\n  stateOrRef && stateOrRef.current ? stateOrRef.current : stateOrRef;\n\n// Undo and redo functionality for Fabric.js\nconst undoCanvas = (stateOrRef, setToolSettings) => {\n  const state = getState(stateOrRef);\n  if (!state?.canvas) return;\n\n  const canvas = state.canvas;\n\n  if (state.undoStack?.length > 0) {\n    const undoStack = [...state.undoStack];\n    const redoStack = [...(state.redoStack || [])];\n\n    const lastItem = undoStack.pop();\n    redoStack.push(lastItem);\n\n    const penultimateItem = undoStack[undoStack.length - 1];\n    if (!penultimateItem) {\n      // nothing meaningful to load\n      setToolSettings({ ...state, undoStack, redoStack });\n      return;\n    }\n\n    canvas.clear();\n    canvas.renderAll();\n\n    canvas.loadFromJSON(penultimateItem, () => {\n      canvas.discardActiveObject();\n      canvas.renderAll();\n    });\n\n    setToolSettings({ ...state, undoStack, redoStack });\n  }\n};\n\nconst redoCanvas = (stateOrRef, setToolSettings) => {\n  const state = getState(stateOrRef);\n  if (!state?.canvas) return;\n\n  const canvas = state.canvas;\n\n  if (state.redoStack?.length > 0) {\n    const undoStack = [...(state.undoStack || [])];\n    const redoStack = [...state.redoStack];\n\n    const lastItem = redoStack.pop();\n    undoStack.push(lastItem);\n\n    canvas.loadFromJSON(lastItem, () => {\n      canvas.discardActiveObject();\n      canvas.renderAll();\n    });\n\n    setToolSettings({ ...state, undoStack, redoStack });\n  }\n};\n\nconst saveCanvas = (stateOrRef, setToolSettings) => {\n  const state = getState(stateOrRef);\n  if (!state?.canvas) return;\n\n  const canvas = state.canvas;\n\n  const json = canvas.toJSON([\n    \"id\",\n    \"selectable\",\n    \"evented\",\n    \"hasControls\",\n    \"hasBorders\",\n    \"hasRotatingPoint\",\n    \"subTargetCheck\",\n    \"originX\",\n    \"originY\",\n    \"perPixelTargetFind\",\n    \"skipAutoWidthAdjustment\",\n  ]);\n\n  const jsonString = JSON.stringify(json);\n  const undoStack = [...(state.undoStack || []), jsonString];\n\n  setToolSettings({\n    ...state,\n    undoStack,\n    redoStack: [],\n  });\n};\n\nconst checkChanges = (canvas, stateRef, setToolSettings) => {\n  const onChange = () => {\n    // always save with latest state\n    saveCanvas(stateRef, setToolSettings);\n  };\n\n  canvas.on(\"object:modified\", onChange);\n\n  return {\n    removeEventListeners: function () {\n      canvas.off(\"object:modified\", onChange);\n    },\n  };\n};\n\nexport { undoCanvas, redoCanvas, saveCanvas, checkChanges };\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/ImageTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst ImageTool = (\n  canvas,\n  src,\n  toolSettings,\n  setToolSettings,\n  saveCanvas,\n  contentState\n) => {\n  const image = new Image();\n  let fabricImage = null;\n\n  image.src = src;\n\n  const state = {\n    isPlacing: true,\n  };\n\n  const cleanup = () => {\n    state.isPlacing = false;\n    canvas.off(\"mouse:move\", onMouseMove);\n    canvas.off(\"mouse:down\", onMouseDown);\n    canvas.off(\"mouse:up\", onMouseUp);\n  };\n\n  const onMouseMove = (o) => {\n    if (!fabricImage || !state.isPlacing) return;\n    const pointer = canvas.getPointer(o.e);\n    fabricImage.set({ left: pointer.x, top: pointer.y });\n    fabricImage.setCoords();\n    canvas.requestRenderAll();\n  };\n\n  const onMouseDown = () => {\n    if (!fabricImage || !state.isPlacing) return;\n    state.isPlacing = false;\n\n    fabricImage.set({\n      opacity: 1,\n      selectable: true,\n    });\n    fabricImage.setCoords();\n\n    canvas.setActiveObject(fabricImage);\n    canvas.requestRenderAll();\n\n    saveCanvas(toolSettings, setToolSettings);\n    setToolSettings({ ...toolSettings, tool: \"select\", isAddingImage: false });\n\n    // Make other objects selectable again\n    canvas.forEachObject((obj) => {\n      obj.selectable = true;\n    });\n\n    cleanup();\n  };\n\n  const onMouseUp = () => {};\n\n  image.onload = () => {\n    fabricImage = new fabric.Image(image);\n    fabricImage.set({\n      left: 0,\n      top: 0,\n      originX: \"left\",\n      originY: \"top\",\n      strokeUniform: true,\n      fill: \"transparent\",\n      angle: 0,\n      noScaleCache: false,\n      opacity: 0.5,\n      selectable: false,\n    });\n\n    // Scale down\n    const maxWidth = 500;\n    const maxHeight = 500;\n    const width = fabricImage.width;\n    const height = fabricImage.height;\n    const ratio = Math.min(maxWidth / width, maxHeight / height);\n    fabricImage.scale(ratio);\n\n    canvas.add(fabricImage);\n    canvas.bringToFront(fabricImage);\n    canvas.requestRenderAll();\n\n    // Make other objects unselectable\n    canvas.forEachObject((obj) => {\n      obj.selectable = false;\n    });\n\n    canvas.on(\"mouse:move\", onMouseMove);\n    canvas.on(\"mouse:down\", onMouseDown);\n    canvas.on(\"mouse:up\", onMouseUp);\n\n    // Open toast cancel handler\n    contentState.openToast(chrome.i18n.getMessage(\"addImageToastTitle\"), () => {\n      if (fabricImage) {\n        canvas.remove(fabricImage);\n        canvas.requestRenderAll();\n      }\n      setToolSettings({\n        ...toolSettings,\n        tool: \"select\",\n        isAddingImage: false,\n      });\n      canvas.forEachObject((obj) => (obj.selectable = true));\n      cleanup();\n    });\n  };\n\n  return {\n    removeEventListeners: cleanup,\n  };\n};\n\nexport default ImageTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/PenTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst PenTool = (canvas, contentStateRef, setContentState, saveCanvas) => {\n  const getState = () => contentStateRef.current;\n\n  const resetBrushStroke = () => {\n    const brush = canvas.freeDrawingBrush;\n    if (!brush) return;\n\n    // Fabric versions differ; do all safe resets.\n    if (Array.isArray(brush._points)) brush._points.length = 0;\n    if (Array.isArray(brush.points)) brush.points.length = 0;\n\n    // Some brushes keep a \"latest\" pointer\n    brush._reset && brush._reset();\n  };\n\n  const ensureBrush = () => {\n    // fabric creates freeDrawingBrush lazily; make sure it's there\n    if (!canvas.freeDrawingBrush) {\n      canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);\n    }\n    return canvas.freeDrawingBrush;\n  };\n\n  const syncBrushFromState = () => {\n    const state = getState();\n    if (!state) return;\n\n    const brush = ensureBrush();\n\n    // reset defaults\n    brush.drawStraightLine = false;\n    brush.straightLineKey = \"none\";\n    brush.strokeLineCap = \"round\";\n    brush.globalCompositeOperation = \"source-over\";\n\n    if (state.tool === \"pen\") {\n      brush.width = (state.strokeWidth || 2) * 4;\n      brush.color = state.color;\n      resetBrushStroke();\n      return;\n    }\n\n    if (state.tool === \"highlighter\") {\n      brush.width = (state.strokeWidth || 2) * 10;\n      brush.color = new fabric.Color(state.color).setAlpha(0.5).toRgba();\n      brush.globalCompositeOperation = \"destination-over\";\n      brush.strokeLineCap = \"square\";\n      resetBrushStroke();\n    }\n  };\n\n  // Sync brush on interactions so tool switches apply immediately\n  const onMouseDown = () => {\n    syncBrushFromState();\n  };\n\n  const onMouseUp = () => {\n    const state = getState();\n    if (!state) return;\n    if (state.tool !== \"pen\" && state.tool !== \"highlighter\") return;\n\n    // Save with latest state\n    // If you updated saveCanvas to accept ref, use: saveCanvas(contentStateRef, setContentState)\n    saveCanvas(state, setContentState);\n  };\n\n  const onPathCreated = (o) => {\n    // Only wrap paths if we were drawing (pen/highlighter)\n    const state = getState();\n    if (!state) return;\n    if (state.tool !== \"pen\" && state.tool !== \"highlighter\") return;\n\n    const path = o.path;\n    if (!path) return;\n\n    const pathCopy = new fabric.Path(path.path, {\n      id: \"select-stroke\",\n      stroke: \"#0D99FF\",\n      strokeWidth: 2,\n      fill: null,\n      opacity: 0,\n      selectable: false,\n      evented: false,\n    });\n\n    const group = new fabric.Group([path, pathCopy], {\n      selectable: true,\n      evented: true,\n      id: \"select-group\",\n      subTargetCheck: true,\n      perPixelTargetFind: true,\n      hasControls: false,\n      hasBorders: false,\n    });\n\n    canvas.add(group);\n    canvas.remove(path);\n    canvas.requestRenderAll();\n  };\n\n  const hideAllSelectStrokes = () => {\n    canvas.getObjects().forEach((obj) => {\n      if (obj.type === \"group\") {\n        obj._objects?.forEach((child) => {\n          if (child.id === \"select-stroke\") child.set({ opacity: 0 });\n        });\n      }\n    });\n  };\n\n  const activateStroke = () => {\n    const state = getState();\n    if (!state) return;\n    if (state.tool !== \"select\") return;\n\n    hideAllSelectStrokes();\n\n    const activeObject = canvas.getActiveObject();\n    if (activeObject && activeObject.id === \"select-group\") {\n      activeObject._objects?.forEach((child) => {\n        if (child.id === \"select-stroke\") child.set({ opacity: 1 });\n      });\n    }\n\n    const activeObjects = canvas.getActiveObjects();\n    if (activeObjects && activeObjects.length > 1) {\n      activeObjects.forEach((obj) => {\n        if (obj.id === \"select-group\") {\n          obj._objects?.forEach((child) => {\n            if (child.id === \"select-stroke\") child.set({ opacity: 1 });\n          });\n        }\n      });\n    }\n\n    canvas.requestRenderAll();\n  };\n\n  const deactivateStroke = () => {\n    hideAllSelectStrokes();\n    canvas.requestRenderAll();\n  };\n\n  // Attach once\n  canvas.on(\"mouse:down\", onMouseDown);\n  canvas.on(\"mouse:up\", onMouseUp);\n  canvas.on(\"path:created\", onPathCreated);\n  canvas.on(\"selection:created\", activateStroke);\n  canvas.on(\"selection:updated\", activateStroke);\n  canvas.on(\"selection:cleared\", deactivateStroke);\n\n  // Initial sync\n  syncBrushFromState();\n\n  return {\n    removeEventListeners: () => {\n      canvas.off(\"mouse:down\", onMouseDown);\n      canvas.off(\"mouse:up\", onMouseUp);\n      canvas.off(\"path:created\", onPathCreated);\n      canvas.off(\"selection:created\", activateStroke);\n      canvas.off(\"selection:updated\", activateStroke);\n      canvas.off(\"selection:cleared\", deactivateStroke);\n    },\n  };\n};\n\nexport default PenTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/SelectTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst SelectTool = (canvas, contentStateRef, setContentState) => {\n  const getState = () => contentStateRef.current;\n\n  // On mouse over object\n  const onMouseOver = (o) => {\n    const state = getState();\n    if (!state) return;\n    if (state.tool !== \"select\") return;\n    if (state.isAddingImage) return;\n    if (!o.target) return;\n\n    if (o.target !== canvas.getActiveObject()) {\n      if (o.target.type === \"group\" && o.target.id === \"select-group\") {\n        const selectStroke = o.target._objects?.find(\n          (object) => object.id === \"select-stroke\",\n        );\n        if (selectStroke) selectStroke.set({ opacity: 1 });\n      } else if (o.target.type === \"group\" && o.target.id === \"arrowGroup\") {\n        o.target._objects?.forEach((obj) => {\n          if (obj.id === \"arrowLineControl\") obj.set({ opacity: 1 });\n        });\n      } else {\n        // draws fabric controls on the top context\n        o.target._renderControls(o.target.canvas.contextTop, {\n          hasControls: false,\n        });\n      }\n      canvas.requestRenderAll();\n    }\n  };\n\n  // On mouse out object\n  const onMouseOut = (o) => {\n    const state = getState();\n    if (!state) return;\n    if (state.tool !== \"select\") return;\n    if (!o.target) return;\n\n    if (o.target !== canvas.getActiveObject()) {\n      if (o.target.type === \"group\" && o.target.id === \"select-group\") {\n        const selectStroke = o.target._objects?.find(\n          (object) => object.id === \"select-stroke\",\n        );\n        if (selectStroke) selectStroke.set({ opacity: 0 });\n      } else if (o.target.type === \"group\" && o.target.id === \"arrowGroup\") {\n        o.target._objects?.forEach((obj) => {\n          if (obj.id === \"arrowLineControl\") obj.set({ opacity: 0 });\n        });\n      }\n    }\n\n    if (o.target?.canvas?.contextTop) {\n      o.target.canvas.clearContext(o.target.canvas.contextTop);\n    }\n    canvas.requestRenderAll();\n  };\n\n  const onMouseDown = (o) => {\n    const state = getState();\n    if (!state) return;\n    if (state.isAddingImage) return;\n    if (state.tool !== \"select\") return;\n    if (!o.target?.canvas?.contextTop) return;\n\n    o.target.canvas.clearContext(o.target.canvas.contextTop);\n  };\n\n  canvas.on(\"mouse:over\", onMouseOver);\n  canvas.on(\"mouse:out\", onMouseOut);\n  canvas.on(\"mouse:down\", onMouseDown);\n\n  return {\n    removeEventListeners: function () {\n      canvas.off(\"mouse:over\", onMouseOver);\n      canvas.off(\"mouse:out\", onMouseOut);\n      canvas.off(\"mouse:down\", onMouseDown);\n    },\n  };\n};\n\nexport default SelectTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/ShapeTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst ShapeTool = (canvas, contentStateRef, setContentState, saveCanvas) => {\n  const getState = () => contentStateRef.current;\n\n  let shape = null;\n  let isDown = false;\n  let origX = 0;\n  let origY = 0;\n\n  const makeShape = (state, pointer) => {\n    const color = state.color;\n    const strokeWidth = (state.strokeWidth || 2) * 6;\n    const fill = state.shapeFill ? color : \"transparent\";\n\n    if (state.shape === \"rectangle\") {\n      return new fabric.Rect({\n        left: origX,\n        top: origY,\n        originX: \"left\",\n        originY: \"top\",\n        width: 0,\n        height: 0,\n        strokeUniform: true,\n        angle: 0,\n        fill,\n        noScaleCache: false,\n        stroke: color,\n        strokeWidth,\n      });\n    }\n\n    if (state.shape === \"triangle\") {\n      return new fabric.Triangle({\n        left: origX,\n        top: origY,\n        originX: \"left\",\n        originY: \"top\",\n        strokeMilterLimit: 8,\n        objectCaching: false,\n        width: 0,\n        height: 0,\n        strokeUniform: true,\n        angle: 0,\n        fill,\n        noScaleCache: false,\n        stroke: color,\n        strokeWidth,\n      });\n    }\n\n    // circle\n    return new fabric.Circle({\n      left: origX,\n      top: origY,\n      originX: \"left\",\n      originY: \"top\",\n      radius: 0,\n      strokeUniform: true,\n      angle: 0,\n      fill,\n      noScaleCache: false,\n      stroke: color,\n      strokeWidth,\n    });\n  };\n\n  const onMouseDown = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"shape\") return;\n\n    isDown = true;\n\n    const pointer = canvas.getPointer(o.e);\n    origX = pointer.x;\n    origY = pointer.y;\n\n    shape = makeShape(state, pointer);\n    canvas.add(shape);\n    canvas.requestRenderAll();\n  };\n\n  const onMouseMove = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"shape\") return;\n    if (!isDown || !shape) return;\n\n    const pointer = canvas.getPointer(o.e);\n\n    // Handle dragging “backwards”\n    const left = Math.min(origX, pointer.x);\n    const top = Math.min(origY, pointer.y);\n    const w = Math.abs(pointer.x - origX);\n    const h = Math.abs(pointer.y - origY);\n\n    shape.set({ left, top });\n\n    if (state.shape === \"rectangle\" || state.shape === \"triangle\") {\n      shape.set({ width: w, height: h });\n    } else if (state.shape === \"circle\") {\n      // Use min dimension so it stays circular\n      const r = Math.min(w, h) / 2;\n      shape.set({ radius: r });\n    }\n\n    shape.setCoords();\n    canvas.requestRenderAll();\n  };\n\n  const onMouseUp = () => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"shape\") return;\n    if (!isDown) return;\n\n    isDown = false;\n\n    canvas.requestRenderAll();\n\n    // Save with latest state (not stale snapshot)\n    saveCanvas({ ...state, tool: \"select\" }, setContentState);\n\n    if (shape) {\n      canvas.setActiveObject(shape);\n      canvas.requestRenderAll();\n    }\n  };\n\n  canvas.on(\"mouse:down\", onMouseDown);\n  canvas.on(\"mouse:move\", onMouseMove);\n  canvas.on(\"mouse:up\", onMouseUp);\n\n  return {\n    removeEventListeners: () => {\n      canvas.off(\"mouse:down\", onMouseDown);\n      canvas.off(\"mouse:move\", onMouseMove);\n      canvas.off(\"mouse:up\", onMouseUp);\n    },\n  };\n};\n\nexport default ShapeTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/modules/TextTool.jsx",
    "content": "import { fabric } from \"fabric\";\n\nconst TextTool = (canvas, contentStateRef, setContentState, saveCanvas) => {\n  const getState = () => contentStateRef.current;\n\n  // Track the currently-created textbox so we can finalize it cleanly\n  let activeCreatedText = null;\n\n  const finalizeIfNeeded = () => {\n    if (!activeCreatedText) return;\n\n    const text = activeCreatedText;\n    const currentActive = canvas.getActiveObject();\n\n    // If user clicked away / ended editing\n    if (currentActive !== text) {\n      if ((text.text || \"\").trim() === \"\") {\n        canvas.remove(text);\n      } else {\n        // Save using latest state\n        saveCanvas({ ...getState(), canvas }, setContentState);\n      }\n      activeCreatedText = null;\n      canvas.requestRenderAll();\n    }\n  };\n\n  const onCanvasMouseDownCapture = (o) => {\n    // This runs on any mouse down to potentially finalize the last text.\n    // But don't interfere if user is still editing that text.\n    finalizeIfNeeded();\n  };\n\n  const onMouseDown = (o) => {\n    const state = getState();\n    if (!state?.drawingMode) return;\n    if (state.tool !== \"text\") return;\n\n    // Before creating a new one, finalize previous if any\n    finalizeIfNeeded();\n\n    const pointer = canvas.getPointer(o.e);\n    const x = pointer.x;\n    const y = pointer.y;\n\n    const text = new fabric.Textbox(\"\", {\n      left: x,\n      top: y,\n      fontFamily: \"Satoshi-Medium\",\n      fontSize: 20,\n      fill: state.color,\n      fontWeight: \"normal\",\n      fontStyle: \"normal\",\n      originX: \"left\",\n      originY: \"top\",\n      textAlign: \"center\",\n      lockUniScaling: true,\n      centeredScaling: true,\n      skipAutoWidthAdjustment: false,\n      perPixelTargetFind: false,\n    });\n\n    text.on(\"editing:entered\", () => {\n      text.borderColor = \"#0D99FF\";\n    });\n\n    canvas.add(text);\n    canvas.setActiveObject(text);\n\n    // Start editing\n    text.enterEditing();\n    text.selectAll();\n\n    activeCreatedText = text;\n\n    // Set tool back to select (use latest state)\n    setContentState((prev) => ({\n      ...prev,\n      tool: \"select\",\n    }));\n\n    canvas.requestRenderAll();\n  };\n\n  const onKeyPress = (event) => {\n    const obj = canvas.getActiveObject();\n    if (!obj || typeof obj !== \"object\") return;\n\n    if (obj.type !== \"textbox\" || !obj.isEditing) return;\n\n    const text = obj;\n\n    // If user explicitly resized, skip\n    if (text.skipAutoWidthAdjustment) return;\n\n    // Guard: _textLines might not exist at some moments\n    const lines = text._textLines || [];\n    if (lines.length === 0) return;\n\n    let currentLine = lines[lines.length - 1].join(\"\");\n\n    // Backspace\n    if (event.key === \"Backspace\" && currentLine.length > 0) {\n      currentLine = currentLine.slice(0, -1);\n    } else if (event.key && event.key.length === 1) {\n      currentLine += event.key;\n    } else {\n      // ignore other keys\n      return;\n    }\n\n    // Measure\n    const tempCanvas = document.createElement(\"canvas\");\n    const tempCtx = tempCanvas.getContext(\"2d\");\n    tempCtx.font = `${text.fontSize}px ${text.fontFamily}`;\n    const textMetrics = tempCtx.measureText(currentLine);\n\n    const maxLineWidth = getMaxLineWidth(lines, text);\n\n    // Expand width if needed\n    if (textMetrics.width > text.width) {\n      const nextWidth = Math.max(text.width, textMetrics.width + 2);\n      text.set({\n        left: text.left - (nextWidth - text.width) / 2,\n        width: nextWidth,\n      });\n    } else if (textMetrics.width < maxLineWidth) {\n      text.set({\n        left: text.left + (text.width - maxLineWidth) / 2,\n        width: maxLineWidth,\n      });\n    }\n\n    canvas.requestRenderAll();\n  };\n\n  function getMaxLineWidth(textLines, text) {\n    let maxLineWidth = 0;\n    const tempCanvas = document.createElement(\"canvas\");\n    const tempCtx = tempCanvas.getContext(\"2d\");\n    tempCtx.font = `${text.fontSize}px ${text.fontFamily}`;\n\n    for (let i = 0; i < textLines.length; i++) {\n      const line = textLines[i].join(\"\");\n      const lineWidth = tempCtx.measureText(line).width;\n      maxLineWidth = Math.max(maxLineWidth, lineWidth);\n    }\n\n    return maxLineWidth;\n  }\n\n  const onResize = (e) => {\n    if (e?.target?.type !== \"textbox\") return;\n    e.target.skipAutoWidthAdjustment = true;\n    canvas.requestRenderAll();\n  };\n\n  // Hover logic: DON'T use anonymous mouse:move that you later nuke.\n  const onMouseMove = (event) => {\n    const pointer = canvas.getPointer(event.e);\n    let hoveringTextbox = false;\n\n    canvas.forEachObject((obj) => {\n      if (obj.type === \"textbox\" && obj.containsPoint(pointer)) {\n        hoveringTextbox = true;\n      }\n    });\n\n    canvas.perPixelTargetFind = !hoveringTextbox;\n  };\n\n  // Attach listeners once\n  // Note: we attach the “finalize” handler too, but it’s cheap.\n  canvas.on(\"mouse:down\", onCanvasMouseDownCapture);\n  canvas.on(\"mouse:down\", onMouseDown);\n\n  document.addEventListener(\"keydown\", onKeyPress);\n  canvas.on(\"object:resizing\", onResize);\n  canvas.on(\"mouse:move\", onMouseMove);\n\n  return {\n    removeEventListeners: () => {\n      canvas.off(\"mouse:down\", onCanvasMouseDownCapture);\n      canvas.off(\"mouse:down\", onMouseDown);\n      document.removeEventListener(\"keydown\", onKeyPress);\n      canvas.off(\"object:resizing\", onResize);\n      canvas.off(\"mouse:move\", onMouseMove);\n    },\n  };\n};\n\nexport default TextTool;\n"
  },
  {
    "path": "src/pages/Content/canvas/styles/_Canvas.scss",
    "content": "@use '../../styles/_variables' as *;\n@use './layout/Canvas';"
  },
  {
    "path": "src/pages/Content/canvas/styles/layout/_Canvas.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.CanvasContainer {\n\twidth: 100%;\n\theight: 100%;\n\tposition: absolute;\n\tpointer-events: all!important;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999!important;\n}\n.canvas {\n\twidth: 100%;\n\theight: 100%;\n\tposition: absolute;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999!important;\n}\n.canvas-container {\n\twidth: 100vw!important;\n\theight: 100vh!important;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tz-index: 99999999999;\n\tposition: absolute!important;\n}"
  },
  {
    "path": "src/pages/Content/context/ContentState.jsx",
    "content": "import React, {\n  createContext,\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n} from \"react\";\n\nimport { updateFromStorage } from \"./utils/updateFromStorage\";\n\n// Shortcuts\nimport Shortcuts from \"../shortcuts/Shortcuts\";\nimport DevHUD from \"../DevHUD\";\n\n// import { initializeContentMessageListener } from \"./messaging/messageListener\";\nimport { setupHandlers } from \"./messaging/handlers\";\n\nimport { checkAuthStatus } from \"./utils/checkAuthStatus\";\nimport {\n  initStartFlowTrace,\n  traceStep,\n  setStartFlowOutcome,\n} from \"../../utils/startFlowTrace\";\n\n//create a context, with createContext api\nexport const contentStateContext = createContext();\nexport const contentStateRef = { current: null };\nexport let setContentState = () => {};\nexport let setTimer = () => {};\n\nconst CURSOR_EFFECTS = [\"target\", \"highlight\", \"spotlight\"];\n\nconst normalizeCursorEffects = (effects) => {\n  if (!Array.isArray(effects)) return [];\n  return effects.filter((effect) => CURSOR_EFFECTS.includes(effect));\n};\n\nconst deriveCursorMode = (effects, fallbackMode) => {\n  if (effects.length === 0) return \"none\";\n  if (effects.length === 1) return effects[0];\n  if (fallbackMode && effects.includes(fallbackMode)) return fallbackMode;\n  return effects[0] || \"none\";\n};\n\nconst ContentState = (props) => {\n  const [timer, setTimerInternal] = React.useState(0);\n  const CLOUD_FEATURES_ENABLED =\n    process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n  setTimer = setTimerInternal;\n  const [URL, setURL] = useState(\n    \"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/why-does-screenity-ask-for-permissions/9AAE8zJ6iiUtCAtjn4SUT1\",\n  );\n  const [URL2, setURL2] = useState(\n    \"https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/how-to-grant-screenity-permission-to-record-your-camera-and-microphone/x6U69TnrbMjy5CQ96Er2E9\",\n  );\n  const startBeepRef = useRef(null);\n  const stopBeepRef = useRef(null);\n  const prevRecordingRef = useRef(null);\n  const hydratedRef = useRef(false);\n  const suppressStopBeepRef = useRef(false);\n  const suppressStartBeepRef = useRef(false);\n  const tabIdRef = useRef(null);\n  const activeTabRef = useRef(null);\n  const tabRecordedIdRef = useRef(null);\n  const recordingUiTabRef = useRef(null);\n  const recordingStartTimeRef = useRef(null);\n  const timerReadSeqRef = useRef(0);\n  const lastBeepStartTimeRef = useRef(null);\n  const recordingBeepTabIdRef = useRef(null);\n  const verifyDebounceRef = useRef(null);\n\n  const isTargetTab = useCallback(() => {\n    const tabId = tabIdRef.current;\n    const tabRecordedID = tabRecordedIdRef.current;\n    const recordingUiTabId = recordingUiTabRef.current;\n    const activeTab = activeTabRef.current;\n\n    if (tabRecordedID != null) {\n      return tabId != null && tabId === tabRecordedID;\n    }\n    if (recordingUiTabId != null) {\n      return tabId != null && tabId === recordingUiTabId;\n    }\n    if (activeTab != null) {\n      return tabId != null && tabId === activeTab;\n    }\n    return true;\n  }, []);\n\n  // Check if the user is logged in\n  const verifyUser = useCallback(async () => {\n    if (!CLOUD_FEATURES_ENABLED) return;\n    const result = await checkAuthStatus();\n\n    setContentState((prev) => ({\n      ...prev,\n      isLoggedIn: result.authenticated,\n      screenityUser: result.user,\n      isSubscribed: result.subscribed,\n      hasSubscribedBefore: result.hasSubscribedBefore,\n      proSubscription: result.proSubscription,\n      ...(result.authenticated ? { wasLoggedIn: false } : {}),\n    }));\n\n    if (result.authenticated) {\n      // Offscreen recording and client-side zoom are not available\n      setContentState((prev) => ({\n        ...prev,\n        offscreenRecording: false,\n        zoomEnabled: false,\n      }));\n\n      chrome.storage.local.set({\n        offscreenRecording: false,\n        zoomEnabled: false,\n        wasLoggedIn: false,\n      });\n    }\n  }, [CLOUD_FEATURES_ENABLED]);\n  useEffect(() => {\n    verifyUser();\n  }, [verifyUser]);\n\n  useEffect(() => {\n    chrome.runtime.sendMessage({ type: \"get-tab-id\" }, (response) => {\n      if (response?.tabId !== undefined && response?.tabId !== null) {\n        tabIdRef.current = response.tabId;\n      }\n    });\n\n    chrome.storage.local.get(\n      [\n        \"activeTab\",\n        \"tabRecordedID\",\n        \"recordingUiTabId\",\n        \"recordingStartTime\",\n        \"recordingBeepTabId\",\n        \"recordingNow\",\n      ],\n      (result) => {\n        activeTabRef.current = result.activeTab ?? null;\n        tabRecordedIdRef.current = result.tabRecordedID ?? null;\n        recordingUiTabRef.current = result.recordingUiTabId ?? null;\n        recordingStartTimeRef.current = result.recordingStartTime ?? null;\n        recordingBeepTabIdRef.current = result.recordingBeepTabId ?? null;\n      },\n    );\n  }, []);\n\n  useEffect(() => {\n    const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n    if (!locale.includes(\"en\")) {\n      setURL(\n        \"https://translate.google.com/translate?sl=en&tl=\" +\n          locale +\n          \"&u=https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/why-does-screenity-ask-for-permissions/9AAE8zJ6iiUtCAtjn4SUT1\",\n      );\n      setURL2(\n        \"https://translate.google.com/translate?sl=en&tl=\" +\n          locale +\n          \"&u=https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/how-to-grant-screenity-permission-to-record-your-camera-and-microphone/x6U69TnrbMjy5CQ96Er2E9\",\n      );\n    }\n  }, []);\n\n  const startRecording = useCallback(() => {\n    const shouldClearCountdown =\n      !contentStateRef.current?.isCountdownVisible &&\n      !contentStateRef.current?.countdownActive;\n    if (contentStateRef.current.alarm) {\n      if (contentStateRef.current.alarmTime === 0) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          alarm: false,\n        }));\n        chrome.storage.local.set({ alarm: false });\n        setTimer(0);\n      } else {\n        setTimer(contentStateRef.current.alarmTime);\n      }\n    } else {\n      setTimer(0);\n    }\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      recording: true,\n      paused: false,\n      timeWarning: false,\n      pendingRecording: false,\n      preparingRecording: false,\n      ...(shouldClearCountdown\n        ? { countdownActive: false, isCountdownVisible: false }\n        : {}),\n    }));\n    if (tabIdRef.current != null) {\n      const preferredTab =\n        tabRecordedIdRef.current ??\n        recordingUiTabRef.current ??\n        tabIdRef.current;\n      chrome.storage.local.set({ recordingBeepTabId: preferredTab });\n      recordingBeepTabIdRef.current = preferredTab;\n    }\n    chrome.storage.local.set({ restarting: false });\n    traceStep(\"recordingStarted\");\n    setStartFlowOutcome(\"ok\");\n\n    // This cannot be triggered from here because the user might not have the page focused\n    //chrome.runtime.sendMessage({ type: \"start-recording\" });\n  }, []);\n\n  const restartRecording = useCallback(() => {\n    // Restart transitions recording true -> false briefly; suppress the normal\n    // \"stop\" beep because this is not a final stop/save action.\n    suppressStopBeepRef.current = true;\n    const sourceTabId = tabIdRef.current ?? activeTabRef.current ?? null;\n    chrome.storage.local.set({ restarting: true });\n    setTimeout(() => {\n      chrome.runtime.sendMessage({ type: \"discard-backup-restart\" });\n      chrome.runtime.sendMessage({ type: \"handle-restart\", sourceTabId });\n      if (contentStateRef.current.alarm) {\n        setTimer(contentStateRef.current.alarmTime);\n      } else {\n        setTimer(0);\n      }\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        recording: false,\n        time: 0,\n        paused: false,\n      }));\n    }, 100);\n  }, []);\n\n  useEffect(() => {\n    (async () => {\n      let realSupport = false;\n\n      if (\"VideoEncoder\" in window) {\n        try {\n          const support = await VideoEncoder.isConfigSupported({\n            codec: \"vp8\",\n            width: 16,\n            height: 16,\n          });\n          realSupport = support.supported;\n        } catch {}\n      }\n\n      chrome.storage.local.set({ realWebCodecsSupport: realSupport });\n    })();\n  }, []);\n\n  const stopRecording = useCallback(() => {\n    chrome.runtime.sendMessage({ type: \"clear-recording-alarm\" });\n    chrome.storage.local.set({\n      restarting: false,\n      tabRecordedID: null,\n      drawingMode: false,\n      blurMode: false,\n      cursorMode: \"none\",\n      cursorEffects: [],\n    });\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      recording: false,\n      paused: false,\n      timeWarning: false,\n      showExtension: false,\n      blurMode: false,\n      showPopup: true,\n      pendingRecording: false,\n      tabCaptureFrame: false,\n      pipEnded: false,\n      time: 0,\n      timer: 0,\n      preparingRecording: false,\n      drawingMode: false,\n      blurMode: false,\n      toolbarMode: \"\",\n      cursorMode: \"none\",\n      cursorEffects: [],\n    }));\n    // Remove blur from all elements\n    const elements = document.querySelectorAll(\".screenity-blur\");\n    elements.forEach((element) => {\n      element.classList.remove(\"screenity-blur\");\n    });\n    setTimer(0);\n    chrome.runtime.sendMessage(\n      { type: \"stop-recording-tab\", reason: \"content-toolbar-stop\" },\n      (res) => {\n      if (!res || res.ok !== true) {\n        console.warn(\"Stop command not acknowledged, retrying…\");\n        setTimeout(() => {\n          chrome.runtime.sendMessage({\n            type: \"stop-recording-tab\",\n            reason: \"content-toolbar-stop-retry\",\n          });\n        }, 200);\n      }\n      },\n    );\n  });\n\n  const pauseRecording = useCallback((dismiss) => {\n    if (contentStateRef.current?.paused) return;\n    chrome.runtime.sendMessage({ type: \"pause-recording-tab\" });\n\n    setTimeout(() => {\n      setContentState((prev) => ({\n        ...prev,\n        paused: true,\n      }));\n      if (!dismiss) {\n        contentStateRef.current.openToast(\n          chrome.i18n.getMessage(\"pausedRecordingToast\"),\n          function () {},\n        );\n      }\n    }, 100);\n  });\n\n  const resumeRecording = useCallback(() => {\n    if (!contentStateRef.current?.paused) return;\n    chrome.runtime.sendMessage({ type: \"resume-recording-tab\" });\n\n    setContentState((prev) => ({\n      ...prev,\n      paused: false,\n    }));\n  });\n\n  const dismissRecording = useCallback(() => {\n    setStartFlowOutcome(\"cancelled\");\n    suppressStopBeepRef.current = true;\n    chrome.storage.local.set({ restarting: false });\n    chrome.runtime.sendMessage({ type: \"dismiss-recording-tab\" });\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      recording: false,\n      paused: false,\n      showExtension: false,\n      timeWarning: false,\n      showPopup: true,\n      time: 0,\n      timer: 0,\n      tabCaptureFrame: false,\n      pendingRecording: false,\n      preparingRecording: false,\n      pipEnded: false,\n      blurMode: false,\n      drawingMode: false,\n    }));\n    // Remove blur from all elements\n    const elements = document.querySelectorAll(\".screenity-blur\");\n    elements.forEach((element) => {\n      element.classList.remove(\"screenity-blur\");\n    });\n    setTimer(0);\n  });\n\n  const checkChromeCapturePermissions = useCallback(async () => {\n    const permissions = [\"desktopCapture\", \"alarms\", \"offscreen\"];\n\n    // Only request clipboardWrite if the user is logged in and subscribed\n    if (\n      contentStateRef.current?.isLoggedIn &&\n      contentStateRef.current?.isSubscribed\n    ) {\n      permissions.push(\"clipboardWrite\");\n    }\n\n    const containsPromise = new Promise((resolve) => {\n      chrome.permissions.contains({ permissions }, (result) => {\n        resolve(result);\n      });\n    });\n\n    const result = await containsPromise;\n\n    if (!result) {\n      const requestPromise = new Promise((resolve) => {\n        chrome.permissions.request({ permissions }, (granted) => {\n          resolve(granted);\n        });\n      });\n\n      const granted = await requestPromise;\n\n      if (!granted) {\n        return false;\n      } else {\n        chrome.runtime.sendMessage({ type: \"add-alarm-listener\" });\n        return true;\n      }\n    } else {\n      return true;\n    }\n  }, []);\n\n  const checkChromeCapturePermissionsSW = useCallback(async () => {\n    const { isLoggedIn, isSubscribed } = contentStateRef.current;\n\n    return new Promise((resolve) => {\n      chrome.runtime.sendMessage(\n        {\n          type: \"check-capture-permissions\",\n          isLoggedIn,\n          isSubscribed,\n        },\n        (response) => {\n          resolve(response.status === \"ok\");\n        },\n      );\n    });\n  }, []);\n\n  const startStreaming = useCallback(async () => {\n    // Init start-flow trace for this attempt\n    const attemptId = `ra-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;\n    await initStartFlowTrace(attemptId, {\n      recordingType: contentStateRef.current.recordingType,\n      isPro: Boolean(\n        contentStateRef.current.isLoggedIn &&\n        contentStateRef.current.isSubscribed &&\n        CLOUD_FEATURES_ENABLED,\n      ),\n      countdown: Boolean(contentStateRef.current.countdown),\n    });\n    traceStep(\"startStreaming\");\n\n    // Overlay becomes non-blocking while pending. Popup stays open until the\n    // recorder tab takes focus. Also clear countdownCancelled so a stale\n    // cancellation from a previous attempt can't block this one.\n    // Write to storage immediately (not via useEffect) so the tab activation\n    // listener sees pendingRecording=true before the recorder tab opens.\n    chrome.storage.local.set({ pendingRecording: true });\n    setContentState((prev) => ({\n      ...prev,\n      pendingRecording: true,\n      countdownCancelled: false,\n    }));\n\n    let permission = false;\n\n    if (\n      contentStateRef.current?.isLoggedIn &&\n      contentStateRef.current?.isSubscribed &&\n      CLOUD_FEATURES_ENABLED\n    ) {\n      const storageResponse = await chrome.runtime.sendMessage({\n        type: \"check-storage-quota\",\n      });\n\n      const { success, canUpload, error } = storageResponse;\n\n      if (success && canUpload === false) {\n        contentStateRef.current.openModal(\n          chrome.i18n.getMessage(\"storageLimitReachedTitle\"),\n          chrome.i18n.getMessage(\"storageLimitReachedDescription\"),\n          chrome.i18n.getMessage(\"manageStorageButtonLabel\"),\n          chrome.i18n.getMessage(\"closeModalLabel\"),\n          () => {\n            window.open(process.env.SCREENITY_APP_BASE, \"_blank\");\n          },\n          () => {},\n        );\n      } else if (!success) {\n        const isSubError = error === \"Subscription inactive\";\n        const isAuthError = error === \"Not authenticated\";\n\n        // Update content state if subscription is inactive\n        if (isSubError) {\n          contentStateRef.current.setContentState((prev) => ({\n            ...prev,\n            isSubscribed: false,\n          }));\n        } else if (isAuthError) {\n          contentStateRef.current.setContentState((prev) => ({\n            ...prev,\n            isSubscribed: false,\n            isLoggedIn: false,\n            screenityUser: null,\n            proSubscription: null,\n          }));\n        }\n\n        const message = isAuthError\n          ? chrome.i18n.getMessage(\"storageCheckFailAuthDescription\")\n          : chrome.i18n.getMessage(\"storageCheckFailDescription\");\n\n        contentStateRef.current.openModal(\n          chrome.i18n.getMessage(\"storageCheckFailTitle\"),\n          message,\n          chrome.i18n.getMessage(\"retryButtonLabel\"),\n          chrome.i18n.getMessage(\"closeModalLabel\"),\n          async () => {\n            window.location.reload(); // or retry logic\n          },\n          () => {},\n        );\n      }\n\n      if (!success || (success && canUpload === false)) {\n        setStartFlowOutcome(\"error\", {\n          error: canUpload === false ? \"storage-limit\" : (error || \"quota-check-failed\"),\n        });\n        setContentState((prev) => ({\n          ...prev,\n          pendingRecording: false,\n          preparingRecording: false,\n        }));\n        return; // Stop recording setup\n      }\n    }\n\n    // Check if in content script or extension page (Chrome)\n    if (window.location.href.includes(\"chrome-extension://\")) {\n      permission = await checkChromeCapturePermissions();\n    } else {\n      permission = await checkChromeCapturePermissionsSW();\n    }\n\n    if (!permission) {\n      contentStateRef.current.openModal(\n        chrome.i18n.getMessage(\"chromePermissionsModalTitle\"),\n        chrome.i18n.getMessage(\"chromePermissionsModalDescription\"),\n        chrome.i18n.getMessage(\"chromePermissionsModalAction\"),\n        chrome.i18n.getMessage(\"chromePermissionsModalCancel\"),\n        async () => {\n          await checkChromeCapturePermissionsSW();\n          startStreaming(); // Retry streaming\n        },\n        () => {},\n        null,\n        chrome.i18n.getMessage(\"learnMoreDot\"),\n        URL,\n        true,\n      );\n      setStartFlowOutcome(\"cancelled\", { error: \"permission-denied\" });\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        pendingRecording: false,\n        preparingRecording: false,\n      }));\n      return;\n    }\n\n    const data = await chrome.runtime.sendMessage({ type: \"available-memory\" });\n\n    if (\n      data.quota < 524288000 &&\n      !contentStateRef.current.isLoggedIn &&\n      !contentStateRef.current.isSubscribed\n    ) {\n      if (typeof contentStateRef.current.openModal === \"function\") {\n        let clear = null;\n        let clearAction = () => {};\n        const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n        let helpURL =\n          \"https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/what-does-%E2%80%9Cmemory-limit-reached%E2%80%9D-mean-when-recording/8WkwHbt3puuXunYqQnyPcb\";\n\n        if (!locale.includes(\"en\")) {\n          helpURL =\n            \"https://translate.google.com/translate?sl=en&tl=\" +\n            locale +\n            \"&u=https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/what-does-%E2%80%9Cmemory-limit-reached%E2%80%9D-mean-when-recording/8WkwHbt3puuXunYqQnyPcb\";\n        }\n\n        const response = await chrome.runtime.sendMessage({\n          type: \"check-restore\",\n        });\n        if (response.restore) {\n          clear = chrome.i18n.getMessage(\"clearSpaceButton\");\n          clearAction = () => {\n            chrome.runtime.sendMessage({ type: \"clear-recordings\" });\n          };\n        }\n\n        contentStateRef.current.openModal(\n          chrome.i18n.getMessage(\"notEnoughSpaceTitle\"),\n          chrome.i18n.getMessage(\"notEnoughSpaceDescription\"),\n          clear,\n          chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n          clearAction,\n          () => {},\n          null,\n          chrome.i18n.getMessage(\"learnMoreDot\"),\n          helpURL,\n          false, // colorSafe\n          chrome.i18n.getMessage(\"getHelpButton\"),\n          () => {\n            chrome.runtime.sendMessage({\n              type: \"report-error\",\n              errorCode: \"REC_RUN_MEMORY\",\n              source: \"not-enough-space\",\n            });\n          },\n        );\n      }\n      setStartFlowOutcome(\"error\", { error: \"insufficient-memory\" });\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        pendingRecording: false,\n        preparingRecording: false,\n      }));\n      return;\n    }\n    chrome.storage.local.set({\n      tabRecordedID: null,\n    });\n\n    if (\n      contentStateRef.current.recordingType === \"region\" &&\n      contentStateRef.current.cropTarget\n    ) {\n      contentStateRef.current.regionCaptureRef.contentWindow.postMessage(\n        {\n          type: \"crop-target\",\n          target: contentStateRef.current.cropTarget,\n          width: contentStateRef.current.regionWidth,\n          height: contentStateRef.current.regionHeight,\n        },\n        \"*\",\n      );\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      showOnboardingArrow: false,\n    }));\n\n    if (\n      !contentStateRef.current.micActive &&\n      contentStateRef.current.askMicrophone\n    ) {\n      contentStateRef.current.openModal(\n        chrome.i18n.getMessage(\"micMutedModalTitle\"),\n        chrome.i18n.getMessage(\"micMutedModalDescription\"),\n        chrome.i18n.getMessage(\"micMutedModalAction\"),\n        chrome.i18n.getMessage(\"micMutedModalCancel\"),\n        () => {\n          chrome.runtime.sendMessage({\n            type: \"desktop-capture\",\n            region:\n              contentStateRef.current.recordingType === \"region\" ? true : false,\n            customRegion: contentStateRef.current.customRegion,\n            offscreenRecording: contentStateRef.current.offscreenRecording,\n            camera:\n              contentStateRef.current.recordingType === \"camera\" ? true : false,\n          });\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n\n            surface: \"default\",\n            pipEnded: false,\n          }));\n        },\n        () => {},\n        false,\n        false,\n        false,\n        false,\n        chrome.i18n.getMessage(\"noShowAgain\"),\n        () => {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            askMicrophone: false,\n          }));\n          chrome.storage.local.set({ askMicrophone: false });\n        },\n      );\n    } else {\n      chrome.runtime.sendMessage({\n        type: \"desktop-capture\",\n        region:\n          contentStateRef.current.recordingType === \"region\" ? true : false,\n        customRegion: contentStateRef.current.customRegion,\n        offscreenRecording: contentStateRef.current.offscreenRecording,\n        camera:\n          contentStateRef.current.recordingType === \"camera\" ? true : false,\n      });\n      traceStep(\"desktopCaptureSent\");\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n\n        surface: \"default\",\n        pipEnded: false,\n      }));\n    }\n  }, [contentStateRef]);\n\n  const tryRestartRecording = useCallback(() => {\n    if (!contentStateRef.current.paused) {\n      contentStateRef.current.pauseRecording();\n    }\n    contentStateRef.current.openModal(\n      chrome.i18n.getMessage(\"restartModalTitle\"),\n      chrome.i18n.getMessage(\"restartModalDescription\"),\n      chrome.i18n.getMessage(\"restartModalRestart\"),\n      chrome.i18n.getMessage(\"restartModalResume\"),\n      () => {\n        contentStateRef.current.restartRecording();\n      },\n      () => {\n        contentStateRef.current.resumeRecording();\n      },\n    );\n  }, []);\n\n  const tryDismissRecording = useCallback(() => {\n    if (contentStateRef.current.askDismiss) {\n      if (!contentStateRef.current.paused) {\n        contentStateRef.current.pauseRecording(true);\n      }\n      contentStateRef.current.openModal(\n        chrome.i18n.getMessage(\"discardModalTitle\"),\n        chrome.i18n.getMessage(\"discardModalDescription\"),\n        chrome.i18n.getMessage(\"discardModalDiscard\"),\n        chrome.i18n.getMessage(\"discardModalResume\"),\n        () => {\n          contentStateRef.current.dismissRecording();\n        },\n        () => {\n          contentStateRef.current.resumeRecording();\n        },\n      );\n    } else {\n      contentStateRef.current.dismissRecording();\n    }\n  }, [contentStateRef]);\n\n  const handleDevicePermissions = (data) => {\n    if (data && data != undefined && data.success) {\n      // I need to convert to a regular array of objects\n      const audioInput = data.audioinput;\n      const videoInput = data.videoinput;\n      const cameraPermission = data.cameraPermission;\n      const microphonePermission = data.microphonePermission;\n\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        audioInput: audioInput,\n        videoInput: videoInput,\n        cameraPermission: cameraPermission,\n        microphonePermission: microphonePermission,\n      }));\n\n      const audioInputById = Array.isArray(audioInput)\n        ? Object.fromEntries(\n            audioInput.map((device) => [device.deviceId, device.label]),\n          )\n        : {};\n      const videoInputById = Array.isArray(videoInput)\n        ? Object.fromEntries(\n            videoInput.map((device) => [device.deviceId, device.label]),\n          )\n        : {};\n\n      const defaultAudioInputLabel =\n        audioInputById[contentStateRef.current.defaultAudioInput] || \"\";\n      const defaultVideoInputLabel =\n        videoInputById[contentStateRef.current.defaultVideoInput] || \"\";\n\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        defaultAudioInputLabel:\n          defaultAudioInputLabel || prevContentState.defaultAudioInputLabel,\n        defaultVideoInputLabel:\n          defaultVideoInputLabel || prevContentState.defaultVideoInputLabel,\n      }));\n\n      chrome.storage.local.set({\n        defaultAudioInputLabel:\n          defaultAudioInputLabel ||\n          contentStateRef.current.defaultAudioInputLabel,\n        defaultVideoInputLabel:\n          defaultVideoInputLabel ||\n          contentStateRef.current.defaultVideoInputLabel,\n      });\n\n      chrome.runtime.sendMessage({\n        type: \"switch-camera\",\n        id: contentStateRef.current.defaultVideoInput,\n      });\n\n      // Check if first time setting devices\n      if (!contentStateRef.current.setDevices) {\n        // Set default devices\n        // Check if audio devices exist\n        if (audioInput.length > 0) {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            defaultAudioInput: audioInput[0].deviceId,\n            defaultAudioInputLabel: audioInput[0].label || \"\",\n            micActive: true,\n          }));\n          chrome.storage.local.set({\n            defaultAudioInput: audioInput[0].deviceId,\n            defaultAudioInputLabel: audioInput[0].label || \"\",\n            micActive: true,\n          });\n        }\n        if (videoInput.length > 0) {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            defaultVideoInput: videoInput[0].deviceId,\n            defaultVideoInputLabel: videoInput[0].label || \"\",\n            cameraActive: true,\n          }));\n          chrome.storage.local.set({\n            defaultVideoInput: videoInput[0].deviceId,\n            defaultVideoInputLabel: videoInput[0].label || \"\",\n            cameraActive: true,\n          });\n        }\n        if (audioInput.length > 0 || videoInput.length > 0) {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            setDevices: true,\n          }));\n          chrome.storage.local.set({\n            setDevices: true,\n          });\n        }\n      }\n    } else {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        cameraPermission: false,\n        microphonePermission: false,\n      }));\n      if (contentStateRef.current.askForPermissions) {\n        contentStateRef.current.openModal(\n          chrome.i18n.getMessage(\"permissionsModalTitle\"),\n          chrome.i18n.getMessage(\"permissionsModalDescription\"),\n          chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n          chrome.i18n.getMessage(\"permissionsModalNoShowAgain\"),\n          () => {},\n          () => {\n            noMorePermissions();\n          },\n          chrome.runtime.getURL(\"assets/helper/permissions.webp\"),\n          chrome.i18n.getMessage(\"learnMoreDot\"),\n          URL2,\n          true,\n          false,\n        );\n      }\n    }\n  };\n\n  const noMorePermissions = useCallback(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      askForPermissions: false,\n    }));\n    chrome.storage.local.set({ askForPermissions: false });\n  });\n\n  useEffect(() => {\n    const handleMessage = (event) => {\n      if (event.data.type === \"screenity-permissions\") {\n        handleDevicePermissions(event.data);\n      } else if (event.data.type === \"screenity-permissions-loaded\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          permissionsLoaded: true,\n        }));\n      }\n    };\n\n    window.addEventListener(\"message\", handleMessage);\n\n    return () => {\n      window.removeEventListener(\"message\", handleMessage);\n    };\n  }, []);\n\n  // These settings are available throughout the Content\n  const [contentState, setContentStateInternal] = useState({\n    color: \"#4597F7\",\n    strokeWidth: 2,\n    drawingMode: false,\n    tool: \"pen\",\n    undoStack: [],\n    redoStack: [],\n    canvas: null,\n    swatch: 1,\n    time: 0,\n    timer: 0,\n    processingProgress: 0,\n    recording: false,\n    startRecording: startRecording,\n    restartRecording: restartRecording,\n    stopRecording: stopRecording,\n    pauseRecording: pauseRecording,\n    resumeRecording: resumeRecording,\n    dismissRecording: dismissRecording,\n    startStreaming: startStreaming,\n    setToolbarMode: null,\n    openModal: null,\n    openToast: null,\n    timeWarning: false,\n    audioInput: [],\n    videoInput: [],\n    setDevices: false,\n    defaultAudioInput: \"none\",\n    defaultVideoInput: \"none\",\n    defaultAudioInputLabel: \"\",\n    defaultVideoInputLabel: \"\",\n    cameraActive: false,\n    micActive: false,\n    sortBy: \"newest\",\n    paused: false,\n    toolbarPosition: {\n      left: true,\n      right: false,\n      bottom: true,\n      top: false,\n      offsetX: 0,\n      offsetY: 100,\n    },\n    popupPosition: {\n      left: false,\n      right: true,\n      top: true,\n      bottom: false,\n      offsetX: 0,\n      offsetY: 0,\n      fixed: true,\n    },\n    cameraDimensions: {\n      size: 200,\n      x: 100,\n      y: 100,\n    },\n    cameraFlipped: false,\n    backgroundEffect: \"blur\",\n    backgroundEffectsActive: false,\n    countdown: true,\n    showExtension: false,\n    showPopup: false,\n    blurMode: false,\n    recordingType: \"screen\",\n    customRegion: false,\n    regionWidth: 800,\n    surface: \"default\",\n    regionHeight: 500,\n    regionX: 100,\n    regionY: 100,\n    fromRegion: false,\n    cropTarget: null,\n    hideToolbar: false,\n    alarm: false,\n    alarmTime: 5 * 60,\n    fromAlarm: false,\n    pendingRecording: false,\n    askForPermissions: true,\n    cameraPermission: true,\n    microphonePermission: true,\n    askMicrophone: true,\n    recordingShortcut: \"⌥⇧W\",\n    recordingShortcut: \"⌥⇧D\",\n    toggleDrawingModeShortcut: \"\",\n    toggleBlurModeShortcut: \"\",\n    toggleCursorModeShortcut: \"\",\n    cursorMode: \"none\",\n    cursorEffects: [],\n    shape: \"rectangle\",\n    shapeFill: false,\n    pushToTalk: false,\n    zoomEnabled: false,\n    offscreenRecording: false,\n    isAddingImage: false,\n    pipEnded: false,\n    tabCaptureFrame: false,\n    showOnboardingArrow: false,\n    offline: false,\n    updateChrome: false,\n    permissionsChecked: false,\n    permissionsLoaded: false,\n    parentRef: null,\n    shadowRef: null,\n    settingsOpen: false,\n    hideUIAlerts: false,\n    toolbarHover: false,\n    hideUI: false,\n    bigTab: \"record\",\n    askDismiss: true,\n    quality: \"max\",\n    systemAudio: true,\n    backup: false,\n    backupSetup: false,\n    openWarning: false,\n    hasOpenedBefore: false,\n    qualityValue: \"1080p\",\n    fpsValue: \"30\",\n    fastRecorderBeta: null,\n    fastRecorderStatus: null,\n    useWebCodecsRecorder: false,\n    countdownActive: false,\n    countdownCancelled: false,\n    multiMode: false,\n    isCountdownVisible: false,\n    multiSceneCount: 0,\n    preparingRecording: false,\n    wasLoggedIn: false,\n    hasSeenInstantModeModal: false,\n    instantMode: false,\n    onboarding: false,\n    showProSplash: false,\n    hasSubscribedBefore: false,\n    startRecordingAfterCountdown: () => {\n      if (!contentStateRef.current.countdownCancelled) {\n        contentStateRef.current.startRecording();\n      }\n    },\n    cancelCountdown: () => {\n      setStartFlowOutcome(\"cancelled\");\n      // Eagerly clear recording flags in storage so the action button never\n      // sees a stale isRecordingActive state between now and when the\n      // background processes dismiss-recording-tab.\n      chrome.storage.local.set({\n        pendingRecording: false,\n        recording: false,\n        restarting: false,\n      });\n      chrome.runtime.sendMessage({ type: \"diag-countdown-cancelled\" }).catch(() => {});\n      setContentState((prev) => ({\n        ...prev,\n        countdownActive: false,\n        countdownCancelled: true,\n        isCountdownVisible: false,\n        recording: false,\n        showPopup: true,\n        showExtension: true,\n      }));\n      // Call dismissRecording to ensure everything is properly cleaned up\n      contentStateRef.current.dismissRecording();\n    },\n    resetCountdown: () => {\n      setContentState((prev) => ({\n        ...prev,\n        countdownCancelled: false,\n      }));\n    },\n    onCountdownFinished: () => {\n      if (!contentStateRef.current?.countdownCancelled && isTargetTab()) {\n        suppressStartBeepRef.current = true;\n        playBeep(startBeepRef, \"assets/sounds/beep2.mp3\");\n      }\n    },\n  });\n  contentStateRef.current = contentState;\n\n  setContentState = (updater) => {\n    if (typeof updater === \"function\") {\n      setContentStateInternal((prevState) => {\n        const newState = updater(prevState);\n        contentStateRef.current = newState;\n        return newState;\n      });\n    } else {\n      setContentStateInternal(updater);\n      contentStateRef.current = updater;\n    }\n  };\n\n  const playBeep = (ref, filename) => {\n    if (!ref.current) {\n      ref.current = new Audio(chrome.runtime.getURL(filename));\n    }\n    const audio = ref.current;\n    audio.volume = 0.5;\n    try {\n      audio.currentTime = 0;\n    } catch {}\n    const playPromise = audio.play();\n    if (playPromise?.catch) {\n      playPromise.catch((error) => {\n        console.warn(\"Beep playback failed:\", error);\n      });\n    }\n  };\n\n  useEffect(() => {\n    chrome.runtime.sendMessage({ type: \"sync-recording-state\" }, (state) => {\n      if (!state) return;\n      setContentState((prev) => ({ ...prev, ...state }));\n      setTimeout(() => {\n        hydratedRef.current = true;\n        prevRecordingRef.current = Boolean(state.recording);\n      }, 0);\n    });\n  }, []);\n\n  useEffect(() => {\n    if (!contentState.preparingRecording) return;\n    let canceled = false;\n    let pollTimer = null;\n\n    const getStatus = async () => {\n      const { fastRecorderActiveRecordingId, postStopRecordingId } =\n        await chrome.storage.local.get([\n          \"fastRecorderActiveRecordingId\",\n          \"postStopRecordingId\",\n        ]);\n      const recordingId =\n        fastRecorderActiveRecordingId || postStopRecordingId || null;\n      if (!recordingId) return null;\n      const key = `freeFinalizeStatus:${recordingId}`;\n      const res = await chrome.storage.local.get([key]);\n      return res[key] || null;\n    };\n\n    const applyStatus = (status) => {\n      if (!status || canceled) return;\n      const pct =\n        typeof status.percent === \"number\" ? Math.round(status.percent) : 0;\n      setContentState((prev) => ({\n        ...prev,\n        processingProgress: pct,\n      }));\n    };\n\n    const onChanged = (changes, area) => {\n      if (area !== \"local\") return;\n      const entry = Object.keys(changes).find((k) =>\n        k.startsWith(\"freeFinalizeStatus:\"),\n      );\n      if (!entry) return;\n      applyStatus(changes[entry].newValue);\n    };\n\n    chrome.storage.onChanged.addListener(onChanged);\n\n    (async () => {\n      const status = await getStatus();\n      applyStatus(status);\n      pollTimer = setInterval(async () => {\n        const s = await getStatus();\n        applyStatus(s);\n      }, 500);\n    })();\n\n    return () => {\n      canceled = true;\n      chrome.storage.onChanged.removeListener(onChanged);\n      if (pollTimer) clearInterval(pollTimer);\n    };\n  }, [contentState.preparingRecording]);\n\n  // Stuck-state detector (diagnostics only).\n  // Writes to startFlowTrace if pending/preparing stays active too long.\n  useEffect(() => {\n    const PENDING_TIMEOUT_MS = 30000;\n    const PREPARING_TIMEOUT_MS = 45000;\n\n    if (contentState.recording) return; // not stuck if recording started\n\n    let timer = null;\n    let fired = false;\n\n    if (\n      contentState.pendingRecording &&\n      !contentState.preparingRecording\n    ) {\n      timer = setTimeout(() => {\n        if (fired) return;\n        fired = true;\n        setStartFlowOutcome(\"stuck\", {\n          stuck: {\n            state: \"pending\",\n            since: Date.now() - PENDING_TIMEOUT_MS,\n            durationMs: PENDING_TIMEOUT_MS,\n          },\n        });\n      }, PENDING_TIMEOUT_MS);\n    } else if (contentState.preparingRecording) {\n      timer = setTimeout(() => {\n        if (fired) return;\n        fired = true;\n        setStartFlowOutcome(\"stuck\", {\n          stuck: {\n            state: \"preparing\",\n            since: Date.now() - PREPARING_TIMEOUT_MS,\n            durationMs: PREPARING_TIMEOUT_MS,\n          },\n        });\n      }, PREPARING_TIMEOUT_MS);\n    }\n\n    return () => {\n      if (timer) clearTimeout(timer);\n    };\n  }, [\n    contentState.pendingRecording,\n    contentState.preparingRecording,\n    contentState.recording,\n  ]);\n\n  useEffect(() => {\n    const isRecording = Boolean(contentState.recording);\n    if (!hydratedRef.current) {\n      prevRecordingRef.current = isRecording;\n      return;\n    }\n    if (prevRecordingRef.current === null) {\n      prevRecordingRef.current = isRecording;\n      return;\n    }\n    if (!isTargetTab()) {\n      prevRecordingRef.current = isRecording;\n      return;\n    }\n\n    if (prevRecordingRef.current === false && isRecording === true) {\n      if (\n        recordingBeepTabIdRef.current != null &&\n        tabIdRef.current != null &&\n        recordingBeepTabIdRef.current !== tabIdRef.current\n      ) {\n        prevRecordingRef.current = isRecording;\n        return;\n      }\n      const startTime = recordingStartTimeRef.current;\n      const hasStartTime = Number.isFinite(startTime) && startTime > 0;\n      const sessionMarker = hasStartTime ? startTime : Date.now();\n      const isNewSession = sessionMarker !== lastBeepStartTimeRef.current;\n      if (!isNewSession) {\n        prevRecordingRef.current = isRecording;\n        return;\n      }\n      lastBeepStartTimeRef.current = sessionMarker;\n      if (suppressStartBeepRef.current) {\n        suppressStartBeepRef.current = false;\n      } else {\n        playBeep(startBeepRef, \"assets/sounds/beep2.mp3\");\n      }\n    } else if (prevRecordingRef.current === true && isRecording === false) {\n      if (suppressStopBeepRef.current) {\n        suppressStopBeepRef.current = false;\n      } else {\n        playBeep(stopBeepRef, \"assets/sounds/beep.mp3\");\n      }\n    }\n\n    prevRecordingRef.current = isRecording;\n  }, [contentState.recording, isTargetTab]);\n\n  // Check Chrome version\n  useEffect(() => {\n    const version = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n\n    const MIN_CHROME_VERSION = 109;\n\n    if (version && parseInt(version[2], 10) < MIN_CHROME_VERSION) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        updateChrome: true,\n      }));\n    }\n  }, []);\n\n  useEffect(() => {\n    if (typeof contentState.openWarning === \"function\") {\n      const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n      const warningList = [\n        \"youtube.com\",\n        \"meet.google.com\",\n        \"zoom.us\",\n        \"hangouts.google.com\",\n        \"teams.microsoft.com\",\n        \"web.whatsapp.com\",\n        \"web.skype.com\",\n        \"discord.com\",\n        \"vimeo.com\",\n      ];\n\n      if (\n        !contentState.recording &&\n        isMac &&\n        warningList.some((el) => window.location.href.includes(el)) &&\n        contentState.recordingType != \"region\" &&\n        contentState.recordingType != \"camera\"\n      ) {\n        contentState.openWarning(\n          chrome.i18n.getMessage(\"audioWarningTitle\"),\n          chrome.i18n.getMessage(\n            \"audioWarningDescription\",\n            chrome.i18n.getMessage(\"tabType\"),\n          ),\n          \"AudioIcon\",\n          10000,\n        );\n        // Check if url contains \"playground.html\" and \"chrome-extension://\"\n      } else if (\n        window.location.href.includes(\"playground.html\") &&\n        window.location.href.includes(\"chrome-extension://\") &&\n        !contentState.recording\n      ) {\n        contentState.openWarning(\n          chrome.i18n.getMessage(\"extensionNotSupportedTitle\"),\n          chrome.i18n.getMessage(\"extensionNotSupportedDescription\"),\n          \"NotSupportedIcon\",\n          10000,\n        );\n      }\n    }\n  }, [\n    contentState.openWarning,\n    contentState.recording,\n    contentState.recordingType,\n  ]);\n\n  useEffect(() => {\n    if (!contentState) return;\n    if (typeof contentState.openModal === \"function\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        tryRestartRecording: tryRestartRecording,\n        tryDismissRecording: tryDismissRecording,\n      }));\n    }\n  }, [contentState.openModal]);\n\n  const updateTimerFromStorage = useCallback(async () => {\n    const seq = ++timerReadSeqRef.current;\n    const { recording, recordingStartTime, paused, pausedAt, totalPausedMs } =\n      await chrome.storage.local.get([\n        \"recording\",\n        \"recordingStartTime\",\n        \"paused\",\n        \"pausedAt\",\n        \"totalPausedMs\",\n      ]);\n    if (seq !== timerReadSeqRef.current) return;\n\n    if (!recording || !recordingStartTime) {\n      setTimer(0);\n      return;\n    }\n\n    const now = Date.now();\n    const basePaused = totalPausedMs || 0;\n    const extraPaused = paused && pausedAt ? Math.max(0, now - pausedAt) : 0;\n    const elapsedSeconds = Math.max(\n      0,\n      Math.floor((now - recordingStartTime - basePaused - extraPaused) / 1000),\n    );\n\n    if (contentStateRef.current?.alarm) {\n      const alarmTime = contentStateRef.current?.alarmTime || 0;\n      const nextRemaining = Math.max(0, alarmTime - elapsedSeconds);\n      setTimer((prev) => (prev === nextRemaining ? prev : nextRemaining));\n      return;\n    }\n\n    setTimer((prev) => (prev === elapsedSeconds ? prev : elapsedSeconds));\n  }, []);\n\n  useEffect(() => {\n    updateTimerFromStorage();\n    const interval = setInterval(() => {\n      updateTimerFromStorage();\n    }, 1000);\n    const handleVisibility = () => {\n      if (!document.hidden) {\n        updateTimerFromStorage();\n      }\n    };\n    const handleFocus = () => updateTimerFromStorage();\n    document.addEventListener(\"visibilitychange\", handleVisibility);\n    window.addEventListener(\"focus\", handleFocus);\n    return () => {\n      clearInterval(interval);\n      document.removeEventListener(\"visibilitychange\", handleVisibility);\n      window.removeEventListener(\"focus\", handleFocus);\n    };\n  }, [updateTimerFromStorage]);\n\n  useEffect(() => {\n    const onChanged = (changes, area) => {\n      if (area !== \"local\") return;\n      let shouldUpdateTimer = false;\n\n      if (changes.activeTab) {\n        activeTabRef.current = changes.activeTab.newValue ?? null;\n      }\n      if (changes.tabRecordedID) {\n        tabRecordedIdRef.current = changes.tabRecordedID.newValue ?? null;\n      }\n      if (changes.recordingUiTabId) {\n        recordingUiTabRef.current = changes.recordingUiTabId.newValue ?? null;\n      }\n      if (changes.recordingStartTime) {\n        recordingStartTimeRef.current =\n          changes.recordingStartTime.newValue ?? null;\n      }\n      if (changes.recordingBeepTabId) {\n        recordingBeepTabIdRef.current =\n          changes.recordingBeepTabId.newValue ?? null;\n      }\n      if (\n        changes.screenityToken ||\n        changes.screenityUser ||\n        changes.isSubscribed ||\n        changes.proSubscription ||\n        changes.lastAuthCheck ||\n        changes.isLoggedIn\n      ) {\n        // Debounce: multiple auth-related storage keys change in quick\n        // succession (e.g. loginWithWebsite writes isLoggedIn + isSubscribed\n        // + lastAuthCheck). Coalesce into one verify call.\n        clearTimeout(verifyDebounceRef.current);\n        verifyDebounceRef.current = setTimeout(verifyUser, 2000);\n      }\n      if (changes.recordingNow) {\n        shouldUpdateTimer = true;\n      }\n      if (changes.paused) {\n        setContentState((prev) => ({\n          ...prev,\n          paused: Boolean(changes.paused.newValue),\n        }));\n        shouldUpdateTimer = true;\n      }\n      if (changes.recording) {\n        const isRecording = Boolean(changes.recording.newValue);\n        if (isRecording && !isTargetTab()) {\n          setContentState((prev) => ({\n            ...prev,\n            recording: false,\n          }));\n          return;\n        }\n        const shouldHideCountdown =\n          isRecording &&\n          !contentStateRef.current?.isCountdownVisible &&\n          !contentStateRef.current?.countdownActive;\n        setContentState((prev) => ({\n          ...prev,\n          recording: isRecording,\n          ...(shouldHideCountdown\n            ? {\n                countdownActive: false,\n                isCountdownVisible: false,\n              }\n            : {}),\n        }));\n        shouldUpdateTimer = true;\n      }\n      if (changes.cursorEffects) {\n        const nextEffects = normalizeCursorEffects(\n          changes.cursorEffects.newValue,\n        );\n        const fallbackMode =\n          changes.cursorMode?.newValue ||\n          contentStateRef.current?.cursorMode ||\n          \"none\";\n        const nextMode = deriveCursorMode(nextEffects, fallbackMode);\n        setContentState((prev) => ({\n          ...prev,\n          cursorEffects: nextEffects,\n          cursorMode: nextMode,\n        }));\n      } else if (changes.cursorMode) {\n        const mode = changes.cursorMode.newValue || \"none\";\n        const fallbackEffects = mode !== \"none\" ? [mode] : [];\n        setContentState((prev) => ({\n          ...prev,\n          cursorMode: mode,\n          cursorEffects:\n            Array.isArray(prev.cursorEffects) && prev.cursorEffects.length > 0\n              ? prev.cursorEffects\n              : fallbackEffects,\n        }));\n      }\n      if (\n        changes.recordingNow ||\n        changes.paused ||\n        changes.recording ||\n        changes.recordingStartTime ||\n        changes.pausedAt ||\n        changes.totalPausedMs\n      ) {\n        shouldUpdateTimer = true;\n      }\n      if (shouldUpdateTimer) {\n        updateTimerFromStorage();\n      }\n    };\n\n    chrome.storage.onChanged.addListener(onChanged);\n    return () => chrome.storage.onChanged.removeListener(onChanged);\n  }, [isTargetTab, updateTimerFromStorage, verifyUser]);\n\n  useEffect(() => {\n    if (!contentState.customRegion) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        cropTarget: null,\n      }));\n    }\n  }, [contentState.customRegion]);\n\n  // Check when hiding the toolbar\n  useEffect(() => {\n    if (contentState.hideToolbar && contentState.hideUI) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        drawingMode: false,\n        blurMode: false,\n      }));\n    }\n  }, [contentState.hideToolbar, contentState.hideUI]);\n\n  useEffect(() => {\n    if (window.__screenitySetupHandlersInitialized) return;\n    window.__screenitySetupHandlersInitialized = true;\n    setupHandlers();\n  }, []);\n\n  useEffect(() => {\n    chrome.storage.local.set({\n      pendingRecording: contentState.pendingRecording,\n    });\n  }, [contentState.pendingRecording]);\n\n  // Check if user has enough RAM to record for each quality option\n  useEffect(() => {\n    if (!contentState.qualityValue) {\n      const suggested = \"1080p\"; // safe and high enough quality\n      setContentState((prev) => ({ ...prev, qualityValue: suggested }));\n      chrome.storage.local.set({ qualityValue: suggested });\n    }\n  }, []);\n\n  useEffect(() => {\n    if (contentState.pushToTalk) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        micActive: false,\n      }));\n\n      chrome.storage.local.set({\n        micActive: false,\n      });\n\n      chrome.runtime.sendMessage({\n        type: \"set-mic-active-tab\",\n        active: false,\n        defaultAudioInput: contentState.defaultAudioInput,\n      });\n    }\n  }, [contentState.pushToTalk]);\n\n  useEffect(() => {\n    if (contentState.backgroundEffectsActive) {\n      chrome.runtime.sendMessage({ type: \"background-effects-active\" });\n    } else {\n      chrome.runtime.sendMessage({ type: \"background-effects-inactive\" });\n    }\n  }, [contentState.backgroundEffectsActive]);\n\n  useEffect(() => {\n    if (contentState.backgroundEffectsActive) {\n      chrome.runtime.sendMessage({\n        type: \"set-background-effect\",\n        effect: contentState.backgroundEffect,\n      });\n    }\n  }, [contentState.backgroundEffect, contentState.backgroundEffectsActive]);\n\n  // Programmatically add custom scrollbars\n  useEffect(() => {\n    if (!contentState.parentRef) return;\n\n    // Check if on mac\n    const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n    if (isMac) return;\n\n    const parentDiv = contentState.parentRef;\n\n    const elements = parentDiv.querySelectorAll(\"*\");\n    elements.forEach((element) => {\n      element.classList.add(\"screenity-scrollbar\");\n    });\n\n    const observer = new MutationObserver((mutationsList) => {\n      for (const mutation of mutationsList) {\n        if (mutation.type === \"childList\") {\n          const addedNodes = Array.from(mutation.addedNodes);\n          const removedNodes = Array.from(mutation.removedNodes);\n\n          addedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.add(\"screenity-scrollbar\");\n            }\n          });\n\n          removedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.remove(\"screenity-scrollbar\");\n            }\n          });\n        }\n      }\n    });\n\n    observer.observe(parentDiv, { childList: true, subtree: true });\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [contentState.parentRef]);\n\n  // Programmatically add custom scrollbars\n  useEffect(() => {\n    if (!contentState.shadowRef) return;\n\n    // Check if on mac\n    const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n    if (isMac) return;\n\n    const shadowRoot = contentState.shadowRef.shadowRoot;\n\n    const elements = shadowRoot.querySelectorAll(\"*\");\n    elements.forEach((element) => {\n      element.classList.add(\"screenity-scrollbar\");\n    });\n\n    const observer = new MutationObserver((mutationsList) => {\n      for (const mutation of mutationsList) {\n        if (mutation.type === \"childList\") {\n          const addedNodes = Array.from(mutation.addedNodes);\n          const removedNodes = Array.from(mutation.removedNodes);\n\n          addedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.add(\"screenity-scrollbar\");\n            }\n          });\n\n          removedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.remove(\"screenity-scrollbar\");\n            }\n          });\n        }\n      }\n    });\n\n    observer.observe(shadowRoot, { childList: true, subtree: true });\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [\n    contentState.parentRef,\n    contentState.shadowRef,\n    contentState.bigTab,\n    contentState.recordingType,\n  ]);\n\n  useEffect(() => {\n    if (!contentState.hideUI) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        hideUIAlerts: false,\n        hideToolbar: false,\n        toolbarHover: false,\n      }));\n    }\n  }, [contentState.hideUI]);\n\n  useEffect(() => {\n    updateFromStorage();\n  }, []);\n\n  // (No storage fallback listener here; use direct messaging flow)\n\n  return (\n    // this is the provider providing state\n    <contentStateContext.Provider\n      value={[contentState, setContentState, timer, setTimer]}\n    >\n      {props.children}\n      <Shortcuts shortcuts={contentState.shortcuts} />\n      {process.env.SCREENITY_DEV_MODE === \"true\" && (\n        <DevHUD contentStateRef={contentStateRef} setContentState={setContentState} />\n      )}\n    </contentStateContext.Provider>\n  );\n};\n\nexport default ContentState;\n"
  },
  {
    "path": "src/pages/Content/context/messaging/handlers.js",
    "content": "// src/content/handlers/recordingHandlers.js\nimport {\n  registerMessage,\n  messageRouter,\n} from \"../../../../messaging/messageRouter\";\nimport { setContentState, contentStateRef } from \"../ContentState\";\nimport { updateFromStorage } from \"../utils/updateFromStorage\";\n\nimport { checkAuthStatus } from \"../utils/checkAuthStatus\";\nimport { traceStep, setStartFlowOutcome } from \"../../../utils/startFlowTrace\";\nimport JSZip from \"jszip\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nconst getState = () => contentStateRef.current;\n\nexport const setupHandlers = () => {\n  if (window.__screenitySetupHandlersRan) return;\n  window.__screenitySetupHandlersRan = true;\n  let lastToggleDrawingAt = 0;\n  const TOGGLE_DRAWING_COOLDOWN_MS = 400;\n  let projectReadySeq = 0;\n  const LOCAL_PLAYBACK_MAX_BYTES = 250 * 1024 * 1024;\n  let latestLocalPlaybackOffer = null;\n  let latestLocalPlaybackProjectId = null;\n  let latestLocalPlaybackSceneId = null;\n  let localPlaybackBuildPromise = null;\n  let localPlaybackBuildOfferId = null;\n  let activeLocalPlaybackSource = null;\n  const TRUSTED_APP_ORIGIN = (() => {\n    try {\n      const appBase = process.env.SCREENITY_APP_BASE;\n      return appBase ? new URL(appBase).origin : null;\n    } catch {\n      return null;\n    }\n  })();\n\n  const getProjectMessageTargetOrigin = () => {\n    if (!TRUSTED_APP_ORIGIN) return null;\n    return window.location.origin === TRUSTED_APP_ORIGIN\n      ? TRUSTED_APP_ORIGIN\n      : null;\n  };\n\n  const postProjectHandoff = (payload) => {\n    const targetOrigin = getProjectMessageTargetOrigin();\n    if (!targetOrigin) {\n      console.warn(\n        \"[Screenity][Content] Ignoring project handoff on untrusted origin\",\n        {\n          source: payload?.source || \"unknown\",\n          pageOrigin: window.location.origin,\n          trustedOrigin: TRUSTED_APP_ORIGIN,\n          projectId: payload?.projectId || null,\n        },\n      );\n      return false;\n    }\n\n    window.postMessage(payload, targetOrigin);\n    // Replay once shortly after first post to reduce race conditions with late listeners.\n    setTimeout(() => {\n      window.postMessage(\n        {\n          ...payload,\n          replay: true,\n          replayAt: Date.now(),\n        },\n        targetOrigin,\n      );\n    }, 250);\n    return true;\n  };\n\n  const revokeActiveLocalPlaybackSource = (reason = \"unknown\") => {\n    if (activeLocalPlaybackSource?.url) {\n      URL.revokeObjectURL(activeLocalPlaybackSource.url);\n      console.info(\"[Screenity][Content] Revoked local screen playback URL\", {\n        reason,\n        offerId: activeLocalPlaybackSource.offerId || null,\n      });\n    }\n    activeLocalPlaybackSource = null;\n  };\n\n  const markLocalPlaybackFallback = async ({\n    offerId,\n    projectId,\n    sceneId,\n    reason,\n  }) => {\n    if (!offerId) return;\n    try {\n      await chrome.runtime.sendMessage({\n        type: \"cloud-local-playback-mark-fallback\",\n        offerId,\n        projectId: projectId || null,\n        sceneId: sceneId || null,\n        reason: reason || \"unknown\",\n      });\n    } catch {\n      // best effort\n    }\n  };\n\n  const fetchLocalPlaybackSourceFromExtension = async ({\n    offerId,\n    projectId,\n    sceneId,\n  }) => {\n    const offerRes = await chrome.runtime.sendMessage({\n      type: \"cloud-local-playback-get-offer\",\n      offerId,\n      projectId,\n      sceneId,\n    });\n    if (!offerRes?.ok || !offerRes.offer) {\n      throw new Error(\"local-playback-offer-unavailable\");\n    }\n    const offer = offerRes.offer;\n    if (\n      !offer.chunkCount ||\n      !offer.estimatedBytes ||\n      offer.estimatedBytes > LOCAL_PLAYBACK_MAX_BYTES\n    ) {\n      throw new Error(\"local-playback-offer-too-large-or-empty\");\n    }\n\n    const parts = [];\n    for (let i = 0; i < offer.chunkCount; i += 1) {\n      // eslint-disable-next-line no-await-in-loop\n      const chunkRes = await chrome.runtime.sendMessage({\n        type: \"cloud-local-playback-read-chunk\",\n        offerId: offer.offerId,\n        projectId: offer.projectId,\n        sceneId: offer.sceneId,\n        index: i,\n      });\n      if (!chunkRes?.ok || !chunkRes.chunk?.base64) {\n        throw new Error(`local-playback-chunk-read-failed:${i}`);\n      }\n      const binary = atob(chunkRes.chunk.base64);\n      const bytes = new Uint8Array(binary.length);\n      for (let j = 0; j < binary.length; j += 1) {\n        bytes[j] = binary.charCodeAt(j);\n      }\n      const mimeType = chunkRes.chunk.mimeType || \"video/webm\";\n      parts.push(new Blob([bytes], { type: mimeType }));\n    }\n\n    const blob = new Blob(parts, {\n      type: parts[0]?.type || \"video/webm\",\n    });\n    const url = URL.createObjectURL(blob);\n    return {\n      offer,\n      url,\n      size: blob.size || 0,\n      mimeType: blob.type || \"video/webm\",\n      chunkCount: parts.length,\n    };\n  };\n\n  const ensureLocalPlaybackReady = async ({ projectId, sceneId, offer }) => {\n    if (!offer?.offerId) {\n      throw new Error(\"local-playback-offer-missing\");\n    }\n\n    if (\n      activeLocalPlaybackSource?.offerId === offer.offerId &&\n      activeLocalPlaybackSource?.url\n    ) {\n      return activeLocalPlaybackSource;\n    }\n\n    if (\n      localPlaybackBuildPromise &&\n      localPlaybackBuildOfferId === offer.offerId\n    ) {\n      return localPlaybackBuildPromise;\n    }\n\n    localPlaybackBuildOfferId = offer.offerId;\n    localPlaybackBuildPromise = (async () => {\n      const source = await fetchLocalPlaybackSourceFromExtension({\n        offerId: offer.offerId,\n        projectId,\n        sceneId,\n      });\n\n      revokeActiveLocalPlaybackSource(\"new-offer\");\n      activeLocalPlaybackSource = {\n        offerId: source.offer.offerId,\n        projectId: source.offer.projectId || projectId || null,\n        sceneId: source.offer.sceneId || sceneId || null,\n        url: source.url,\n        mimeType: source.mimeType,\n        size: source.size,\n        chunkCount: source.chunkCount,\n        expiresAt: source.offer.expiresAt || null,\n      };\n\n      try {\n        await chrome.runtime.sendMessage({\n          type: \"cloud-local-playback-mark-used\",\n          offerId: source.offer.offerId,\n          projectId: source.offer.projectId || null,\n          sceneId: source.offer.sceneId || null,\n          usedBy: \"app-editor\",\n        });\n      } catch {\n        // best effort\n      }\n\n      return activeLocalPlaybackSource;\n    })();\n\n    try {\n      const ready = await localPlaybackBuildPromise;\n      return ready;\n    } finally {\n      localPlaybackBuildPromise = null;\n      localPlaybackBuildOfferId = null;\n    }\n  };\n\n  const postLocalPlaybackHandoff = ({\n    projectId,\n    sceneId,\n    offer,\n    readySource = null,\n    fallbackReason = null,\n    forceRefresh = true,\n  }) =>\n    postProjectHandoff({\n      source: \"update-project-ready-local-playback\",\n      projectId: projectId || null,\n      sceneId: sceneId || null,\n      forceRefresh,\n      handoffAt: Date.now(),\n      localPlayback: {\n        available: Boolean(offer?.offerId),\n        trackType: \"screen\",\n        offerId: offer?.offerId || null,\n        chunkCount: offer?.chunkCount || 0,\n        estimatedBytes: offer?.estimatedBytes || 0,\n        expiresAt: offer?.expiresAt || null,\n        source: offer?.source || \"indexeddb-screen-chunks\",\n        ready: Boolean(readySource?.url),\n        url: readySource?.url || null,\n        mimeType: readySource?.mimeType || null,\n        localBytes: readySource?.size || null,\n        fallbackReason: fallbackReason || null,\n      },\n    });\n\n  const onWindowProjectMessage = (event) => {\n    if (event.source !== window) return;\n    if (event.origin !== TRUSTED_APP_ORIGIN) return;\n    const data = event?.data || {};\n    if (data?.type !== \"screenity-local-playback-request\") return;\n\n    const requestedProjectId = data?.projectId || null;\n    const requestedSceneId = data?.sceneId || null;\n    const requestId = data?.requestId || null;\n    const offer = latestLocalPlaybackOffer;\n\n    if (\n      !offer?.offerId ||\n      !offer.available ||\n      !requestedProjectId ||\n      requestedProjectId !== latestLocalPlaybackProjectId ||\n      (requestedSceneId &&\n        latestLocalPlaybackSceneId &&\n        requestedSceneId !== latestLocalPlaybackSceneId)\n    ) {\n      postProjectHandoff({\n        source: \"screenity-local-playback-response\",\n        requestId,\n        projectId: requestedProjectId,\n        sceneId: requestedSceneId,\n        localPlayback: {\n          available: false,\n          trackType: \"screen\",\n          fallbackReason: \"offer-unavailable\",\n        },\n      });\n      return;\n    }\n\n    void ensureLocalPlaybackReady({\n      projectId: requestedProjectId,\n      sceneId: requestedSceneId || latestLocalPlaybackSceneId,\n      offer,\n    })\n      .then((readySource) => {\n        console.info(\"[Screenity][Content] Local screen playback used\", {\n          projectId: requestedProjectId,\n          sceneId: requestedSceneId || latestLocalPlaybackSceneId || null,\n          offerId: offer.offerId,\n          bytes: readySource?.size || 0,\n        });\n        postProjectHandoff({\n          source: \"screenity-local-playback-response\",\n          requestId,\n          projectId: requestedProjectId,\n          sceneId: requestedSceneId || latestLocalPlaybackSceneId || null,\n          forceRefresh: true,\n          localPlayback: {\n            available: true,\n            ready: true,\n            trackType: \"screen\",\n            offerId: offer.offerId,\n            url: readySource?.url || null,\n            mimeType: readySource?.mimeType || null,\n            localBytes: readySource?.size || null,\n            chunkCount: offer.chunkCount || 0,\n            estimatedBytes: offer.estimatedBytes || 0,\n            expiresAt: offer.expiresAt || null,\n            source: offer.source || \"indexeddb-screen-chunks\",\n          },\n        });\n      })\n      .catch((err) => {\n        const reason = err?.message || \"local-playback-build-failed\";\n        console.warn(\n          \"[Screenity][Content] Local screen playback fallback\",\n          {\n            projectId: requestedProjectId,\n            sceneId: requestedSceneId || latestLocalPlaybackSceneId || null,\n            offerId: offer.offerId,\n            reason,\n          },\n        );\n        void markLocalPlaybackFallback({\n          offerId: offer.offerId,\n          projectId: requestedProjectId,\n          sceneId: requestedSceneId || latestLocalPlaybackSceneId || null,\n          reason,\n        });\n        postProjectHandoff({\n          source: \"screenity-local-playback-response\",\n          requestId,\n          projectId: requestedProjectId,\n          sceneId: requestedSceneId || latestLocalPlaybackSceneId || null,\n          forceRefresh: true,\n          localPlayback: {\n            available: true,\n            ready: false,\n            trackType: \"screen\",\n            offerId: offer.offerId,\n            fallbackReason: reason,\n          },\n        });\n      });\n  };\n\n  window.addEventListener(\"message\", onWindowProjectMessage);\n  window.addEventListener(\"beforeunload\", () => {\n    revokeActiveLocalPlaybackSource(\"content-beforeunload\");\n  });\n\n  // Initialize message router\n  if (!window.__screenityHandlersInitialized) {\n    messageRouter();\n    window.__screenityHandlersInitialized = true;\n  }\n\n  // Register content message handlers\n  registerMessage(\"time\", () => {\n    // Timer is driven by ContentState's storage-based tick.\n    // Ignore external timer pushes to avoid jitter/skips.\n  });\n\n  registerMessage(\"toggle-popup\", () => {\n    setContentState((prev) => ({\n      ...prev,\n      showExtension: !prev.showExtension,\n      hasOpenedBefore: true,\n      showPopup: true,\n    }));\n    setTimer(0);\n    updateFromStorage();\n  });\n\n  registerMessage(\"ready-to-record\", () => {\n    traceStep(\"readyToRecordReceived\");\n\n    setContentState((prev) => ({\n      ...prev,\n      showPopup: false,\n      showExtension: true,\n      preparingRecording: false,\n      pendingRecording: true,\n    }));\n    const state = getState();\n\n    if (state.countdown) {\n      // Start countdown\n      traceStep(\"countdownStart\");\n      setContentState((prev) => ({\n        ...prev,\n        countdownActive: true,\n        isCountdownVisible: true,\n        countdownCancelled: false,\n      }));\n      chrome.runtime.sendMessage({ type: \"diag-countdown-started\" }).catch(() => {});\n    } else {\n      // No countdown, start immediately. countdownCancelled is cleared\n      // in startStreaming so it can't be stale here.\n      state.startRecordingAfterCountdown();\n    }\n  });\n\n  registerMessage(\"stop-recording-tab\", () => {\n    const state = getState();\n    if (!state.recording) return;\n    state.stopRecording();\n  });\n\n  registerMessage(\"toggle-drawing-mode\", () => {\n    const now = Date.now();\n    if (now - lastToggleDrawingAt < TOGGLE_DRAWING_COOLDOWN_MS) {\n      return;\n    }\n    lastToggleDrawingAt = now;\n    if (document.hidden || !document.hasFocus()) {\n      return;\n    }\n    const nextDrawingMode = !contentStateRef.current.drawingMode;\n    setContentState((prev) => ({\n      ...prev,\n      drawingMode: nextDrawingMode,\n      blurMode: nextDrawingMode ? false : prev.blurMode,\n    }));\n\n    registerMessage(\"toggle-drawing-mode\", () => {\n      const now = Date.now();\n      if (now - lastToggleDrawingAt < TOGGLE_DRAWING_COOLDOWN_MS) return;\n      lastToggleDrawingAt = now;\n      if (document.hidden || !document.hasFocus()) return;\n\n      const nextDrawingMode = !contentStateRef.current.drawingMode;\n\n      setContentState((prev) => ({\n        ...prev,\n        drawingMode: nextDrawingMode,\n        blurMode: nextDrawingMode ? false : prev.blurMode,\n      }));\n\n      chrome.storage.local.set({\n        drawingMode: nextDrawingMode,\n        ...(nextDrawingMode ? { blurMode: false } : {}),\n      });\n    });\n  });\n\n  registerMessage(\"toggle-blur-mode\", () => {\n    const nextBlurMode = !contentStateRef.current.blurMode;\n    setContentState((prev) => ({\n      ...prev,\n      blurMode: nextBlurMode,\n      drawingMode: nextBlurMode ? false : prev.drawingMode,\n    }));\n    chrome.storage.local.set({\n      blurMode: nextBlurMode,\n      drawingMode: nextBlurMode ? false : contentStateRef.current.drawingMode,\n    });\n  });\n\n  registerMessage(\"toggle-hide-ui\", () => {\n    const nextHideUI = !contentStateRef.current.hideUI;\n    setContentState((prev) => ({\n      ...prev,\n      hideUI: nextHideUI,\n      hideToolbar: nextHideUI ? true : prev.hideToolbar,\n      hideUIAlerts: nextHideUI ? true : prev.hideUIAlerts,\n    }));\n    chrome.storage.local.set({\n      hideUI: nextHideUI,\n      ...(nextHideUI ? { hideToolbar: true, hideUIAlerts: true } : {}),\n    });\n  });\n\n  registerMessage(\"toggle-cursor-mode\", () => {\n    const state = getState();\n    const nextMode =\n      contentStateRef.current.cursorMode === \"none\" ? \"cursor\" : \"\";\n    if (state?.setToolbarMode) {\n      state.setToolbarMode(nextMode);\n    } else {\n      setContentState((prev) => ({\n        ...prev,\n        toolbarMode: nextMode,\n      }));\n    }\n  });\n\n  registerMessage(\"recording-ended\", async () => {\n    const state = getState();\n\n    // Double-check with storage before resetting UI\n    // This prevents false positives when service worker restarts with stale state\n    const { recording, recorderSession, pendingRecording } =\n      await chrome.storage.local.get([\n        \"recording\",\n        \"recorderSession\",\n        \"pendingRecording\",\n      ]);\n\n    const isActuallyRecording =\n      recording || (recorderSession && recorderSession.status === \"recording\");\n\n    // Only reset if we're truly not recording\n    if (isActuallyRecording || pendingRecording) {\n      // Recording is actually still active - ignore this stale message\n      console.warn(\n        \"Ignoring stale recording-ended message - recording still active\",\n      );\n      return;\n    }\n\n    if (!state.showPopup) {\n      setContentState((prev) => ({\n        ...prev,\n        showExtension: false,\n        recording: false,\n        paused: false,\n        pipEnded: false,\n        time: 0,\n        timer: 0,\n      }));\n    }\n  });\n\n  registerMessage(\"recording-error\", () => {\n    setStartFlowOutcome(\"error\");\n    setContentState((prev) => ({\n      ...prev,\n      pendingRecording: false,\n      preparingRecording: false,\n      pipEnded: false,\n    }));\n  });\n\n  registerMessage(\"start-stream\", () => {\n    const state = getState();\n    if (\n      state.preparingRecording ||\n      state.pendingRecording ||\n      state.recording ||\n      state.pipEnded\n    ) {\n      console.warn(\"[Screenity][Content] start-stream BLOCKED by guard state:\", {\n        preparingRecording: state.preparingRecording,\n        pendingRecording: state.pendingRecording,\n        recording: state.recording,\n        pipEnded: state.pipEnded,\n      });\n      return;\n    }\n\n    setContentState((prev) => ({\n      ...prev,\n      showExtension: true,\n      showPopup: true,\n    }));\n\n    if (state.recordingType !== \"camera\") {\n      state.startStreaming();\n    } else if (state.defaultVideoInput !== \"none\" && state.cameraActive) {\n      state.startStreaming();\n    }\n  });\n\n  registerMessage(\"commands\", (message) => {\n    if (!message) return;\n\n    const startRecordingCommand = message.commands.find(\n      (command) => command.name === \"start-recording\",\n    );\n    const cancelRecordingCommand = message.commands.find(\n      (command) => command.name === \"cancel-recording\",\n    );\n    const toggleDrawingModeCommand = message.commands.find(\n      (command) => command.name === \"toggle-drawing-mode\",\n    );\n    const toggleBlurModeCommand = message.commands.find(\n      (command) => command.name === \"toggle-blur-mode\",\n    );\n    const toggleCursorModeCommand = message.commands.find(\n      (command) => command.name === \"toggle-cursor-mode\",\n    );\n\n    setContentState((prev) => ({\n      ...prev,\n      recordingShortcut: startRecordingCommand.shortcut,\n      dismissRecordingShortcut: cancelRecordingCommand.shortcut,\n      toggleDrawingModeShortcut: toggleDrawingModeCommand?.shortcut || \"\",\n      toggleBlurModeShortcut: toggleBlurModeCommand?.shortcut || \"\",\n      toggleCursorModeShortcut: toggleCursorModeCommand?.shortcut || \"\",\n    }));\n  });\n\n  registerMessage(\"cancel-recording\", () => {\n    const state = getState();\n    state.dismissRecording();\n  });\n\n  registerMessage(\"pause-recording\", () => {\n    const state = getState();\n    if (state.paused) {\n      state.resumeRecording();\n    } else {\n      state.pauseRecording();\n    }\n  });\n\n  registerMessage(\"set-surface\", (message) => {\n    setContentState((prev) => ({\n      ...prev,\n      surface: message.surface,\n    }));\n  });\n\n  registerMessage(\"pip-ended\", () => {\n    const state = getState();\n    if (state.recording || state.pendingRecording) {\n      setContentState((prev) => ({\n        ...prev,\n        pipEnded: true,\n      }));\n    }\n  });\n\n  registerMessage(\"pip-started\", () => {\n    const state = getState();\n    if (state.recording || state.pendingRecording) {\n      setContentState((prev) => ({\n        ...prev,\n        pipEnded: false,\n      }));\n    }\n  });\n\n  registerMessage(\"setup-complete\", () => {\n    setContentState((prev) => ({\n      ...prev,\n      showOnboardingArrow: true,\n    }));\n  });\n\n  registerMessage(\"hide-popup-recording\", () => {\n    setContentState((prev) => ({\n      ...prev,\n      showPopup: false,\n      showExtension: false,\n    }));\n  });\n\n  registerMessage(\"stream-error\", (message) => {\n    const state = getState();\n    const errorCode = message?.errorCode || null;\n    const errorWhy = message?.why || message?.error || null;\n\n    state.openModal(\n      chrome.i18n.getMessage(\"streamErrorModalTitle\"),\n      chrome.i18n.getMessage(\"streamErrorModalDescription\"),\n      chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n      null,\n      () => {\n        state.dismissRecording();\n      },\n      () => {\n        state.dismissRecording();\n      },\n      null, // image\n      null, // learnMore\n      null, // learnMoreLink\n      false, // colorSafe\n      chrome.i18n.getMessage(\"getHelpButton\"),\n      () => {\n        chrome.runtime.sendMessage({\n          type: \"report-error\",\n          errorCode,\n          errorWhy,\n          source: \"stream-error\",\n        });\n      },\n    );\n  });\n\n  registerMessage(\"stream-ended-warning\", (message) => {\n    const state = getState();\n    // Show a toast warning but don't stop the recording\n    // The user can decide whether to continue or stop manually\n    if (state.openToast) {\n      state.openToast(\n        message.message ||\n          chrome.i18n.getMessage(\"streamEndedWarningToast\"),\n        () => {},\n        10000, // Show for 10 seconds\n      );\n    }\n  });\n\n  registerMessage(\"show-toast\", (message) => {\n    const state = getState();\n    if (typeof state.openToast !== \"function\") return;\n    state.openToast(message?.message || \"\", () => {}, message?.timeout || 5000);\n  });\n\n  registerMessage(\"backup-error\", () => {\n    const state = getState();\n    state.openModal(\n      chrome.i18n.getMessage(\"backupPermissionFailTitle\"),\n      chrome.i18n.getMessage(\"backupPermissionFailDescription\"),\n      chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n      null,\n      () => {\n        state.dismissRecording();\n      },\n      () => {\n        state.dismissRecording();\n      },\n    );\n  });\n\n  registerMessage(\"fast-recorder-hard-fail\", async () => {\n    const state = getState();\n    if (typeof state.openModal !== \"function\") return;\n\n    const downloadBundle = async () => {\n      const userAgent = navigator.userAgent;\n      let platformInfo = {};\n      try {\n        platformInfo = await chrome.runtime.sendMessage({\n          type: \"get-platform-info\",\n        });\n      } catch {}\n\n      const manifestInfo = chrome.runtime.getManifest().version;\n      const fastRecorderData = await chrome.storage.local.get([\n        \"fastRecorderBeta\",\n        \"fastRecorderDecision\",\n        \"fastRecorderDisabledForDevice\",\n        \"fastRecorderDisabledReason\",\n        \"fastRecorderDisabledDetails\",\n        \"fastRecorderDisabledAt\",\n        \"fastRecorderProbe\",\n        \"fastRecorderValidation\",\n        \"fastRecorderValidationFailed\",\n        \"fastRecorderInUse\",\n        \"fastRecorderActiveRecordingId\",\n      ]);\n\n      const data = {\n        userAgent: userAgent,\n        platformInfo: platformInfo,\n        manifestInfo: manifestInfo,\n        defaultAudioInput: state.defaultAudioInput,\n        defaultAudioOutput: state.defaultAudioOutput,\n        defaultVideoInput: state.defaultVideoInput,\n        quality: state.quality,\n        systemAudio: state.systemAudio,\n        audioInput: state.audioInput,\n        audioOutput: state.audioOutput,\n        backgroundEffectsActive: state.backgroundEffectsActive,\n        recording: state.recording,\n        recordingType: state.recordingType,\n        askForPermissions: state.askForPermissions,\n        cameraPermission: state.cameraPermission,\n        microphonePermission: state.microphonePermission,\n        askMicrophone: state.askMicrophone,\n        cursorMode: state.cursorMode,\n        zoomEnabled: state.zoomEnabled,\n        offscreenRecording: state.offscreenRecording,\n        updateChrome: state.updateChrome,\n        permissionsChecked: state.permissionsChecked,\n        permissionsLoaded: state.permissionsLoaded,\n        hideUI: state.hideUI,\n        alarm: state.alarm,\n        alarmTime: state.alarmTime,\n        surface: state.surface,\n        blurMode: state.blurMode,\n        fastRecorder: fastRecorderData,\n      };\n\n      const zip = new JSZip();\n      zip.file(\"troubleshooting.json\", JSON.stringify(data));\n      const blob = await zip.generateAsync({ type: \"blob\" });\n      const url = window.URL.createObjectURL(blob);\n\n      const a = document.createElement(\"a\");\n      a.href = url;\n      a.download = \"screenity-troubleshooting.zip\";\n      a.click();\n      window.URL.revokeObjectURL(url);\n\n      chrome.runtime.sendMessage({ type: \"indexed-db-download\" });\n    };\n\n    state.openModal(\n      chrome.i18n.getMessage(\"fastRecorderFailedTitle\"),\n      chrome.i18n.getMessage(\"fastRecorderFailedDescription\"),\n      chrome.i18n.getMessage(\"downloadAnywayButton\"),\n      chrome.i18n.getMessage(\"cancelButton\"),\n      () => {\n        chrome.runtime.sendMessage({ type: \"open-download-mp4\" });\n      },\n      () => {},\n      null,\n      null,\n      null,\n      true,\n      false,\n      () => {},\n    );\n  });\n\n  registerMessage(\"recording-check\", (message, sender) => {\n    const state = getState();\n\n    if (!message.force) {\n      if (!state.showExtension && !state.recording) {\n        updateFromStorage(true, sender.id);\n      }\n    } else {\n      // After navigation, PiP is always destroyed (the iframe that owned it\n      // was torn down with the old page).  Set pipEnded: true so the inline\n      // camera overlay is visible immediately.  If the camera iframe\n      // successfully re-enters PiP later, a \"pip-started\" message will flip\n      // this back to false.\n      setContentState((prev) => ({\n        ...prev,\n        showExtension: true,\n        recording: true,\n        pipEnded: true,\n      }));\n      updateFromStorage(false, sender.id);\n    }\n  });\n\n  registerMessage(\"stop-pending\", () => {\n    setStartFlowOutcome(\"error\");\n    setContentState((prev) => ({\n      ...prev,\n      pendingRecording: false,\n      preparingRecording: false,\n      pipEnded: false,\n    }));\n  });\n\n  registerMessage(\"reopen-popup-multi\", (message) => {\n    setContentState((prev) => ({\n      ...prev,\n      showExtension: true,\n      showPopup: true,\n    }));\n    updateFromStorage(false, message.senderId);\n\n    setTimeout(() => {\n      const state = getState();\n      if (state.openToast) {\n        state.openToast(chrome.i18n.getMessage(\"addedToMultiToast\"), () => {});\n      }\n    }, 1000);\n  });\n\n  registerMessage(\"open-popup-project\", (message) => {\n    setContentState((prev) => ({\n      ...prev,\n      showExtension: true,\n      showPopup: true,\n      recordingProjectTitle: message.projectTitle,\n      projectId: message.projectId,\n      recordingToScene: message.recordingToScene,\n      activeSceneId: message.activeSceneId,\n    }));\n\n    updateFromStorage(false, message.senderId);\n\n    setTimeout(() => {\n      const state = getState();\n      if (state.openToast) {\n        state.openToast(\n          chrome.i18n.getMessage(\"readyRecordSceneToast\"),\n          () => {},\n        );\n      }\n    }, 1000);\n  });\n\n  registerMessage(\"time-warning\", () => {\n    // Only trigger when actively recording\n    const state = getState();\n\n    if (state.recording && !state.paused) {\n      setContentState((prev) => ({\n        ...prev,\n        timeWarning: true,\n      }));\n\n      if (state.openToast) {\n        state.openToast(\n          chrome.i18n.getMessage(\"reachingRecordingLimitToast\"),\n          () => {},\n          5000,\n        );\n      }\n    }\n  });\n  registerMessage(\"time-stopped\", () => {\n    const state = getState();\n    // Only trigger when actively recording\n    if (state.recording && !state.paused) {\n      setContentState((prev) => ({\n        ...prev,\n        timeWarning: false,\n      }));\n\n      if (state.openToast) {\n        state.openToast(\n          chrome.i18n.getMessage(\"recordingLimitReachedToast\"),\n          () => {},\n          5000,\n        );\n      }\n    }\n  });\n\n  registerMessage(\"get-project-info\", (message) => {\n    const payload = {\n      source: \"get-project-info\",\n      requestedAt: Date.now(),\n    };\n    if (activeLocalPlaybackSource?.url && latestLocalPlaybackOffer?.offerId) {\n      payload.localPlayback = {\n        available: true,\n        ready: true,\n        trackType: \"screen\",\n        offerId: latestLocalPlaybackOffer.offerId,\n        url: activeLocalPlaybackSource.url,\n        mimeType: activeLocalPlaybackSource.mimeType || \"video/webm\",\n        localBytes: activeLocalPlaybackSource.size || null,\n        chunkCount: latestLocalPlaybackOffer.chunkCount || 0,\n        estimatedBytes: latestLocalPlaybackOffer.estimatedBytes || 0,\n        expiresAt: latestLocalPlaybackOffer.expiresAt || null,\n      };\n    }\n    postProjectHandoff(payload);\n  });\n  registerMessage(\"check-auth\", async (message) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // Default to local user\n      const { recording } = await chrome.storage.local.get(\"recording\");\n\n      setContentState((prev) => ({\n        ...prev,\n        isLoggedIn: false,\n        screenityUser: null,\n        isSubscribed: false,\n        proSubscription: null,\n        showExtension: true,\n        showPopup: !recording,\n      }));\n\n      return;\n    }\n\n    const result = await checkAuthStatus();\n\n    const { recording } = await chrome.storage.local.get(\"recording\");\n\n    setContentState((prev) => ({\n      ...prev,\n      isLoggedIn: result.authenticated,\n      screenityUser: result.user,\n      isSubscribed: result.subscribed,\n      proSubscription: result.proSubscription,\n      ...(result.authenticated ? { wasLoggedIn: false } : {}),\n      showExtension: true,\n      showPopup: !recording,\n    }));\n\n    if (result.authenticated) {\n      // Offscreen recording and client-side zoom are not available\n      setContentState((prev) => ({\n        ...prev,\n        offscreenRecording: false,\n        onboarding: false,\n        showProSplash: false,\n        zoomEnabled: false,\n      }));\n\n      chrome.storage.local.set({\n        offscreenRecording: false,\n        zoomEnabled: false,\n        wasLoggedIn: false,\n      });\n    }\n  });\n  registerMessage(\"update-project-loading\", (message, sender) => {\n    window.postMessage(\n      { source: \"update-project-loading\", multiMode: message.multiMode },\n      \"*\",\n    );\n\n    if (!message.multiMode) {\n      setContentState((prev) => ({\n        ...prev,\n        showExtension: false,\n        showPopup: false,\n      }));\n    }\n\n    updateFromStorage(true, sender.id);\n  });\n  registerMessage(\"update-project-ready\", (message, sender) => {\n    const projectId = message?.projectId || null;\n    if (!projectId) {\n      console.warn(\n        \"[Screenity][Content] Ignoring update-project-ready without projectId\",\n      );\n      return;\n    }\n\n    projectReadySeq += 1;\n    const handoffAt = Date.now();\n    const handoffId = `${projectId}:${handoffAt}:${projectReadySeq}`;\n    const localPlayback = message?.localPlayback || null;\n    latestLocalPlaybackOffer =\n      localPlayback?.available && localPlayback?.trackType === \"screen\"\n        ? localPlayback\n        : null;\n    latestLocalPlaybackProjectId = latestLocalPlaybackOffer ? projectId : null;\n    latestLocalPlaybackSceneId = latestLocalPlaybackOffer\n      ? message.sceneId || null\n      : null;\n\n    const posted = postProjectHandoff({\n      source: \"update-project-ready\",\n      share: message.share,\n      newProject: message.newProject,\n      sceneId: message.sceneId,\n      projectId,\n      localPlayback:\n        localPlayback?.available && localPlayback?.trackType === \"screen\"\n          ? {\n              ...localPlayback,\n              ready:\n                activeLocalPlaybackSource?.offerId === localPlayback.offerId &&\n                Boolean(activeLocalPlaybackSource?.url),\n              url:\n                activeLocalPlaybackSource?.offerId === localPlayback.offerId\n                  ? activeLocalPlaybackSource.url\n                  : null,\n              mimeType:\n                activeLocalPlaybackSource?.offerId === localPlayback.offerId\n                  ? activeLocalPlaybackSource.mimeType || \"video/webm\"\n                  : null,\n              localBytes:\n                activeLocalPlaybackSource?.offerId === localPlayback.offerId\n                  ? activeLocalPlaybackSource.size || null\n                  : null,\n            }\n          : {\n              available: false,\n              trackType: \"screen\",\n            },\n      handoffAt,\n      handoffId,\n      handoffSeq: projectReadySeq,\n      forceRefresh: true,\n    });\n\n    if (posted) {\n      window.__screenityLastProjectReady = {\n        projectId,\n        sceneId: message.sceneId || null,\n        handoffAt,\n        handoffId,\n        localPlaybackOfferId: localPlayback?.offerId || null,\n      };\n      updateFromStorage(false, sender?.id);\n    }\n\n    const capturedOffer = latestLocalPlaybackOffer;\n    if (posted && capturedOffer?.offerId) {\n      const capturedSceneId = message.sceneId || null;\n      console.info(\"[Screenity][Content] Local screen playback offered\", {\n        projectId,\n        sceneId: capturedSceneId,\n        offerId: capturedOffer.offerId,\n        chunkCount: capturedOffer.chunkCount || 0,\n        estimatedBytes: capturedOffer.estimatedBytes || 0,\n      });\n      void ensureLocalPlaybackReady({\n        projectId,\n        sceneId: capturedSceneId,\n        offer: capturedOffer,\n      })\n        .then((readySource) => {\n          console.info(\"[Screenity][Content] Local screen playback ready\", {\n            projectId,\n            sceneId: capturedSceneId,\n            offerId: capturedOffer.offerId,\n            bytes: readySource?.size || 0,\n          });\n          postLocalPlaybackHandoff({\n            projectId,\n            sceneId: capturedSceneId,\n            offer: capturedOffer,\n            readySource,\n          });\n        })\n        .catch((err) => {\n          const reason = err?.message || \"local-playback-build-failed\";\n          console.warn(\"[Screenity][Content] Local screen playback fallback\", {\n            projectId,\n            sceneId: capturedSceneId,\n            offerId: capturedOffer.offerId,\n            reason,\n          });\n          void markLocalPlaybackFallback({\n            offerId: capturedOffer.offerId,\n            projectId,\n            sceneId: capturedSceneId,\n            reason,\n          });\n          postLocalPlaybackHandoff({\n            projectId,\n            sceneId: capturedSceneId,\n            offer: capturedOffer,\n            fallbackReason: reason,\n          });\n        });\n    }\n  });\n  registerMessage(\"clear-project-recording\", (message) => {\n    updateFromStorage(false, message.senderId);\n  });\n  registerMessage(\"preparing-recording\", () => {\n    traceStep(\"preparingReceived\");\n    setContentState((prev) => ({\n      ...prev,\n      preparingRecording: true,\n      showExtension: true,\n      showPopup: false,\n    }));\n  });\n};\n"
  },
  {
    "path": "src/pages/Content/context/utils/checkAuthStatus.js",
    "content": "export const checkAuthStatus = async () => {\n  return new Promise((resolve) => {\n    chrome.runtime.sendMessage({ type: \"check-auth-status\" }, (response) => {\n      if (chrome.runtime.lastError) {\n        console.error(\n          \"❌ Error checking auth status:\",\n          chrome.runtime.lastError.message\n        );\n        resolve({ authenticated: false });\n      } else {\n        resolve({\n          authenticated: !!response?.authenticated,\n          user: response?.user ?? null,\n          subscribed: !!response?.subscribed,\n          proSubscription: response?.proSubscription ?? null,\n          cached: response?.cached ?? false,\n        });\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "src/pages/Content/context/utils/checkRecording.js",
    "content": "import { setContentState } from \"../ContentState\";\n\nexport const checkRecording = async (id) => {\n  const { recording } = await chrome.storage.local.get(\"recording\");\n  const { tabRecordedID } = await chrome.storage.local.get(\"tabRecordedID\");\n  if (id == null && tabRecordedID) {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      recording: false,\n    }));\n  } else if (recording && tabRecordedID) {\n    if (id != tabRecordedID) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        recording: false,\n      }));\n    }\n  }\n};\n"
  },
  {
    "path": "src/pages/Content/context/utils/updateFromStorage.js",
    "content": "import { setContentState } from \"../ContentState\";\nimport { checkRecording } from \"./checkRecording\";\n\nconst CURSOR_EFFECTS = [\"target\", \"highlight\", \"spotlight\"];\n\nconst normalizeCursorEffects = (effects) => {\n  if (!Array.isArray(effects)) return [];\n  return effects.filter((effect) => CURSOR_EFFECTS.includes(effect));\n};\n\nconst deriveCursorMode = (effects, fallbackMode) => {\n  if (effects.length === 0) return \"none\";\n  if (effects.length === 1) return effects[0];\n  if (fallbackMode && effects.includes(fallbackMode)) return fallbackMode;\n  return effects[0] || \"none\";\n};\n\nexport const updateFromStorage = (check = true, id = null) => {\n  chrome.storage.local.get(\n    [\n      \"audioInput\",\n      \"videoInput\",\n      \"defaultAudioInput\",\n      \"defaultVideoInput\",\n      \"defaultAudioInputLabel\",\n      \"defaultVideoInputLabel\",\n      \"cameraDimensions\",\n      \"cameraFlipped\",\n      \"cameraActive\",\n      \"micActive\",\n      \"recording\",\n      \"paused\",\n      \"backgroundEffect\",\n      \"backgroundEffectsActive\",\n      \"toolbarPosition\",\n      \"countdown\",\n      \"recordingType\",\n      \"customRegion\",\n      \"regionWidth\",\n      \"regionHeight\",\n      \"regionX\",\n      \"regionY\",\n      \"hideToolbar\",\n      \"alarm\",\n      \"alarmTime\",\n      \"pendingRecording\",\n      \"askForPermissions\",\n      \"cursorMode\",\n      \"cursorEffects\",\n      \"pushToTalk\",\n      \"askMicrophone\",\n      \"offscreenRecording\",\n      \"zoomEnabled\",\n      \"setDevices\",\n      \"popupPosition\",\n      \"surface\",\n      \"hideUIAlerts\",\n      \"hideUI\",\n      \"bigTab\",\n      \"toolbarHover\",\n      \"askDismiss\",\n      \"swatch\",\n      \"color\",\n      \"strokeWidth\",\n      \"quality\",\n      \"systemAudio\",\n      \"backup\",\n      \"backupSetup\",\n      \"qualityValue\",\n      \"fpsValue\",\n      \"fastRecorderBeta\",\n      \"fastRecorderStatus\",\n      \"useWebCodecsRecorder\",\n      \"multiMode\",\n      \"multiSceneCount\",\n      \"sortBy\",\n      \"wasLoggedIn\",\n      \"instantMode\",\n      \"hasSeenInstantModeModal\",\n      \"hasSubscribedBefore\",\n    ],\n    (result) => {\n      const storedEffects = normalizeCursorEffects(result.cursorEffects);\n      const hasStoredEffects = Array.isArray(result.cursorEffects);\n      const legacyMode =\n        result.cursorMode !== undefined && result.cursorMode !== null\n          ? result.cursorMode\n          : \"none\";\n      const cursorEffects = hasStoredEffects\n        ? storedEffects\n        : legacyMode !== \"none\"\n        ? [legacyMode]\n        : [];\n      const cursorMode = deriveCursorMode(cursorEffects, legacyMode);\n\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        audioInput:\n          result.audioInput !== undefined && result.audioInput !== null\n            ? result.audioInput\n            : prevContentState.audioInput,\n        videoInput:\n          result.videoInput !== undefined && result.videoInput !== null\n            ? result.videoInput\n            : prevContentState.videoInput,\n        defaultAudioInput:\n          result.defaultAudioInput !== undefined &&\n          result.defaultAudioInput !== null\n            ? result.defaultAudioInput\n            : prevContentState.defaultAudioInput,\n        defaultVideoInput:\n          result.defaultVideoInput !== undefined &&\n          result.defaultVideoInput !== null\n            ? result.defaultVideoInput\n            : prevContentState.defaultVideoInput,\n        defaultAudioInputLabel:\n          result.defaultAudioInputLabel !== undefined &&\n          result.defaultAudioInputLabel !== null\n            ? result.defaultAudioInputLabel\n            : prevContentState.defaultAudioInputLabel,\n        defaultVideoInputLabel:\n          result.defaultVideoInputLabel !== undefined &&\n          result.defaultVideoInputLabel !== null\n            ? result.defaultVideoInputLabel\n            : prevContentState.defaultVideoInputLabel,\n        cameraDimensions:\n          result.cameraDimensions !== undefined &&\n          result.cameraDimensions !== null\n            ? result.cameraDimensions\n            : prevContentState.cameraDimensions,\n        cameraFlipped:\n          result.cameraFlipped !== undefined && result.cameraFlipped !== null\n            ? result.cameraFlipped\n            : prevContentState.cameraFlipped,\n        cameraActive:\n          result.cameraActive !== undefined && result.cameraActive !== null\n            ? result.cameraActive\n            : prevContentState.cameraActive,\n        micActive:\n          result.micActive !== undefined && result.micActive !== null\n            ? result.micActive\n            : prevContentState.micActive,\n        backgroundEffect:\n          result.backgroundEffect !== undefined &&\n          result.backgroundEffect !== null\n            ? result.backgroundEffect\n            : prevContentState.backgroundEffect,\n        backgroundEffectsActive:\n          result.backgroundEffectsActive !== undefined &&\n          result.backgroundEffectsActive !== null\n            ? result.backgroundEffectsActive\n            : prevContentState.backgroundEffectsActive,\n        toolbarPosition:\n          result.toolbarPosition !== undefined &&\n          result.toolbarPosition !== null\n            ? result.toolbarPosition\n            : prevContentState.toolbarPosition,\n        countdown:\n          result.countdown !== undefined && result.countdown !== null\n            ? result.countdown\n            : prevContentState.countdown,\n        recording:\n          result.recording !== undefined && result.recording !== null\n            ? result.recording\n            : prevContentState.recording,\n        paused:\n          result.paused !== undefined && result.paused !== null\n            ? result.paused\n            : prevContentState.paused,\n        recordingType:\n          result.recordingType !== undefined && result.recordingType !== null\n            ? result.recordingType\n            : prevContentState.recordingType,\n        customRegion:\n          result.customRegion !== undefined && result.customRegion !== null\n            ? result.customRegion\n            : prevContentState.customRegion,\n        regionWidth:\n          result.regionWidth !== undefined && result.regionWidth !== null\n            ? result.regionWidth\n            : prevContentState.regionWidth,\n        regionHeight:\n          result.regionHeight !== undefined && result.regionHeight !== null\n            ? result.regionHeight\n            : prevContentState.regionHeight,\n        regionX:\n          result.regionX !== undefined && result.regionX !== null\n            ? result.regionX\n            : prevContentState.regionX,\n        regionY:\n          result.regionY !== undefined && result.regionY !== null\n            ? result.regionY\n            : prevContentState.regionY,\n        hideToolbar:\n          result.hideToolbar !== undefined && result.hideToolbar !== null\n            ? result.hideToolbar\n            : prevContentState.hideToolbar,\n        alarm:\n          result.alarm !== undefined && result.alarm !== null\n            ? result.alarm\n            : prevContentState.alarm,\n        alarmTime:\n          result.alarmTime !== undefined && result.alarmTime !== null\n            ? result.alarmTime\n            : prevContentState.alarmTime,\n        pendingRecording:\n          result.pendingRecording !== undefined &&\n          result.pendingRecording !== null\n            ? result.pendingRecording\n            : prevContentState.pendingRecording,\n        askForPermissions:\n          result.askForPermissions !== undefined &&\n          result.askForPermissions !== null\n            ? result.askForPermissions\n            : prevContentState.askForPermissions,\n        cursorMode: cursorMode || prevContentState.cursorMode,\n        cursorEffects:\n          cursorEffects.length > 0 || hasStoredEffects\n            ? cursorEffects\n            : prevContentState.cursorEffects,\n        pushToTalk:\n          result.pushToTalk !== undefined && result.pushToTalk !== null\n            ? result.pushToTalk\n            : prevContentState.pushToTalk,\n        zoomEnabled:\n          result.zoomEnabled !== undefined && result.zoomEnabled !== null\n            ? result.zoomEnabled\n            : prevContentState.zoomEnabled,\n        askMicrophone:\n          result.askMicrophone !== undefined && result.askMicrophone !== null\n            ? result.askMicrophone\n            : prevContentState.askMicrophone,\n        offscreenRecording:\n          result.offscreenRecording !== undefined &&\n          result.offscreenRecording !== null\n            ? result.offscreenRecording\n            : prevContentState.offscreenRecording,\n        setDevices:\n          result.setDevices !== undefined && result.setDevices !== null\n            ? result.setDevices\n            : prevContentState.setDevices,\n        popupPosition:\n          result.popupPosition !== undefined && result.popupPosition !== null\n            ? result.popupPosition\n            : prevContentState.popupPosition,\n        surface:\n          result.surface !== undefined && result.surface !== null\n            ? result.surface\n            : prevContentState.surface,\n        hideUIAlerts:\n          result.hideUIAlerts !== undefined && result.hideUIAlerts !== null\n            ? result.hideUIAlerts\n            : prevContentState.hideUIAlerts,\n        hideUI:\n          result.hideUI !== undefined && result.hideUI !== null\n            ? result.hideUI\n            : prevContentState.hideUI,\n        bigTab:\n          result.bigTab !== undefined && result.bigTab !== null\n            ? result.bigTab\n            : prevContentState.bigTab,\n        toolbarHover:\n          result.toolbarHover !== undefined && result.toolbarHover !== null\n            ? result.toolbarHover\n            : prevContentState.toolbarHover,\n        askDismiss:\n          result.askDismiss !== undefined && result.askDismiss !== null\n            ? result.askDismiss\n            : prevContentState.askDismiss,\n        swatch:\n          result.swatch !== undefined && result.swatch !== null\n            ? result.swatch\n            : prevContentState.swatch,\n        color:\n          result.color !== undefined && result.color !== null\n            ? result.color\n            : prevContentState.color,\n        strokeWidth:\n          result.strokeWidth !== undefined && result.strokeWidth !== null\n            ? result.strokeWidth\n            : prevContentState.strokeWidth,\n        quality:\n          result.quality !== undefined && result.quality !== null\n            ? result.quality\n            : prevContentState.quality,\n        systemAudio:\n          result.systemAudio !== undefined && result.systemAudio !== null\n            ? result.systemAudio\n            : prevContentState.systemAudio,\n        backup:\n          result.backup !== undefined && result.backup !== null\n            ? result.backup\n            : prevContentState.backup,\n        backupSetup:\n          result.backupSetup !== undefined && result.backupSetup !== null\n            ? result.backupSetup\n            : prevContentState.backupSetup,\n        qualityValue:\n          result.qualityValue !== undefined && result.qualityValue !== null\n            ? result.qualityValue\n            : prevContentState.qualityValue,\n        fpsValue:\n          result.fpsValue !== undefined && result.fpsValue !== null\n            ? result.fpsValue\n            : prevContentState.fpsValue,\n        fastRecorderBeta:\n          result.fastRecorderBeta !== undefined &&\n          result.fastRecorderBeta !== null\n            ? result.fastRecorderBeta\n            : prevContentState.fastRecorderBeta,\n        fastRecorderStatus:\n          result.fastRecorderStatus !== undefined &&\n          result.fastRecorderStatus !== null\n            ? result.fastRecorderStatus\n            : prevContentState.fastRecorderStatus,\n        useWebCodecsRecorder:\n          result.useWebCodecsRecorder !== undefined &&\n          result.useWebCodecsRecorder !== null\n            ? result.useWebCodecsRecorder\n            : prevContentState.useWebCodecsRecorder,\n        multiMode: result.multiMode || false,\n        multiSceneCount: result.multiSceneCount || 0,\n        wasLoggedIn: result.wasLoggedIn || false,\n        sortBy: result.sortBy || \"newest\",\n        instantMode: result.instantMode || false,\n        hasSeenInstantModeModal: result.hasSeenInstantModeModal || false,\n        onboarding: result.onboarding || false,\n        hasSubscribedBefore: result.hasSubscribedBefore || false,\n        showProSplash: result.showProSplash || false,\n      }));\n\n      if (result.systemAudio === undefined || result.systemAudio === null) {\n        chrome.storage.local.set({ systemAudio: true });\n      }\n\n      if (\n        result.backgroundEffect === undefined ||\n        result.backgroundEffect === null\n      ) {\n        chrome.storage.local.set({ backgroundEffect: \"blur\" });\n      }\n\n      if (result.backup === undefined || result.backup === null) {\n        chrome.storage.local.set({ backup: false });\n      }\n\n      if (result.countdown === undefined || result.countdown === null) {\n        chrome.storage.local.set({ countdown: true });\n      }\n\n      if (result.backupSetup === undefined || result.backupSetup === null) {\n        chrome.storage.local.set({ backupSetup: false });\n      }\n\n      if (!hasStoredEffects && legacyMode) {\n        chrome.storage.local.set({\n          cursorEffects: cursorEffects,\n          cursorMode: cursorMode,\n        });\n      }\n\n      if (result.backgroundEffectsActive) {\n        chrome.runtime.sendMessage({ type: \"backgroundEffectsActive\" });\n      }\n\n      if (check) {\n        checkRecording(id);\n      }\n\n      if (result.alarm) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          time: parseFloat(result.alarmTime),\n          timer: parseFloat(result.alarmTime),\n        }));\n      } else if (!result.recording) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          time: 0,\n          timer: 0,\n        }));\n      }\n    }\n  );\n};\n"
  },
  {
    "path": "src/pages/Content/countdown/Countdown.jsx",
    "content": "import React, { useState, useEffect, useContext, useRef } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst COUNTDOWN_TIME = 3;\nconst DEBUG_START_FLOW =\n  typeof window !== \"undefined\" ? !!window.SCREENITY_DEBUG_RECORDER : false;\n\nconst Countdown = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [count, setCount] = useState(COUNTDOWN_TIME);\n  const [isTransforming, setIsTransforming] = useState(false);\n  const [isRotating, setIsRotating] = useState(false);\n\n  const END_HOLD_MS = 1000;\n  const POST_HIDE_START_DELAY_MS = 150;\n  const HIDE_AFTER_END_MS = 0;\n\n  const intervalRef = useRef(null);\n  const activeRef = useRef(false);\n  const cancelledRef = useRef(false);\n  const wasVisibleRef = useRef(false);\n  const finishRef = useRef(null);\n  const resetRef = useRef(null);\n  const completedRef = useRef(false);\n  const hideTimeoutRef = useRef(null);\n  const finishTimeoutRef = useRef(null);\n  const startTimeoutRef = useRef(null);\n  const startAtRef = useRef(0);\n  const runIdRef = useRef(0);\n\n  const cleanupTimers = () => {\n    if (intervalRef.current) {\n      clearInterval(intervalRef.current);\n      intervalRef.current = null;\n    }\n    if (hideTimeoutRef.current) {\n      clearTimeout(hideTimeoutRef.current);\n      hideTimeoutRef.current = null;\n    }\n    if (finishTimeoutRef.current) {\n      clearTimeout(finishTimeoutRef.current);\n      finishTimeoutRef.current = null;\n    }\n    if (startTimeoutRef.current) {\n      clearTimeout(startTimeoutRef.current);\n      startTimeoutRef.current = null;\n    }\n  };\n\n  const logStartFlow = (event, data = {}) => {\n    if (!DEBUG_START_FLOW) return;\n    const payload = { ts: Date.now(), event, ...data };\n    console.info(\"[Screenity][StartFlow]\", payload);\n    try {\n      const update = {\n        startFlowDebug: {\n          ...(data || {}),\n          event,\n          ts: payload.ts,\n        },\n      };\n      if (event === \"countdown_start\") {\n        update.countdownVisibleAt = payload.ts;\n      } else if (event === \"countdown_end\") {\n        update.countdownHiddenAt = payload.ts;\n      }\n      chrome.storage.local.set(update);\n    } catch {}\n  };\n\n  useEffect(() => {\n    activeRef.current = contentState.countdownActive;\n    cancelledRef.current = contentState.countdownCancelled;\n    finishRef.current = contentState.onCountdownFinished;\n    resetRef.current = contentState.resetCountdown;\n  }, [\n    contentState.countdownActive,\n    contentState.countdownCancelled,\n    contentState.onCountdownFinished,\n    contentState.resetCountdown,\n  ]);\n\n  // Handle cancellation\n  const handleCancel = () => {\n    if (contentState.countdownActive || contentState.isCountdownVisible) {\n      cleanupTimers();\n      setCount(COUNTDOWN_TIME);\n      setIsTransforming(false);\n      setIsRotating(false);\n      completedRef.current = false;\n      logStartFlow(\"countdown_cancel\", { visible: false });\n      contentState.cancelCountdown();\n    }\n  };\n\n  // Countdown logic\n  useEffect(() => {\n    if (!contentState.countdownActive) {\n      cleanupTimers();\n      completedRef.current = false;\n      return;\n    }\n\n    cleanupTimers();\n    // Reset immediately at run start so a stale previous `1` cannot\n    // short-circuit the next countdown cycle.\n    setCount(COUNTDOWN_TIME);\n    completedRef.current = false;\n    runIdRef.current += 1;\n    const runId = runIdRef.current;\n    startAtRef.current = performance.now();\n    logStartFlow(\"countdown_start\", { visible: true });\n\n    const tick = () => {\n      if (runIdRef.current !== runId) return;\n      if (!activeRef.current || cancelledRef.current) return;\n      const elapsedMs = performance.now() - startAtRef.current;\n      const remainingMs = Math.max(0, COUNTDOWN_TIME * 1000 - elapsedMs);\n      const nextCount = Math.max(1, Math.ceil(remainingMs / 1000));\n      setCount(nextCount);\n\n      if (remainingMs <= 0) {\n        return;\n      }\n\n      intervalRef.current = setTimeout(tick, 100);\n    };\n\n    intervalRef.current = setTimeout(tick, 100);\n\n    return cleanupTimers;\n  }, [contentState.countdownActive]);\n\n  useEffect(() => {\n    if (!activeRef.current || cancelledRef.current) {\n      return;\n    }\n    if (count !== 1) {\n      return;\n    }\n    const elapsedMs = performance.now() - startAtRef.current;\n    // Ignore stale count=1 carried from a previous run; valid final-second\n    // entry happens after ~2s for a 3..2..1 countdown.\n    if (elapsedMs < (COUNTDOWN_TIME - 1) * 1000) {\n      return;\n    }\n    if (completedRef.current) {\n      return;\n    }\n    completedRef.current = true;\n\n    if (intervalRef.current) {\n      clearInterval(intervalRef.current);\n      intervalRef.current = null;\n    }\n\n    if (!cancelledRef.current && activeRef.current) {\n      finishTimeoutRef.current = setTimeout(() => {\n        if (cancelledRef.current || !activeRef.current) {\n          finishTimeoutRef.current = null;\n          return;\n        }\n        setContentState((prev) => ({\n          ...prev,\n          isCountdownVisible: false,\n          countdownActive: false,\n        }));\n        const endedAt = Date.now();\n        const dispatchStartAfterHide = () => {\n          if (cancelledRef.current) return;\n          const startDispatchedAt = Date.now();\n          logStartFlow(\"countdown_end\", {\n            visible: false,\n            endedAt,\n            startDispatchedAt,\n          });\n          chrome.storage.local.set({\n            countdownFinishedAt: endedAt,\n            lastCountdownStartGate: {\n              endedAt,\n              countdownHiddenAt: endedAt,\n              startDispatchedAt,\n              overlayVisible: false,\n              ts: startDispatchedAt,\n            },\n          });\n          finishRef.current?.();\n          // Countdown-enabled flow should start from background only.\n          // Waiting briefly after hide avoids capturing the countdown frame.\n          startTimeoutRef.current = setTimeout(() => {\n            if (cancelledRef.current) {\n              startTimeoutRef.current = null;\n              return;\n            }\n            chrome.runtime\n              .sendMessage({\n                type: \"countdown-finished\",\n                endedAt,\n              })\n              .catch(() => {});\n            startTimeoutRef.current = null;\n          }, POST_HIDE_START_DELAY_MS);\n        };\n        // Wait for at least one paint after hidden state commit.\n        requestAnimationFrame(() => {\n          requestAnimationFrame(dispatchStartAfterHide);\n        });\n        hideTimeoutRef.current = setTimeout(() => {\n          hideTimeoutRef.current = null;\n        }, HIDE_AFTER_END_MS);\n        finishTimeoutRef.current = null;\n      }, END_HOLD_MS);\n    }\n  }, [count, setContentState]);\n\n  // Start animation when countdown becomes visible\n  useEffect(() => {\n    if (contentState.isCountdownVisible && !wasVisibleRef.current) {\n      resetRef.current?.();\n      completedRef.current = false;\n      setCount(COUNTDOWN_TIME);\n      setIsTransforming(true);\n\n      const rotateId = setTimeout(() => {\n        if (!cancelledRef.current) {\n          setIsRotating(true);\n        }\n      }, 10);\n\n      const transformId = setTimeout(() => {\n        if (!cancelledRef.current) {\n          setIsTransforming(false);\n        }\n      }, (COUNTDOWN_TIME * 1000) / 2);\n\n      return () => {\n        clearTimeout(rotateId);\n        clearTimeout(transformId);\n      };\n    }\n\n    if (!contentState.isCountdownVisible) {\n      setIsRotating(false);\n      setIsTransforming(false);\n    }\n  }, [contentState.isCountdownVisible]);\n\n  useEffect(() => {\n    wasVisibleRef.current = contentState.isCountdownVisible;\n  }, [contentState.isCountdownVisible]);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    return cleanupTimers;\n  }, []);\n\n  return (\n    <div\n      className={`countdown ${\n        contentState.countdownActive ? \"recording-countdown\" : \"\"\n      }`}\n      onClick={handleCancel}\n    >\n      {contentState.isCountdownVisible && (\n        <div>\n          <svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n            <defs>\n              <filter id=\"goo\">\n                <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"10\" />\n                <feColorMatrix\n                  in=\"blur\"\n                  mode=\"matrix\"\n                  values=\"1 0 0 0 0\n                          0 1 0 0 0\n                          0 0 1 0 0\n                          0 0 0 20 -10\"\n                  result=\"goo\"\n                />\n              </filter>\n            </defs>\n          </svg>\n\n          <div className=\"countdown-circle\">\n            <div className=\"countdown-number\">{count}</div>\n            <div\n              className=\"background\"\n              style={{\n                transform: isRotating ? \"rotate(90deg)\" : \"rotate(0deg)\",\n              }}\n            >\n              <div\n                className=\"circle\"\n                style={{\n                  transform: isTransforming ? \"scale(1)\" : \"scale(0.8)\",\n                }}\n              ></div>\n              <div className=\"c c2\"></div>\n              <div className=\"c c3\"></div>\n            </div>\n          </div>\n\n          <div className=\"countdown-info\">\n            {chrome.i18n.getMessage(\"countdownMessage\")}\n          </div>\n          <div className=\"countdown-overlay\"></div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default Countdown;\n"
  },
  {
    "path": "src/pages/Content/countdown/styles/_Countdown.scss",
    "content": "@use \"../../styles/_variables\" as *;\n\n.countdown {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: $z-index-max;\n}\n\n.countdown-circle {\n  width: 200px;\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 999;\n  text-align: center;\n}\n.countdown-overlay {\n  width: 100%;\n  height: 100%;\n  background: rgba(0, 0, 0, 0.5);\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 99;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.countdown-number {\n  position: absolute;\n  width: 20px;\n  height: 60px;\n  z-index: 9999999;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  font-weight: 300 !important;\n  font-family: $font-light !important;\n  font-size: 48px !important;\n  line-height: 60px !important;\n  letter-spacing: normal !important;\n  text-transform: none !important;\n  word-spacing: normal !important;\n  color: $color-text-contrast;\n  text-align: center;\n  display: block;\n  transition: all 0.6s ease-in-out;\n}\n.background {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  filter: url(\"#goo\");\n  transform: rotate(0deg);\n  transition: all 3s ease-in-out;\n}\n.circle {\n  z-index: 9;\n  position: absolute;\n  transform: scale(0.8);\n  top: 0px;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: 50%;\n  background: radial-gradient(\n    118.3% 119.01% at 35.44% 0%,\n    #2baef8 23.13%,\n    #3582f6 46.35%,\n    #486def 74.48%,\n    #7b9aea 100%\n  );\n  width: 200px;\n  height: 200px;\n  transition: all 1.5s ease-in-out;\n}\n.c {\n  width: 50px;\n  height: 50px;\n  z-index: 999;\n  border-radius: 50%;\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  opacity: 1;\n}\n.c2 {\n  background: radial-gradient(\n    118.3% 119.01% at 35.44% 0%,\n    #2b96f8 23.13%,\n    #356bf6 64.58%\n  );\n  transform: translate(20px, 20px);\n}\n.c3 {\n  background: radial-gradient(\n    118.3% 119.01% at 35.44% 0%,\n    #4884ca 15.3%,\n    #2b89f8 78.83%\n  );\n  transform: translate(-30px, -40px);\n}\n.c3:after {\n  content: \"\";\n  position: absolute;\n  width: 150px;\n  height: 150px;\n  filter: blur(50px);\n  border-radius: 50%;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  background: #cbe8f7;\n  z-index: -1;\n}\n.recording-countdown {\n  pointer-events: all;\n}\n.recording-countdown .c2 {\n  transform: translate(-15px, 15px);\n}\n.recording-countdown .c3 {\n  transform: translate(-10px, -5px);\n}\n.countdown-info {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  bottom: 20px;\n  border-radius: 30px;\n  border: 2px solid rgba(255, 255, 255, 0.3);\n  text-align: center;\n  display: block;\n  padding: 10px 20px;\n  font-family: $font-medium;\n  font-size: $font-size-normal;\n  line-height: 1.4;\n  letter-spacing: normal;\n  text-transform: none;\n  color: $color-text-contrast;\n  z-index: $z-index-max;\n  width: fit-content;\n}\n"
  },
  {
    "path": "src/pages/Content/cursor/trackClicks.js",
    "content": "export function startClickTracking(\n  isRegion = false,\n  regionWidth = 0,\n  regionHeight = 0,\n  regionX = 0,\n  regionY = 0,\n  contentStateRef = null // <- optional\n) {\n  const handleClick = async (e) => {\n    // Skip if blur mode is active\n    if (contentStateRef?.current?.blurMode) return;\n\n    // Ignore clicks inside the toolbar\n    if (\n      e.target.closest(\".ToolbarRoot\") ||\n      e.target.closest(\".ToolbarRecordingControls\") ||\n      e.target.closest(\".ToolbarToggleWrap\") ||\n      e.target.closest(\".ToolbarPaused\") ||\n      e.target.closest(\".Toast\") ||\n      e.target.closest(\"#screenity-root-container\")\n    ) {\n      return;\n    }\n\n    const canvasWrapper = document.getElementById(\"canvas-wrapper-screenity\");\n    if (canvasWrapper && canvasWrapper.contains(e.target)) {\n      return;\n    }\n\n    const { surface, recordingWindowId, recordingType } =\n      await chrome.storage.local.get([\n        \"surface\",\n        \"recordingWindowId\",\n        \"recordingType\",\n      ]);\n\n    if (recordingType === \"camera\") {\n      return;\n    }\n\n    let clickX = e.clientX;\n    let clickY = e.clientY;\n\n    if (isRegion) {\n      // Check if click is inside region bounds\n      const inRegion =\n        clickX >= regionX &&\n        clickX <= regionX + regionWidth &&\n        clickY >= regionY &&\n        clickY <= regionY + regionHeight;\n\n      if (!inRegion) {\n        return;\n      }\n\n      // Normalize to region-relative coordinates\n      clickX = clickX - regionX;\n      clickY = clickY - regionY;\n    }\n\n    chrome.runtime.sendMessage({\n      type: \"click-event\",\n      payload: {\n        x: clickX,\n        y: clickY,\n        relativeToRegion: isRegion,\n        surface: surface || \"unknown\",\n        recordingWindowId,\n        timestamp: Date.now(),\n        region: isRegion,\n        isTab: recordingType === \"region\",\n      },\n    });\n  };\n\n  window.addEventListener(\"mousedown\", handleClick, true);\n  return () => window.removeEventListener(\"mousedown\", handleClick, true);\n}\n"
  },
  {
    "path": "src/pages/Content/images/popup/images.js",
    "content": "// I need to make this work for a Chrome extension, so I can't import images, instead it needs to be a string with the path to the image\nconst URL =\n  \"chrome-extension://\" + chrome.i18n.getMessage(\"@@extension_id\") + \"/assets\";\n\nconst DropdownIcon = `${URL}/dropdown.svg`;\nconst MicOnIcon = `${URL}/mic-on.svg`;\nconst MicOffIcon = `${URL}/mic-off.svg`;\nconst CameraOnIcon = `${URL}/camera-on.svg`;\nconst CameraOffIcon = `${URL}/camera-off.svg`;\nconst CheckWhiteIcon = `${URL}/check-white.svg`;\nconst Waveform = `${URL}/waveform.svg`;\nconst RecordTabActive = `${URL}/record-tab-active.svg`;\nconst RecordTabInactive = `${URL}/record-tab-inactive.svg`;\nconst VideoTabActive = `${URL}/video-tab-active.svg`;\nconst VideoTabInactive = `${URL}/video-tab-inactive.svg`;\nconst ScreenTabOn = `${URL}/screen-tab-on.svg`;\nconst ScreenTabOff = `${URL}/screen-tab-off.svg`;\nconst RegionTabOn = `${URL}/region-tab-on.svg`;\nconst RegionTabOff = `${URL}/region-tab-off.svg`;\nconst AudioTabOn = `${URL}/audio-tab-on.svg`;\nconst AudioTabOff = `${URL}/audio-tab-off.svg`;\nconst MockupTabOn = `${URL}/mockup-tab-on.svg`;\nconst MockupTabOff = `${URL}/mockup-tab-off.svg`;\n// const TempLogo = `${URL}/temp-logo.png`;\nconst TempLogo = `${URL}/new-logo.svg`;\nconst TempFigma = `${URL}/temp/figma.webp`;\nconst TempTwitter = `${URL}/temp/twitter.webp`;\nconst TempDesignSystem = `${URL}/temp/designsystem.webp`;\nconst TempMarketing = `${URL}/temp/marketing.webp`;\nconst TempSubstack = `${URL}/temp/substack.webp`;\nconst CopyLinkIcon = `${URL}/copy-link.svg`;\nconst MoreActionsIcon = `${URL}/more-actions.svg`;\nconst ProfilePic = `${URL}/pfp.png`;\nconst HandleControl = `${URL}/canvas/handle.png`;\nconst RotateControl = `${URL}/canvas/rotate.png`;\nconst MiddleHandleControl = `${URL}/canvas/middle-handle.png`;\nconst MiddleHandleControlV = `${URL}/canvas/middle-handle-v.png`;\nconst DefaultCursor = `${URL}/cursors/default.svg`;\nconst CameraTabIconOn = `${URL}/camera-tab-icon-on.svg`;\nconst CameraTabIconOff = `${URL}/camera-tab-icon-off.svg`;\nconst CameraOffBlue = `${URL}/camera-off-blue.svg`;\nconst MicOffBlue = `${URL}/mic-off-blue.svg`;\nconst DropdownGroup = `${URL}/dropdown-group.svg`;\nconst PlaceholderThumb = `${URL}/placeholder-thumb.png`;\nconst CloseWhiteIcon = `${URL}/close-white.svg`;\n\nexport {\n  DropdownIcon,\n  MicOnIcon,\n  MicOffIcon,\n  CameraOnIcon,\n  CameraOffIcon,\n  CheckWhiteIcon,\n  Waveform,\n  RecordTabActive,\n  RecordTabInactive,\n  VideoTabActive,\n  VideoTabInactive,\n  ScreenTabOn,\n  ScreenTabOff,\n  RegionTabOn,\n  RegionTabOff,\n  AudioTabOn,\n  AudioTabOff,\n  MockupTabOn,\n  MockupTabOff,\n  TempLogo,\n  TempFigma,\n  TempTwitter,\n  TempDesignSystem,\n  TempMarketing,\n  TempSubstack,\n  CopyLinkIcon,\n  MoreActionsIcon,\n  ProfilePic,\n  HandleControl,\n  RotateControl,\n  MiddleHandleControl,\n  MiddleHandleControlV,\n  DefaultCursor,\n  CameraTabIconOn,\n  CameraTabIconOff,\n  CameraOffBlue,\n  MicOffBlue,\n  DropdownGroup,\n  PlaceholderThumb,\n  CloseWhiteIcon,\n};\n"
  },
  {
    "path": "src/pages/Content/index.css",
    "content": "body {\n  margin: 0;\n  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n    monospace;\n}"
  },
  {
    "path": "src/pages/Content/index.js",
    "content": "import React from \"react\";\nimport ReactDOM from \"react-dom/client\";\nimport \"./styles/app.scss\";\nimport Content from \"./Content\";\n\nconst root = ReactDOM.createRoot(document.getElementById(\"root\"));\nroot.render(\n  <React>\n    <Content />\n  </React>\n);\n"
  },
  {
    "path": "src/pages/Content/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Content from \"./Content\";\n\n// Check if screenity-ui already exists, if so, remove it\nconst existingRoot = document.getElementById(\"screenity-ui\");\nif (existingRoot) {\n  document.body.removeChild(existingRoot);\n}\n\nconst root = document.createElement(\"div\");\nroot.id = \"screenity-ui\";\ndocument.body.appendChild(root);\n\nconst appRoot = createRoot(root);\nappRoot.render(<Content />);\n"
  },
  {
    "path": "src/pages/Content/modal/Modal.jsx",
    "content": "import React, { useState, useEffect, useContext, useCallback } from \"react\";\nimport * as AlertDialog from \"@radix-ui/react-alert-dialog\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst Modal = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [title, setTitle] = useState(\"Test\");\n  const [description, setDescription] = useState(\"Description here\");\n  const [button1, setButton1] = useState(\"Submit\");\n  const [button2, setButton2] = useState(\"Cancel\");\n  const [trigger, setTrigger] = useState(() => {});\n  const [trigger2, setTrigger2] = useState(() => {});\n  const [showModal, setShowModal] = useState(false);\n  const [image, setImage] = useState(null);\n  const [learnmore, setLearnMore] = useState(null);\n  const [learnMoreLink, setLearnMoreLink] = useState(() => {});\n  const [colorSafe, setColorSafe] = useState(false);\n  const [sideButton, setSideButton] = useState(false);\n  const [sideButtonAction, setSideButtonAction] = useState(() => {});\n\n  const openModal = useCallback(\n    (\n      title,\n      description,\n      button1,\n      button2,\n      action,\n      action2,\n      image = null,\n      learnMore = null,\n      learnMoreLink = null,\n      colorSafe = false,\n      sideButton = false,\n      sideButtonAction = () => {}\n    ) => {\n      setTitle(title);\n      setDescription(description);\n      setButton1(button1);\n      setButton2(button2);\n      setShowModal(true);\n      setTrigger(() => action);\n      setTrigger2(() => action2);\n      setImage(image);\n      setLearnMore(learnMore);\n      setLearnMoreLink(() => learnMoreLink);\n      setColorSafe(colorSafe);\n      setSideButton(sideButton);\n      setSideButtonAction(() => sideButtonAction);\n    }\n  );\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      openModal: openModal,\n    }));\n\n    return () => {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        openModal: null,\n      }));\n    };\n  }, []);\n\n  return (\n    <AlertDialog.Root\n      open={showModal}\n      modal={false}\n      onOpenChange={(open) => {\n        setShowModal(open);\n      }}\n    >\n      <AlertDialog.Trigger asChild />\n      <AlertDialog.Portal\n        container={props.shadowRef.current.shadowRoot.querySelector(\n          \".container\"\n        )}\n      >\n        <div className=\"AlertDialogOverlay\"></div>\n        <AlertDialog.Content className=\"AlertDialogContent\">\n          <AlertDialog.Title className=\"AlertDialogTitle\">\n            {title}\n          </AlertDialog.Title>\n          <AlertDialog.Description className=\"AlertDialogDescription\">\n            {description.split(\"\\n\").map((line, idx) => (\n              <React.Fragment key={idx}>\n                {line}\n                <br />\n              </React.Fragment>\n            ))}\n            {learnmore && (\n              <>\n                {\" \"}\n                <a href={learnMoreLink} target=\"_blank\">\n                  {learnmore}\n                </a>\n              </>\n            )}\n          </AlertDialog.Description>\n          {image && (\n            <img\n              src={image}\n              style={{\n                width: \"100%\",\n                marginBottom: 15,\n                marginTop: 5,\n                borderRadius: \"15px\",\n              }}\n            />\n          )}\n          <div style={{ display: \"flex\", gap: 12, justifyContent: \"flex-end\" }}>\n            {sideButton && (\n              <button\n                className=\"SideButtonModal\"\n                onClick={() => {\n                  sideButtonAction();\n                  setShowModal(false);\n                }}\n              >\n                {sideButton}\n              </button>\n            )}\n            {button2 && (\n              <AlertDialog.Cancel asChild>\n                <button className=\"Button grey\" onClick={() => trigger2()}>\n                  {button2}\n                </button>\n              </AlertDialog.Cancel>\n            )}\n            {button1 && (\n              <AlertDialog.Action asChild>\n                <button\n                  className={!colorSafe ? \"Button red\" : \"Button blue\"}\n                  onClick={() => trigger()}\n                >\n                  {button1}\n                </button>\n              </AlertDialog.Action>\n            )}\n          </div>\n        </AlertDialog.Content>\n      </AlertDialog.Portal>\n    </AlertDialog.Root>\n  );\n};\n\nexport default Modal;\n"
  },
  {
    "path": "src/pages/Content/modal/styles/_Modal.scss",
    "content": "@use \"../../styles/_variables\" as *;\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.AlertDialogOverlay {\n  background-color: rgba(0, 0, 0, 0.5);\n  position: fixed;\n  inset: 0;\n  animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent {\n  overflow: auto !important;\n  background-color: white;\n  border-radius: 30px;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 90vw;\n  max-width: 500px;\n  max-height: 85vh;\n  padding: 35px 25px;\n  animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: $z-index-max;\n}\n.AlertDialogContent:focus {\n  outline: none;\n}\n\n.AlertDialogTitle {\n  margin: 0;\n  color: $color-text-primary;\n  font-size: $font-size-normal;\n  line-height: 1.4;\n  font-family: $font-bold;\n  font-weight: 700;\n}\n\n.AlertDialogDescription {\n  margin-bottom: 20px;\n  color: $color-text-secondary;\n  font-size: $font-size-normal;\n  line-height: 1.5;\n\n  a {\n    color: $color-primary !important;\n    font-weight: 600 !important;\n    text-decoration: none !important;\n    display: inline-block;\n    cursor: pointer;\n  }\n}\n\n.Button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n}\n.Button.blue {\n  background-color: rgba(48, 128, 248, 0.1);\n  color: $color-primary;\n\n  &:hover {\n    background-color: rgba(48, 128, 248, 0.15);\n    cursor: pointer;\n  }\n  &:focus {\n    box-shadow: $focus-border;\n  }\n}\n.Button.red {\n  background-color: rgba(247, 56, 90, 0.1);\n  color: rgba(247, 56, 90, 1);\n}\n.Button.red:hover {\n  background-color: rgba(247, 56, 90, 0.15);\n  cursor: pointer;\n}\n.Button.red:focus {\n  box-shadow: $focus-border;\n}\n.Button.grey {\n  background: rgba(110, 118, 132, 0.1);\n  color: $color-text-secondary;\n}\n.Button.grey:hover {\n  background: rgba(110, 118, 132, 0.15);\n  cursor: pointer;\n}\n.Button.grey:focus {\n  box-shadow: $focus-border;\n}\n\n@keyframes overlayShow {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes contentShow {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -48%) scale(0.96);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%) scale(1);\n  }\n}\n\n.SideButtonModal {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n  color: $color-text-secondary;\n  font-family: $font-medium;\n\n  &:hover {\n    cursor: pointer;\n    background: rgba(110, 118, 132, 0.05);\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/PopupContainer.jsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  useContext,\n  useLayoutEffect,\n  useRef,\n} from \"react\";\nimport * as Tabs from \"@radix-ui/react-tabs\";\n\nimport {\n  RecordTabActive,\n  RecordTabInactive,\n  VideoTabActive,\n  VideoTabInactive,\n  TempLogo,\n  ProfilePic,\n} from \"../images/popup/images\";\n\nimport { Rnd } from \"react-rnd\";\n\nimport {\n  CloseIconPopup,\n  GrabIconPopup,\n  HelpIconPopup,\n} from \"../toolbar/components/SVG\";\n\n/* Component import */\nimport RecordingTab from \"./layout/RecordingTab\";\nimport VideosTab from \"./layout/VideosTab\";\n\n// Layouts\nimport SettingsMenu from \"./layout/SettingsMenu\";\nimport InactiveSubscription from \"./layout/InactiveSubscription\";\nimport LoggedOut from \"./layout/LoggedOut\";\nimport Welcome from \"./layout/Welcome\";\nimport {\n  runProPopupOnboardingIfNeeded,\n  runProCameraOnboardingIfNeeded,\n} from \"./onboarding/proOnboarding\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\nimport { supportContextQuery } from \"../../utils/buildSupportContext\";\n\nconst PopupContainer = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const contentStateRef = useRef(contentState);\n  const [tab, setTab] = useState(\"record\");\n  const [badge, setBadge] = useState(TempLogo);\n  const DragRef = useRef(null);\n  const PopupRef = useRef(null);\n  const [elastic, setElastic] = React.useState(\"\");\n  const [shake, setShake] = React.useState(\"\");\n  const [dragging, setDragging] = React.useState(\"\");\n  const [onboarding, setOnboarding] = useState(false);\n  const [showProSplash, setShowProSplash] = useState(false);\n  const [open, setOpen] = useState(false);\n  const recordTabRef = useRef(null);\n  const videoTabRef = useRef(null);\n  const pillRef = useRef(null);\n  const [URL, setURL] = useState(\"https://help.screenity.io/\");\n  const isCloudBuild = process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n  const wasCameraActiveRef = useRef(null);\n\n  useEffect(() => {\n    chrome.storage.local.get([\"onboarding\", \"showProSplash\"], (result) => {\n      const nextOnboarding = Boolean(result.onboarding);\n      const nextShowProSplash = Boolean(result.showProSplash);\n      setOnboarding(nextOnboarding);\n      setShowProSplash(nextShowProSplash);\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        onboarding: nextOnboarding,\n        showProSplash: nextShowProSplash,\n      }));\n    });\n  }, [setContentState]);\n\n  useEffect(() => {\n    if (contentState.isLoggedIn) {\n      setOnboarding(false);\n      setShowProSplash(false);\n      return;\n    }\n    setOnboarding(Boolean(contentState.onboarding));\n    setShowProSplash(Boolean(contentState.showProSplash));\n  }, [\n    contentState.isLoggedIn,\n    contentState.onboarding,\n    contentState.showProSplash,\n  ]);\n\n  useEffect(() => {\n    const buildURL = async () => {\n      const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n\n      // Default URL\n      let baseURL = \"https://help.screenity.io/\";\n\n      // If logged in, switch to Tally with prefilled params\n      if (contentState?.isLoggedIn && contentState?.screenityUser) {\n        const { name, email } = contentState.screenityUser;\n        const qs = await supportContextQuery({\n          includeRecordingState: true,\n          source: \"popup\",\n          user: { name, email },\n        });\n        baseURL = `https://tally.so/r/310MNg?extension=true&${qs}`;\n      }\n\n      // If non-English locale, wrap with Google Translate\n      if (!locale.includes(\"en\")) {\n        setURL(\n          `https://translate.google.com/translate?sl=en&tl=${locale}&u=${encodeURIComponent(\n            baseURL\n          )}`\n        );\n      } else {\n        setURL(baseURL);\n      }\n    };\n    buildURL();\n  }, [contentState]);\n\n  const onValueChange = (tab) => {\n    setTab(tab);\n\n    if (contentState.isLoggedIn && contentState.isSubscribed === false) {\n      setBadge(\n        \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text x='0' y='24' font-size='28'>⚠️</text></svg>\"\n      );\n    } else if (tab === \"record\" && !contentState.isLoggedIn) {\n      setBadge(TempLogo);\n    } else {\n      const avatar = contentState?.screenityUser?.avatar;\n      setBadge(avatar || ProfilePic);\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      bigTab: tab,\n    }));\n  };\n  useEffect(() => {\n    setTab(contentState.bigTab);\n  }, []);\n\n  useEffect(() => {\n    if (contentState.isLoggedIn && contentState.isSubscribed === false) {\n      setBadge(\n        \"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32'><text x='0' y='24' font-size='28'>⚠️</text></svg>\"\n      );\n    } else if (tab === \"record\" && !contentState.isLoggedIn) {\n      setBadge(TempLogo);\n    } else {\n      const avatar = contentState?.screenityUser?.avatar;\n      setBadge(avatar || ProfilePic);\n    }\n  }, [\n    contentState.isLoggedIn,\n    contentState.isSubscribed,\n    contentState.wasLoggedIn,\n    tab,\n  ]);\n\n  const showWelcomeSplash = Boolean(\n    isCloudBuild &&\n      !contentState.isLoggedIn &&\n      !contentState.wasLoggedIn &&\n      (\n        onboarding ||\n        showProSplash ||\n        contentState.onboarding ||\n        contentState.showProSplash\n      ),\n  );\n\n  useLayoutEffect(() => {\n    if (!recordTabRef.current || !videoTabRef.current || !pillRef.current)\n      return;\n\n    const tabRef =\n      tab === \"record\" ? recordTabRef.current : videoTabRef.current;\n\n    pillRef.current.style.left = `${tabRef.offsetLeft}px`;\n    pillRef.current.style.width = `${tabRef.getBoundingClientRect().width}px`;\n  }, [tab]);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  useLayoutEffect(() => {\n    function setPopupPosition(e) {\n      let xpos = DragRef.current.getDraggablePosition().x;\n      let ypos = DragRef.current.getDraggablePosition().y;\n\n      // Width and height of popup\n      const width = PopupRef.current.getBoundingClientRect().width;\n      const height = PopupRef.current.getBoundingClientRect().height;\n\n      // Keep popup positioned relative to the bottom and right of the screen, proportionally\n      if (xpos > window.innerWidth + 10) {\n        xpos = window.innerWidth + 10;\n      }\n      if (ypos + height + 40 > window.innerHeight) {\n        ypos = window.innerHeight - height - 40;\n      }\n\n      // Check if attached to right or bottom, if so, keep it there\n      if (contentStateRef.current.popupPosition.fixed) {\n        if (xpos < window.innerWidth) {\n          xpos = window.innerWidth + 10;\n        }\n      }\n\n      DragRef.current.updatePosition({ x: xpos, y: ypos });\n    }\n    window.addEventListener(\"resize\", setPopupPosition);\n    setPopupPosition();\n    return () => window.removeEventListener(\"resize\", setPopupPosition);\n  }, []);\n\n  const handleDragStart = (e, d) => {\n    setDragging(\"ToolbarDragging\");\n  };\n\n  const handleDrag = (e, d) => {\n    // Width and height\n    const width = PopupRef.current.getBoundingClientRect().width;\n    const height = PopupRef.current.getBoundingClientRect().height;\n\n    if (\n      d.x - 40 < width ||\n      d.x > window.innerWidth + 10 ||\n      d.y < 0 ||\n      d.y + height + 40 > window.innerHeight\n    ) {\n      setShake(\"ToolbarShake\");\n    } else {\n      setShake(\"\");\n    }\n  };\n\n  const handleDrop = (e, d) => {\n    let anim = \"ToolbarElastic\";\n    if (e === null) {\n      anim = \"\";\n    }\n    setShake(\"\");\n    setDragging(\"\");\n    let xpos = d.x;\n    let ypos = d.y;\n\n    // Width and height\n    const width = PopupRef.current.getBoundingClientRect().width;\n    const height = PopupRef.current.getBoundingClientRect().height;\n\n    // Check if popup is off screen\n    if (d.x - 40 < width) {\n      setElastic(anim);\n      xpos = width + 40;\n    } else if (d.x + 10 > window.innerWidth) {\n      setElastic(anim);\n      xpos = window.innerWidth + 10;\n    }\n\n    if (d.y < 0) {\n      setElastic(anim);\n      ypos = 0;\n    } else if (d.y + height + 40 > window.innerHeight) {\n      setElastic(anim);\n      ypos = window.innerHeight - height - 40;\n    }\n    DragRef.current.updatePosition({ x: xpos, y: ypos });\n\n    setTimeout(() => {\n      setElastic(\"\");\n    }, 250);\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      popupPosition: {\n        ...prevContentState.popupPosition,\n        offsetX: xpos,\n        offsetY: ypos,\n        left: xpos < window.innerWidth / 2 ? true : false,\n        right: xpos < window.innerWidth / 2 ? false : true,\n        top: ypos < window.innerHeight / 2 ? true : false,\n        bottom: ypos < window.innerHeight / 2 ? false : true,\n      },\n    }));\n\n    // Is it on the left or right, also top or bottom\n    let left = xpos < window.innerWidth / 2 ? true : false;\n    let right = xpos < window.innerWidth / 2 ? false : true;\n    let top = ypos < window.innerHeight / 2 ? true : false;\n    let bottom = ypos < window.innerHeight / 2 ? false : true;\n    let offsetX = xpos;\n    let offsetY = ypos;\n    let fixed = d.x + 9 > window.innerWidth ? true : false;\n\n    if (right) {\n      offsetX = window.innerWidth - xpos;\n    }\n    if (bottom) {\n      offsetY = window.innerHeight - ypos;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      popupPosition: {\n        ...prevContentState.popupPosition,\n        offsetX: offsetX,\n        offsetY: offsetY,\n        left: left,\n        right: right,\n        top: top,\n        bottom: bottom,\n        fixed: fixed,\n      },\n    }));\n\n    chrome.storage.local.set({\n      popupPosition: {\n        offsetX: offsetX,\n        offsetY: offsetY,\n        left: left,\n        right: right,\n        top: top,\n        bottom: bottom,\n        fixed: fixed,\n      },\n    });\n  };\n\n  useEffect(() => {\n    let x = contentState.popupPosition.offsetX;\n    let y = contentState.popupPosition.offsetY;\n\n    if (contentState.popupPosition.bottom) {\n      y = window.innerHeight - contentState.popupPosition.offsetY;\n    }\n\n    if (contentState.popupPosition.right) {\n      x = window.innerWidth - contentState.popupPosition.offsetX;\n    }\n\n    DragRef.current.updatePosition({ x: x, y: y });\n\n    handleDrop(null, { x: x, y: y });\n  }, []);\n\n  useEffect(() => {\n    requestAnimationFrame(() => {\n      const tabRef =\n        contentState.bigTab === \"record\"\n          ? recordTabRef.current\n          : videoTabRef.current;\n\n      if (tabRef && pillRef.current) {\n        pillRef.current.style.left = `${tabRef.offsetLeft}px`;\n        pillRef.current.style.width = `${\n          tabRef.getBoundingClientRect().width\n        }px`;\n      }\n    });\n  }, [\n    contentState.isLoggedIn,\n    contentState.bigTab,\n    contentState.wasLoggedIn,\n    pillRef.current,\n  ]);\n\n  useEffect(() => {\n    const isPro = Boolean(contentState.isLoggedIn && contentState.isSubscribed);\n    if (!isCloudBuild || !isPro) return;\n    runProPopupOnboardingIfNeeded({\n      rootContext: props.shadowRef?.current?.shadowRoot || document,\n      isPro,\n      isLoggedIn: Boolean(contentState.isLoggedIn),\n      popupOpen: Boolean(contentState.showPopup && contentState.showExtension),\n      cameraEnabled: Boolean(contentState.cameraActive),\n      pendingRecording: Boolean(contentState.pendingRecording),\n      preparingRecording: Boolean(contentState.preparingRecording),\n      recording: Boolean(contentState.recording),\n      countdownActive: Boolean(contentState.countdownActive),\n      isCountdownVisible: Boolean(contentState.isCountdownVisible),\n    });\n  }, [\n    isCloudBuild,\n    contentState.isLoggedIn,\n    contentState.isSubscribed,\n    contentState.showPopup,\n    contentState.showExtension,\n    contentState.recordingToScene,\n    contentState.cameraActive,\n    contentState.pendingRecording,\n    contentState.preparingRecording,\n    contentState.recording,\n    contentState.countdownActive,\n    contentState.isCountdownVisible,\n    props.shadowRef,\n  ]);\n\n  useEffect(() => {\n    const isPro = Boolean(contentState.isLoggedIn && contentState.isSubscribed);\n    const cameraEnabled = Boolean(contentState.cameraActive);\n    if (wasCameraActiveRef.current === null) {\n      wasCameraActiveRef.current = cameraEnabled;\n      return;\n    }\n    const becameEnabled = cameraEnabled && !wasCameraActiveRef.current;\n    wasCameraActiveRef.current = cameraEnabled;\n    if (!becameEnabled || !isCloudBuild || !isPro) return;\n    runProCameraOnboardingIfNeeded({\n      rootContext: props.shadowRef?.current?.shadowRoot || document,\n      isPro,\n      isLoggedIn: Boolean(contentState.isLoggedIn),\n      popupOpen: Boolean(contentState.showPopup && contentState.showExtension),\n      cameraEnabled,\n      pendingRecording: Boolean(contentState.pendingRecording),\n      preparingRecording: Boolean(contentState.preparingRecording),\n      recording: Boolean(contentState.recording),\n      countdownActive: Boolean(contentState.countdownActive),\n      isCountdownVisible: Boolean(contentState.isCountdownVisible),\n    });\n  }, [\n    isCloudBuild,\n    contentState.cameraActive,\n    contentState.isLoggedIn,\n    contentState.isSubscribed,\n    contentState.showPopup,\n    contentState.showExtension,\n    contentState.pendingRecording,\n    contentState.preparingRecording,\n    contentState.recording,\n    contentState.countdownActive,\n    contentState.isCountdownVisible,\n    props.shadowRef,\n  ]);\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        top: 0,\n        left: 0,\n        right: 0,\n        bottom: 0,\n        width: \"100vw\",\n        height: \"100vh\",\n      }}\n    >\n      <div className={\"ToolbarBounds\" + \" \" + shake}></div>\n      <Rnd\n        default={{\n          x: contentState.popupPosition.offsetX,\n          y: contentState.popupPosition.offsetY,\n        }}\n        className={\n          \"react-draggable\" + \" \" + elastic + \" \" + shake + \" \" + dragging\n        }\n        enableResizing={false}\n        dragHandleClassName=\"drag-area\"\n        onDragStart={handleDragStart}\n        onDrag={handleDrag}\n        onDragStop={handleDrop}\n        ref={DragRef}\n      >\n        <div\n          className=\"popup-container\"\n          id=\"pro-onboarding-popup-container\"\n          ref={PopupRef}\n        >\n          <div\n            className={open ? \"popup-drag-head\" : \"popup-drag-head drag-area\"}\n          ></div>\n          <div\n            className={\n              open ? \"popup-controls open\" : \"popup-controls drag-area\"\n            }\n          >\n            <SettingsMenu\n              shadowRef={props.shadowRef}\n              open={open}\n              setOpen={setOpen}\n            />\n            <div\n              style={{ marginBottom: \"-4px\", cursor: \"pointer\" }}\n              onClick={() => {\n                window.open(URL, \"_blank\");\n              }}\n            >\n              <HelpIconPopup />\n            </div>\n            <div\n              className=\"popup-control popup-close\"\n              onClick={() => {\n                setContentState((prevContentState) => ({\n                  ...prevContentState,\n                  showExtension: false,\n                }));\n              }}\n            >\n              <CloseIconPopup />\n            </div>\n          </div>\n          <div className=\"popup-cutout drag-area\">\n            {contentState.isLoggedIn && contentState.isSubscribed === false ? (\n              <div\n                style={{\n                  fontSize: \"34px\",\n                }}\n              >\n                ⚠️\n              </div>\n            ) : (\n              <img\n                src={badge}\n                crossOrigin=\"anonymous\"\n                style={{\n                  width:\n                    tab === \"record\" && !contentState.isLoggedIn\n                      ? \"90%\"\n                      : \"100%\",\n                  height:\n                    tab === \"record\" && !contentState.isLoggedIn\n                      ? \"90%\"\n                      : \"100%\",\n                  filter:\n                    tab === \"record\" && !contentState.isLoggedIn\n                      ? \"drop-shadow(rgba(86, 123, 218, 0.35) 0px 4px 11px) drop-shadow(rgba(53, 87, 98, 0.2) 0px 4px 10px)\"\n                      : \"none\",\n                  userSelect: \"none\",\n                  pointerEvents: \"none\",\n                }}\n                draggable={false}\n                referrerPolicy=\"no-referrer\"\n              />\n            )}\n          </div>\n          <div className=\"popup-nav\"></div>\n          <div className=\"popup-content\">\n            {showWelcomeSplash ? (\n              <Welcome\n                setOnboarding={() => {\n                  setOnboarding(false);\n                  setShowProSplash(false);\n                  chrome.storage.local.set({\n                    onboarding: false,\n                    showProSplash: false,\n                    firstTimePro: false,\n                  });\n                  setContentState((prev) => ({\n                    ...prev,\n                    onboarding: false,\n                    showProSplash: false,\n                  }));\n                }}\n                isBack={showProSplash}\n                clearBack={() => {\n                  setShowProSplash(false);\n                  setContentState((prev) => ({\n                    ...prev,\n                    showProSplash: false,\n                  }));\n                  chrome.storage.local.set({ showProSplash: false });\n                }}\n                setContentState={setContentState}\n              />\n            ) : isCloudBuild &&\n            contentState.isSubscribed === false &&\n            contentState.isLoggedIn === true ? (\n              <InactiveSubscription\n                subscription={contentState.proSubscription}\n                hasSubscribedBefore={contentState.hasSubscribedBefore}\n                onManageClick={() => {\n                  const type = contentState.hasSubscribedBefore\n                    ? \"handle-reactivate\"\n                    : \"handle-upgrade\";\n                  chrome.runtime.sendMessage({ type });\n                }}\n                onDowngradeClick={async () => {\n                  chrome.runtime.sendMessage({ type: \"handle-logout\" });\n                  setContentState((prev) => ({\n                    ...prev,\n                    isLoggedIn: false,\n                    isSubscribed: false,\n                    screenityUser: null,\n                    proSubscription: null,\n                    wasLoggedIn: false,\n                    bigTab: \"record\",\n                  }));\n                  contentState.openToast(\n                    chrome.i18n.getMessage(\"loggedOutToastTitle\"),\n                    () => {},\n                    2000\n                  );\n                }}\n              />\n            ) : isCloudBuild &&\n              !contentState.isLoggedIn &&\n              contentState.wasLoggedIn ? (\n              <LoggedOut\n                onManageClick={() => {\n                  // Log back in\n                  chrome.runtime.sendMessage({ type: \"handle-login\" });\n                }}\n                onDowngradeClick={() => {\n                  chrome.storage.local.set({\n                    wasLoggedIn: false,\n                    stayLoggedOut: true,\n                  });\n                  setContentState((prev) => ({\n                    ...prev,\n                    isLoggedIn: false,\n                    wasLoggedIn: false,\n                    bigTab: \"record\", // Ensure UI state sync\n                  }));\n                  setTab(\"record\"); // Switch immediately\n\n                  requestAnimationFrame(() => {\n                    if (recordTabRef.current && pillRef.current) {\n                      const tabRef = recordTabRef.current;\n                      pillRef.current.style.left = `${tabRef.offsetLeft}px`;\n                      pillRef.current.style.width = `${\n                        tabRef.getBoundingClientRect().width\n                      }px`;\n                    }\n                  });\n                }}\n              />\n            ) : (\n              <Tabs.Root\n                className=\"TabsRoot tl\"\n                value={tab}\n                onValueChange={onValueChange}\n              >\n                <Tabs.List\n                  className=\"TabsList tl\"\n                  data-value={tab}\n                  aria-label=\"Manage your account\"\n                  tabIndex={0}\n                >\n                  <div className=\"pill-anim\" ref={pillRef}></div>\n                  <Tabs.Trigger\n                    className=\"TabsTrigger tl\"\n                    value=\"record\"\n                    ref={recordTabRef}\n                    tabIndex={0}\n                  >\n                    <div className=\"TabsTriggerIcon\">\n                      <img\n                        src={\n                          tab === \"record\" ? RecordTabActive : RecordTabInactive\n                        }\n                      />\n                    </div>\n                    {chrome.i18n.getMessage(\"recordTab\")}\n                  </Tabs.Trigger>\n                  <Tabs.Trigger\n                    className=\"TabsTrigger tl\"\n                    value=\"dashboard\"\n                    ref={videoTabRef}\n                    tabIndex={0}\n                  >\n                    <div className=\"TabsTriggerIcon\">\n                      <img\n                        src={\n                          tab === \"dashboard\"\n                            ? VideoTabActive\n                            : VideoTabInactive\n                        }\n                      />\n                    </div>\n                    {chrome.i18n.getMessage(\"videosTab\")}\n                  </Tabs.Trigger>\n                </Tabs.List>\n                <Tabs.Content className=\"TabsContent tl\" value=\"record\">\n                  <RecordingTab shadowRef={props.shadowRef} />\n                </Tabs.Content>\n                <Tabs.Content className=\"TabsContent tl\" value=\"dashboard\">\n                  <VideosTab shadowRef={props.shadowRef} />\n                </Tabs.Content>\n              </Tabs.Root>\n            )}\n          </div>\n          {contentState.settingsOpen && (\n            <div\n              className=\"HelpSection\"\n              onClick={() => {\n                window.open(URL, \"_blank\");\n              }}\n            >\n              <span className=\"HelpIcon\">\n                <HelpIconPopup />\n              </span>\n              {chrome.i18n.getMessage(\"helpPopup\")}\n            </div>\n          )}\n        </div>\n      </Rnd>\n    </div>\n  );\n};\n\nexport default PopupContainer;\n"
  },
  {
    "path": "src/pages/Content/popup/components/BackgroundEffects.jsx",
    "content": "import React, { useContext } from \"react\";\nimport * as ToggleGroup from \"@radix-ui/react-toggle-group\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst BackgroundEffects = () => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n\n  // Background images\n  const URL =\n    \"chrome-extension://\" +\n    chrome.i18n.getMessage(\"@@extension_id\") +\n    \"/assets/\";\n\n  const images = [\n    URL + \"backgrounds/back1.webp\",\n    URL + \"backgrounds/back2.webp\",\n    URL + \"backgrounds/back3.webp\",\n    URL + \"backgrounds/back4.webp\",\n    URL + \"backgrounds/back5.webp\",\n    URL + \"backgrounds/back6.webp\",\n  ];\n\n  return (\n    <div className=\"background-effects\">\n      <ToggleGroup.Root\n        className=\"background-effects-toggle-group\"\n        type=\"single\"\n        defaultValue=\"blur\"\n        value={contentState.backgroundEffect}\n        onValueChange={(value) => {\n          if (value) {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              backgroundEffect: value,\n            }));\n            chrome.storage.local.set({ backgroundEffect: value });\n          }\n        }}\n      >\n        <ToggleGroup.Item\n          className=\"background-effect\"\n          value=\"blur\"\n          aria-label=\"Blur effect\"\n        >\n          <span>{chrome.i18n.getMessage(\"blurTypeLabel\")}</span>\n          <img src={URL + \"backgrounds/blur.webp\"} alt=\"blur\" />\n        </ToggleGroup.Item>\n        {images.map((image, index) => (\n          <ToggleGroup.Item\n            className=\"background-effect\"\n            value={image}\n            aria-label=\"Background image\"\n            key={index}\n          >\n            <img src={image} alt=\"background\" />\n          </ToggleGroup.Item>\n        ))}\n      </ToggleGroup.Root>\n    </div>\n  );\n};\n\nexport default BackgroundEffects;\n"
  },
  {
    "path": "src/pages/Content/popup/components/Dropdown.jsx",
    "content": "import React, { useEffect, useState, useContext, useRef } from \"react\";\n\nimport * as Select from \"@radix-ui/react-select\";\nimport {\n  DropdownIcon,\n  CheckWhiteIcon,\n  CameraOnIcon,\n  CameraOffIcon,\n  MicOnIcon,\n  MicOffIcon,\n} from \"../../images/popup/images\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst Dropdown = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [label, setLabel] = useState(chrome.i18n.getMessage(\"None\"));\n  const [open, setOpen] = useState(false);\n  const cameraAnchorId =\n    props.type === \"camera\" ? \"pro-onboarding-camera-toggle\" : undefined;\n\n  const updateItems = () => {\n    if (props.type === \"camera\") {\n      if (\n        contentState.defaultVideoInput === \"none\" ||\n        !contentState.cameraActive\n      ) {\n        setLabel(chrome.i18n.getMessage(\"noCameraDropdownLabel\"));\n      } else {\n        // Check if defaultVideoInput is in camdevices, if not set to none\n        if (\n          contentState.videoInput.find(\n            (device) => device.deviceId === contentState.defaultVideoInput\n          )\n        ) {\n          setLabel(\n            contentState.videoInput.find(\n              (device) => device.deviceId === contentState.defaultVideoInput\n            ).label\n          );\n        } else {\n          setLabel(chrome.i18n.getMessage(\"noCameraDropdownLabel\"));\n        }\n      }\n    } else {\n      if (\n        contentState.defaultAudioInput === \"none\" ||\n        (!contentState.micActive && !contentState.pushToTalk)\n      ) {\n        setLabel(chrome.i18n.getMessage(\"noMicrophoneDropdownLabel\"));\n      } else {\n        // Check if defaultAudioInput is in micdevices, if not set to none\n        if (\n          contentState.audioInput.find(\n            (device) => device.deviceId === contentState.defaultAudioInput\n          )\n        ) {\n          setLabel(\n            contentState.audioInput.find(\n              (device) => device.deviceId === contentState.defaultAudioInput\n            ).label\n          );\n        } else {\n          setLabel(chrome.i18n.getMessage(\"noMicrophoneDropdownLabel\"));\n        }\n      }\n    }\n  };\n\n  useEffect(() => {\n    updateItems();\n  }, [\n    contentState.defaultAudioInput,\n    contentState.defaultVideoInput,\n    contentState.audioInput,\n    contentState.videoInput,\n    contentState.cameraActive,\n    contentState.micActive,\n  ]);\n\n  useEffect(() => {\n    updateItems();\n  }, []);\n\n  const toggleActive = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setOpen(false);\n    if (props.type === \"camera\") {\n      if (contentState.cameraActive) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          cameraActive: false,\n        }));\n        chrome.storage.local.set({\n          cameraActive: false,\n        });\n        setLabel(chrome.i18n.getMessage(\"noCameraDropdownLabel\"));\n      } else {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          cameraActive: true,\n        }));\n        chrome.storage.local.set({\n          cameraActive: true,\n        });\n        setLabel(\n          contentState.videoInput.find(\n            (device) => device.deviceId === contentState.defaultVideoInput\n          ).label\n        );\n      }\n    } else {\n      if (contentState.micActive) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          micActive: false,\n        }));\n        chrome.storage.local.set({\n          micActive: false,\n        });\n        setLabel(chrome.i18n.getMessage(\"noMicrophoneDropdownLabel\"));\n      } else {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          micActive: true,\n        }));\n        chrome.storage.local.set({\n          micActive: true,\n        });\n        setLabel(\n          contentState.audioInput.find(\n            (device) => device.deviceId === contentState.defaultAudioInput\n          ).label\n        );\n      }\n    }\n  };\n\n  const clickedIcon = useRef(false);\n\n  return (\n    <Select.Root\n      open={open}\n      onOpenChange={(open) => {\n        if (clickedIcon.current) return;\n        setOpen(open);\n      }}\n      value={\n        props.type === \"camera\" && contentState.cameraActive\n          ? contentState.defaultVideoInput\n          : props.type === \"camera\" && !contentState.cameraActive\n          ? \"none\"\n          : props.type === \"mic\" &&\n            (contentState.micActive || contentState.pushToTalk)\n          ? contentState.defaultAudioInput\n          : props.type === \"mic\" && !contentState.micActive\n          ? \"none\"\n          : \"none\"\n      }\n      onValueChange={(newValue) => {\n        if (props.type === \"camera\") {\n          if (newValue === \"none\") {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              cameraActive: false,\n            }));\n            chrome.storage.local.set({\n              cameraActive: false,\n            });\n            setLabel(chrome.i18n.getMessage(\"noCameraDropdownLabel\"));\n          } else {\n            const selectedLabel =\n              contentState.videoInput.find(\n                (device) => device.deviceId === newValue\n              )?.label || \"\";\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              defaultVideoInput: newValue,\n              defaultVideoInputLabel: selectedLabel,\n              cameraActive: true,\n            }));\n            chrome.storage.local.set({\n              defaultVideoInput: newValue,\n              defaultVideoInputLabel: selectedLabel,\n              cameraActive: true,\n            });\n            chrome.runtime.sendMessage({\n              type: \"switch-camera\",\n              id: newValue,\n            });\n            setLabel(selectedLabel);\n          }\n        } else {\n          if (newValue === \"none\") {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              micActive: false,\n            }));\n            chrome.storage.local.set({\n              micActive: false,\n            });\n            setLabel(chrome.i18n.getMessage(\"noMicrophoneDropdownLabel\"));\n          } else {\n            const selectedLabel =\n              contentState.audioInput.find(\n                (device) => device.deviceId === newValue\n              )?.label || \"\";\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              defaultAudioInput: newValue,\n              defaultAudioInputLabel: selectedLabel,\n              micActive: true,\n            }));\n            chrome.storage.local.set({\n              defaultAudioInput: newValue,\n              defaultAudioInputLabel: selectedLabel,\n              micActive: true,\n            });\n            setLabel(selectedLabel);\n          }\n        }\n      }}\n    >\n      <Select.Trigger\n        className=\"SelectTrigger\"\n        aria-label=\"Food\"\n        id={cameraAnchorId}\n      >\n        <Select.Icon\n          className=\"SelectIconType\"\n          onClick={(e) => {\n            e.stopPropagation();\n            e.preventDefault();\n            setOpen(false);\n            clickedIcon.current = true;\n          }}\n          onMouseDown={(e) => {\n            e.stopPropagation();\n            e.preventDefault();\n            setOpen(false);\n            clickedIcon.current = true;\n          }}\n          onMouseUp={(e) => {\n            clickedIcon.current = false;\n          }}\n        >\n          <div\n            className=\"SelectIconButton\"\n            onClick={(e) => {\n              e.stopPropagation();\n              setOpen(false);\n              toggleActive(e);\n              clickedIcon.current = true;\n            }}\n            onMouseDown={(e) => {\n              e.stopPropagation();\n              e.preventDefault();\n              setOpen(false);\n              clickedIcon.current = true;\n            }}\n            onContextMenu={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n            }}\n            onMouseUp={(e) => {\n              clickedIcon.current = false;\n            }}\n          >\n            {props.type == \"camera\" && (\n              <img\n                src={\n                  contentState.defaultVideoInput === \"none\" ||\n                  !contentState.cameraActive\n                    ? CameraOffIcon\n                    : CameraOnIcon\n                }\n              />\n            )}\n            {props.type == \"mic\" && (\n              <img\n                src={\n                  contentState.defaultAudioInput === \"none\" ||\n                  !contentState.micActive\n                    ? MicOffIcon\n                    : MicOnIcon\n                }\n              />\n            )}\n          </div>\n        </Select.Icon>\n        <div className=\"SelectValue\">\n          <Select.Value\n            placeholder={chrome.i18n.getMessage(\n              \"selectSourceDropdownPlaceholder\"\n            )}\n          >\n            {label}\n          </Select.Value>\n        </div>\n        {props.type == \"camera\" &&\n          (contentState.defaultVideoInput == \"none\" ||\n            !contentState.cameraActive) && (\n            <div className=\"SelectOff\">\n              {chrome.i18n.getMessage(\"offLabel\")}\n            </div>\n          )}\n        {props.type == \"mic\" &&\n          (contentState.defaultAudioInput == \"none\" ||\n            (!contentState.micActive && !contentState.pushToTalk)) && (\n            <div className=\"SelectOff\">\n              {chrome.i18n.getMessage(\"offLabel\")}\n            </div>\n          )}\n        <Select.Icon className=\"SelectIconDrop\">\n          <img src={DropdownIcon} />\n        </Select.Icon>\n      </Select.Trigger>\n      <Select.Portal\n        container={props.shadowRef.current.shadowRoot.querySelector(\n          \".container\"\n        )}\n      >\n        <Select.Content position=\"popper\" className=\"SelectContent\">\n          <Select.ScrollUpButton className=\"SelectScrollButton\"></Select.ScrollUpButton>\n          <Select.Viewport className=\"SelectViewport\">\n            <Select.Group>\n              <SelectItem value=\"none\">\n                {props.type == \"camera\"\n                  ? chrome.i18n.getMessage(\"noCameraDropdownLabel\")\n                  : chrome.i18n.getMessage(\"noMicrophoneDropdownLabel\")}\n              </SelectItem>\n            </Select.Group>\n            {props.type == \"camera\" && contentState.videoInput.length > 0 && (\n              <Select.Separator className=\"SelectSeparator\" />\n            )}\n            {props.type == \"mic\" && contentState.audioInput.length > 0 && (\n              <Select.Separator className=\"SelectSeparator\" />\n            )}\n            <Select.Group>\n              {props.type == \"camera\" &&\n                contentState.videoInput.map((device) => (\n                  <SelectItem value={device.deviceId} key={device.deviceId}>\n                    {device.label}\n                  </SelectItem>\n                ))}\n              {props.type == \"mic\" &&\n                contentState.audioInput.map((device) => (\n                  <SelectItem value={device.deviceId} key={device.deviceId}>\n                    {device.label}\n                  </SelectItem>\n                ))}\n            </Select.Group>\n          </Select.Viewport>\n          <Select.ScrollDownButton className=\"SelectScrollButton\"></Select.ScrollDownButton>\n        </Select.Content>\n      </Select.Portal>\n    </Select.Root>\n  );\n};\n\nconst SelectItem = React.forwardRef(\n  ({ children, className, ...props }, forwardedRef) => {\n    return (\n      <Select.Item className=\"SelectItem\" {...props} ref={forwardedRef}>\n        <Select.ItemText>{children}</Select.ItemText>\n        <Select.ItemIndicator className=\"SelectItemIndicator\">\n          <img src={CheckWhiteIcon} />\n        </Select.ItemIndicator>\n      </Select.Item>\n    );\n  }\n);\n\nexport default Dropdown;\n"
  },
  {
    "path": "src/pages/Content/popup/components/RegionDimensions.jsx",
    "content": "import React, { useState, useRef, useContext, useEffect } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst RegionDimensions = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  const handleWidth = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionWidth: value,\n      fromRegion: false,\n    }));\n    chrome.storage.local.set({\n      regionWidth: value,\n    });\n  };\n\n  const handleHeight = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionHeight: value,\n      fromRegion: false,\n    }));\n    chrome.storage.local.set({\n      regionHeight: value,\n    });\n  };\n\n  return (\n    <div className=\"region-dimensions\">\n      <div className=\"region-input\">\n        <label htmlFor=\"region-width\" style={{ display: \"none\" }}>\n          {chrome.i18n.getMessage(\"regionWidthLabel\")}\n        </label>\n        <input\n          id=\"region-width\"\n          onChange={(e) => handleWidth(e)}\n          onBlur={(e) => {\n            if (e.target.value === \"\") {\n              setContentState((prevContentState) => ({\n                ...prevContentState,\n                regionWidth: 100,\n                fromRegion: false,\n              }));\n              chrome.storage.local.set({\n                regionWidth: 100,\n              });\n            }\n          }}\n          value={contentState.regionWidth}\n        />\n        <span>W</span>\n      </div>\n      <div className=\"region-input\">\n        <label htmlFor=\"region-height\" style={{ display: \"none\" }}>\n          {chrome.i18n.getMessage(\"regionHeightLabel\")}\n        </label>\n        <input\n          id=\"region-height\"\n          onChange={(e) => handleHeight(e)}\n          onBlur={(e) => {\n            if (e.target.value === \"\") {\n              setContentState((prevContentState) => ({\n                ...prevContentState,\n                regionHeight: 100,\n                fromRegion: false,\n              }));\n              chrome.storage.local.set({\n                regionHeight: 100,\n              });\n            }\n          }}\n          value={contentState.regionHeight}\n        />\n        <span>H</span>\n      </div>\n    </div>\n  );\n};\n\nexport default RegionDimensions;\n"
  },
  {
    "path": "src/pages/Content/popup/components/Switch.jsx",
    "content": "import React, { useContext, useEffect, useState, useRef } from \"react\";\nimport * as S from \"@radix-ui/react-switch\";\n\n// Components\nimport { DropdownIcon } from \"../../images/popup/images\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nexport const BaseSwitch = ({ value, checked, onChange }) => (\n  <S.Root\n    className=\"SwitchRoot\"\n    id={value}\n    checked={checked}\n    onCheckedChange={onChange}\n  >\n    <S.Thumb className=\"SwitchThumb\" />\n  </S.Root>\n);\n\nconst Switch = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const switchRef = useRef(null);\n  const switchId = props.anchorId || props.value || props.name;\n  const switchRowId =\n    props.rowAnchorId ||\n    (props.anchorId ? `${props.anchorId}-row` : undefined);\n  const [hideToolbarLabel, setHideToolbarLabel] = useState(\n    chrome.i18n.getMessage(\"hideToolbarLabel\")\n  );\n  const [hideToolbarState, setHideToolbarState] = useState(1);\n\n  useEffect(() => {\n    // Check click outside\n    const handleClickOutside = (event) => {\n      if (props.name != \"hideUI\") return;\n      if (\n        dropdownRef.current &&\n        !dropdownRef.current.contains(event.target) &&\n        !dropdownInRef.current.contains(event.target)\n      ) {\n        if (dropdownRef.current.querySelector(\":hover\")) return;\n        if (dropdownInRef.current.querySelector(\":hover\")) return;\n        // Check if any children of dropdownref are clicked also\n        let children = dropdownRef.current.querySelectorAll(\"*\");\n        for (let i = 0; i < children.length; i++) {\n          if (children[i].contains(event.target)) return;\n        }\n\n        dropdownRef.current.classList.remove(\"labelDropdownActive\");\n      }\n    };\n\n    // Bind the event listener\n    document.addEventListener(\"click\", handleClickOutside);\n\n    return () => {\n      // Unbind the event listener on clean up\n      document.removeEventListener(\"click\", handleClickOutside);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (props.name === \"hideUI\") {\n      if (contentState.hideUIAlerts) {\n        setHideToolbarLabel(chrome.i18n.getMessage(\"hideUIAlerts\"));\n        setHideToolbarState(2);\n      } else if (contentState.hideToolbar) {\n        setHideToolbarLabel(chrome.i18n.getMessage(\"hideToolbarLabel\"));\n        setHideToolbarState(1);\n      } else if (contentState.toolbarHover) {\n        setHideToolbarLabel(chrome.i18n.getMessage(\"toolbarHoverOnly\"));\n        setHideToolbarState(3);\n      }\n    }\n  }, [contentState.hideToolbar]);\n\n  const dropdownRef = useRef(null);\n  const dropdownInRef = useRef(null);\n  return (\n    <form>\n      <div className=\"SwitchRow\" id={switchRowId}>\n        <label\n          className=\"Label\"\n          htmlFor={switchId}\n          style={{ paddingRight: 15 }}\n          onClick={(e) => {\n            if (props.name === \"hideUI\") {\n              e.preventDefault();\n              e.stopPropagation();\n              if (e.target.classList.contains(\"labelDropdownContentItem\"))\n                return;\n              dropdownRef.current.classList.toggle(\"labelDropdownActive\");\n            }\n          }}\n        >\n          {props.name !== \"hideUI\" && props.label}\n          {props.name === \"hideUI\" && (\n            <div className=\"labelDropdownWrap\" ref={dropdownRef}>\n              <div className=\"labelDropdown\" ref={dropdownInRef}>\n                {hideToolbarLabel}\n                <img src={DropdownIcon} />\n              </div>\n              <div className=\"labelDropdownContent\">\n                <div\n                  className=\"labelDropdownContentItem\"\n                  onClick={() => {\n                    setContentState((prevContentState) => ({\n                      ...prevContentState,\n                      hideToolbar: true,\n                      hideUIAlerts: false,\n                      toolbarHover: false,\n                    }));\n                    chrome.storage.local.set({\n                      hideToolbar: true,\n                      hideUIAlerts: false,\n                      toolbarHover: false,\n                    });\n                    setHideToolbarLabel(\n                      chrome.i18n.getMessage(\"hideToolbarLabel\")\n                    );\n                    dropdownRef.current.classList.remove(\"labelDropdownActive\");\n                    setHideToolbarState(1);\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"hideToolbarLabel\")}\n                </div>\n                <div\n                  className=\"labelDropdownContentItem\"\n                  onClick={() => {\n                    setContentState((prevContentState) => ({\n                      ...prevContentState,\n                      hideToolbar: false,\n                      hideUIAlerts: true,\n                      toolbarHover: false,\n                    }));\n                    chrome.storage.local.set({\n                      hideToolbar: false,\n                      hideUIAlerts: true,\n                      toolbarHover: false,\n                    });\n                    setHideToolbarLabel(chrome.i18n.getMessage(\"hideUIAlerts\"));\n                    dropdownRef.current.classList.remove(\"labelDropdownActive\");\n                    setHideToolbarState(2);\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"hideUIAlerts\")}\n                </div>\n                <div\n                  className=\"labelDropdownContentItem\"\n                  onClick={() => {\n                    setContentState((prevContentState) => ({\n                      ...prevContentState,\n                      hideToolbar: false,\n                      hideUIAlerts: false,\n                      toolbarHover: true,\n                    }));\n                    chrome.storage.local.set({\n                      hideToolbar: false,\n                      hideUIAlerts: false,\n                      toolbarHover: true,\n                    });\n                    setHideToolbarLabel(\n                      chrome.i18n.getMessage(\"toolbarHoverOnly\")\n                    );\n                    dropdownRef.current.classList.remove(\"labelDropdownActive\");\n                    setHideToolbarState(3);\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"toolbarHoverOnly\")}\n                </div>\n              </div>\n            </div>\n          )}\n          {props.experimental && (\n            <span className=\"ExperimentalLabel\">Experimental</span>\n          )}\n        </label>\n        {props.value ? (\n          <S.Root\n            className=\"SwitchRoot\"\n            id={switchId}\n            ref={switchRef}\n            checked={contentState[props.value]}\n            disabled={props.disabled}\n            onCheckedChange={(checked) => {\n              if (props.disabled) return;\n\n              setContentState((prevContentState) => ({\n                ...prevContentState,\n                [props.value]: checked,\n              }));\n              chrome.storage.local.set({ [props.value]: checked });\n\n              if (props.value === \"customRegion\") {\n                if (checked) {\n                  chrome.storage.local.set({\n                    region: true,\n                  });\n                }\n              }\n\n              if (props.name === \"hideUI\") {\n                if (hideToolbarState === 1) {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    hideToolbar: true,\n                    hideUIAlerts: false,\n                    toolbarHover: false,\n                  }));\n                  chrome.storage.local.set({\n                    hideToolbar: true,\n                    hideUIAlerts: false,\n                    toolbarHover: false,\n                  });\n                } else if (hideToolbarState === 2) {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    hideToolbar: false,\n                    hideUIAlerts: true,\n                    toolbarHover: false,\n                  }));\n                  chrome.storage.local.set({\n                    hideToolbar: false,\n                    hideUIAlerts: true,\n                    toolbarHover: false,\n                  });\n                } else if (hideToolbarState === 3) {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    hideToolbar: false,\n                    hideUIAlerts: false,\n                    toolbarHover: true,\n                  }));\n                  chrome.storage.local.set({\n                    hideToolbar: false,\n                    hideUIAlerts: false,\n                    toolbarHover: true,\n                  });\n                }\n              } else if (props.name === \"pushToTalk\") {\n                if (!checked) {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    micActive: true,\n                  }));\n                }\n              }\n\n              if (typeof props.onChange === \"function\") {\n                props.onChange(checked);\n              }\n            }}\n          >\n            <S.Thumb className=\"SwitchThumb\" />\n          </S.Root>\n        ) : (\n          <S.Root className=\"SwitchRoot\" id={switchId}>\n            <S.Thumb className=\"SwitchThumb\" />\n          </S.Root>\n        )}\n      </div>\n    </form>\n  );\n};\n\nexport default Switch;\n"
  },
  {
    "path": "src/pages/Content/popup/components/TimeSetter.jsx",
    "content": "import React, { useContext, useEffect, useState, useRef } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst TimeSetter = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [hours, setHours] = useState(Math.floor(contentState.alarmTime / 3600));\n  const [minutes, setMinutes] = useState(\n    Math.floor((contentState.alarmTime % 3600) / 60)\n  );\n  const [seconds, setSeconds] = useState(\n    Math.floor((contentState.alarmTime % 3600) % 60)\n  );\n\n  useEffect(() => {\n    // Get from contentState\n    setHours(Math.floor(contentState.alarmTime / 3600));\n    setMinutes(Math.floor((contentState.alarmTime % 3600) / 60));\n    setSeconds(Math.floor((contentState.alarmTime % 3600) % 60));\n  }, []);\n\n  useEffect(() => {\n    if (!contentState.fromAlarm) return;\n    // Set the time in seconds\n    setHours(Math.floor(contentState.alarmTime / 3600));\n    setMinutes(Math.floor((contentState.alarmTime % 3600) / 60));\n    setSeconds(Math.floor((contentState.alarmTime % 3600) % 60));\n  }, [contentState.alarmTime]);\n\n  useEffect(() => {\n    if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return;\n    if (hours === \"\" || minutes === \"\" || seconds === \"\") return;\n    setHours(parseFloat(hours));\n    setMinutes(parseFloat(minutes));\n    setSeconds(parseFloat(seconds));\n    // Set the time in seconds\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      alarmTime: hours * 3600 + minutes * 60 + seconds,\n      fromAlarm: false,\n      time: hours * 3600 + minutes * 60 + seconds,\n      timer: hours * 3600 + minutes * 60 + seconds,\n    }));\n    chrome.storage.local.set({\n      alarmTime: hours * 3600 + minutes * 60 + seconds,\n    });\n  }, [hours, minutes, seconds]);\n\n  useEffect(() => {\n    if (isNaN(hours) || isNaN(minutes) || isNaN(seconds)) return;\n    if (contentState.alarm) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        time: hours * 3600 + minutes * 60 + seconds,\n        timer: hours * 3600 + minutes * 60 + seconds,\n        fromAlarm: false,\n      }));\n    } else {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        time: 0,\n        timer: 0,\n      }));\n    }\n  }, [contentState.alarm]);\n\n  const handleHours = (e) => {\n    // Limit between 0 to 4, number only\n    // Only 1 digit\n    if (e.target.value.length > 1) {\n      if (e.target.value[0] === \"0\") {\n        e.target.value = parseFloat(e.target.value[1]);\n      } else {\n        return;\n      }\n    }\n    if (isNaN(e.target.value)) {\n      return;\n    }\n    if (e.target.value > 4) {\n      e.target.value = 4;\n    }\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      fromAlarm: true,\n    }));\n\n    setHours(e.target.value);\n  };\n\n  const handleMinutes = (e) => {\n    // Limit between 0 to 59, number only\n    if (isNaN(e.target.value)) {\n      return;\n    }\n    if (e.target.value > 59) {\n      e.target.value = 59;\n    }\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      fromAlarm: true,\n    }));\n\n    setMinutes(e.target.value);\n  };\n\n  const handleSeconds = (e) => {\n    // Limit between 0 to 59, number only\n    if (isNaN(e.target.value)) {\n      return;\n    }\n    if (e.target.value > 59) {\n      e.target.value = 59;\n    }\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      fromAlarm: true,\n    }));\n    setSeconds(e.target.value);\n  };\n\n  return (\n    <div className=\"time-set-parent\">\n      <div className=\"time-set-input\">\n        <input\n          placeholder=\"0\"\n          onChange={handleMinutes}\n          value={minutes}\n          onBlur={(e) => {\n            if (e.target.value === \"\") {\n              e.target.value = 0;\n              setMinutes(0);\n            }\n          }}\n          onFocus={(e) => {\n            e.target.select();\n          }}\n        />\n        <span>M</span>\n      </div>\n      <div className=\"time-set-input\">\n        <input\n          placeholder=\"0\"\n          onChange={handleSeconds}\n          value={seconds}\n          onBlur={(e) => {\n            if (e.target.value === \"\") {\n              e.target.value = 0;\n              setSeconds(0);\n            }\n          }}\n          onFocus={(e) => {\n            e.target.select();\n          }}\n        />\n        <span>S</span>\n      </div>\n    </div>\n  );\n};\n\nexport default TimeSetter;\n"
  },
  {
    "path": "src/pages/Content/popup/components/TooltipWrap.jsx",
    "content": "import React, { useEffect, useContext, useState } from \"react\";\n\nimport * as Tooltip from \"@radix-ui/react-tooltip\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst TooltipWrap = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const classname = props.name ? props.name : \"\";\n  const [override, setOverride] = useState(\"\");\n\n  useEffect(() => {\n    // Check if hideUI is set\n    if (contentState.hideUI) {\n      setOverride(\"override\");\n    } else {\n      setOverride(\"\");\n    }\n  }, [contentState.hideUI]);\n\n  return (\n    <div id={props.id} className={classname} style={props.style}>\n      {props.content == \"\" ? (\n        <div>{props.children}</div>\n      ) : (\n        <Tooltip.Provider>\n          <Tooltip.Root delayDuration={700} defaultOpen={false}>\n            <Tooltip.Trigger asChild>{props.children}</Tooltip.Trigger>\n            <Tooltip.Portal\n              container={\n                document.getElementsByClassName(\"screenity-shadow-dom\")[0]\n              }\n            >\n              <Tooltip.Content\n                className={\n                  \"TooltipContent\" +\n                  \" \" +\n                  props.override +\n                  \" \" +\n                  props.hide +\n                  \" \" +\n                  override\n                }\n                style={{\n                  display: override === \"override\" ? \"none\" : \"block\",\n                  whiteSpace: \"pre-line\",\n                  maxWidth: \"240px\",\n                  lineHeight: \"1.4\",\n                  ...props.tooltipStyle,\n                }}\n                side={props.side || \"left\"}\n                sideOffset={props.sideOffset || 8}\n              >\n                {props.content}\n              </Tooltip.Content>\n            </Tooltip.Portal>\n          </Tooltip.Root>\n        </Tooltip.Provider>\n      )}\n    </div>\n  );\n};\n\nexport default TooltipWrap;\n"
  },
  {
    "path": "src/pages/Content/popup/components/VideoItem.jsx",
    "content": "import React from \"react\";\n\nimport { CopyLinkIcon, MoreActionsIcon } from \"../../images/popup/images\";\n\nconst VideoItem = ({ title, date, thumbnail, onOpen, onCopyLink }) => {\n  const formatRelativeTime = (timestamp) => {\n    const now = new Date();\n    const then = new Date(timestamp);\n    const diffInSeconds = Math.floor((now - then) / 1000);\n\n    const thresholds = [\n      { unit: \"year\", seconds: 31536000 },\n      { unit: \"month\", seconds: 2592000 },\n      { unit: \"week\", seconds: 604800 },\n      { unit: \"day\", seconds: 86400 },\n      { unit: \"hour\", seconds: 3600 },\n      { unit: \"minute\", seconds: 60 },\n      { unit: \"second\", seconds: 1 },\n    ];\n\n    for (const { unit, seconds } of thresholds) {\n      const value = Math.floor(diffInSeconds / seconds);\n      if (value >= 1) {\n        return `${value} ${unit}${value !== 1 ? \"s\" : \"\"} ago`;\n      }\n    }\n\n    return \"just now\";\n  };\n\n  return (\n    <div\n      className=\"video-item-root\"\n      tabIndex=\"0\"\n      onClick={(e) => {\n        if (\n          e.target.closest(\".copy-link\") ||\n          e.target.closest(\".more-actions\")\n        ) {\n          e.stopPropagation();\n          return;\n        }\n        onOpen();\n      }}\n    >\n      <div className=\"video-item\">\n        <div className=\"video-item-left\">\n          {/*\n\t\t\t\t\tNeed a better way to handle thumbnails - proxy from server?\n\n\t\t\t\t\t<div\n            className=\"video-item-thumbnail\"\n            style={{\n              backgroundImage: `url(${thumbnail})`,\n              backgroundSize: \"cover\",\n              backgroundPosition: \"center\",\n            }}\n          ></div> */}\n          <div className=\"video-item-info\">\n            <div className=\"video-item-info-title\">{title}</div>\n            <div className=\"video-item-info-date\">\n              {formatRelativeTime(date)}\n            </div>\n          </div>\n        </div>\n        <div className=\"video-item-right\">\n          <button\n            role=\"button\"\n            tabIndex=\"0\"\n            className=\"copy-link\"\n            onClick={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n              onCopyLink();\n            }}\n          >\n            <img src={CopyLinkIcon} alt=\"Copy link\" />\n          </button>\n          {/* <button\n            role=\"button\"\n            tabIndex=\"0\"\n            title=\"More actions\"\n            className=\"more-actions\"\n            onClick={(e) => {\n              e.preventDefault();\n              e.stopPropagation();\n            }}\n          >\n            <img src={MoreActionsIcon} alt=\"More actions\" />\n          </button> */}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default VideoItem;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/Announcement.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\n\nconst Announcement = (props) => {\n  const [URL, setURL] = useState(\n    \"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what%E2%80%99s-changed-in-the-new-version-of-screenity/bDtvcwAtw9PPesQeNH4zjE\"\n  );\n\n  useEffect(() => {\n    // check i18n locale, and set URL accordingly w/ google translate\n    const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n    if (!locale.includes(\"en\")) {\n      setURL(\n        \"https://translate.google.com/translate?sl=en&tl=\" +\n          locale +\n          \"&u=\" +\n          URL\n      );\n    }\n  }, []);\n  return (\n    <div className=\"welcome\">\n      <div className=\"announcement-wrap\">\n        <div className=\"announcement-hero\">\n          <img src={chrome.runtime.getURL(\"assets/helper/hero.png\")} />\n        </div>\n        <div className=\"announcement-details\">\n          <div className=\"announcement-title\">\n            {chrome.i18n.getMessage(\"updateAnnouncementTitle\")} 👋\n          </div>\n          <div className=\"announcement-description\">\n            {chrome.i18n.getMessage(\"updateAnnouncementDescription\")}{\" \"}\n            <a href={URL} target=\"_blank\">\n              {chrome.i18n.getMessage(\"updateAnnouncementLearnMore\")}\n            </a>\n          </div>\n          <div\n            className=\"announcement-cta\"\n            onClick={() => {\n              props.setOnboarding(false);\n              chrome.storage.local.set({ updatingFromOld: false });\n            }}\n          >\n            {chrome.i18n.getMessage(\"updateAnnouncementButton\")}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Announcement;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/InactiveSubscription.jsx",
    "content": "import React from \"react\";\n\nconst formatDeletionDate = (isoDate) => {\n  try {\n    const date = new Date(isoDate);\n\n    // Get the extension locale (fallback to 'en')\n    const locale = chrome?.i18n?.getUILanguage?.() || \"en\";\n\n    // Use numeric-only format if not English\n    const useFallback = !locale.startsWith(\"en\");\n\n    return date.toLocaleDateString(undefined, {\n      year: \"numeric\",\n      month: useFallback ? \"2-digit\" : \"long\",\n      day: \"2-digit\",\n    });\n  } catch {\n    return \"unknown date\";\n  }\n};\n\nconst InactiveSubscription = ({\n  onManageClick,\n  onDowngradeClick,\n  subscription,\n  hasSubscribedBefore,\n}) => {\n  const deletionDate = subscription?.deletionAt || subscription?.endsAt;\n  const formattedDate = deletionDate ? formatDeletionDate(deletionDate) : null;\n\n  return (\n    <div\n      className=\"announcement\"\n      style={{ marginTop: \"50px\", paddingBottom: \"0px\" }}\n    >\n      <div className=\"announcement-wrap\">\n        <div className=\"announcement-details\">\n          <div className=\"welcome-title\" style={{ marginBottom: \"10px\" }}>\n            {hasSubscribedBefore\n              ? chrome.i18n.getMessage(\"inactiveSubscriptionTitle\") ||\n                \"Your Pro subscription is inactive\"\n              : chrome.i18n.getMessage(\"noSubscriptionYetTitle\") ||\n                \"Unlock Pro to get started\"}\n          </div>\n\n          <div\n            className=\"welcome-description\"\n            style={{\n              fontSize: \"14px\",\n              color: \"#6E7684\",\n              lineHeight: \"1.5\",\n              marginBottom: \"20px\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"inactiveSubscriptionDescription\") ||\n              \"Your Screenity Pro subscription is inactive.\"}\n            <br />\n            {hasSubscribedBefore\n              ? chrome.i18n.getMessage(\"inactiveSubscriptionReactivation\") ||\n                \"Please reactivate to resume access.\"\n              : chrome.i18n.getMessage(\"noSubscriptionYetDescription\") ||\n                \"Please subscribe to access Pro features.\"}\n          </div>\n\n          {formattedDate && hasSubscribedBefore && (\n            <div\n              style={{\n                backgroundColor: \"#FFF8FA\",\n                borderRadius: \"30px\",\n                padding: \"1.25rem 1.5rem\",\n                color: \"#F0175B\",\n                fontSize: \"14px\",\n                fontWeight: 500,\n                lineHeight: \"1.5\",\n                marginBottom: \"1.5rem\",\n              }}\n            >\n              {chrome.i18n.getMessage(\"inactiveSubscriptionDeletionWarning\") ||\n                \"Your videos and data will be permanently deleted on \"}\n              <strong\n                style={{\n                  fontFamily: \"Satoshi-Bold, sans-serif\",\n                }}\n              >\n                {formattedDate}\n              </strong>\n            </div>\n          )}\n\n          <div\n            className=\"welcome-cta\"\n            style={{\n              marginBottom: \"20px\",\n              backgroundColor: \"#29292F\",\n              boxSizing: \"border-box\",\n              color: \"white\",\n              height: \"45px\",\n              width: \"100%\",\n              borderRadius: \"999px\",\n              textAlign: \"center\",\n              fontWeight: \"600\",\n              cursor: \"pointer\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontSize: \"14px\",\n            }}\n            onClick={onManageClick}\n          >\n            {hasSubscribedBefore\n              ? chrome.i18n.getMessage(\"manageSubscriptionButton\") ||\n                \"Manage your subscription\"\n              : chrome.i18n.getMessage(\"upgradeToProButton\") ||\n                \"Upgrade to Pro\"}\n          </div>\n        </div>\n      </div>\n      <div className=\"welcome-content\">\n        <div className=\"welcome-content-wrap\">\n          <div\n            style={{\n              marginBottom: \"12px\",\n              fontSize: \"14px\",\n              color: \"#6E7684\",\n              textAlign: \"center\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"inactiveSubscriptionFreeVersion\") ||\n              \"Want to keep using the free version?\"}\n          </div>\n\n          <div\n            className=\"welcome-cta\"\n            style={{\n              backgroundColor: \"white\",\n              border: \"1px solid #E5E7EB\",\n              boxSizing: \"border-box\",\n              height: \"45px\",\n              width: \"100%\",\n              borderRadius: \"999px\",\n              textAlign: \"center\",\n              fontWeight: \"600\",\n              cursor: \"pointer\",\n              color: \"#141416\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontSize: \"14px\",\n            }}\n            onClick={onDowngradeClick}\n          >\n            {chrome.i18n.getMessage(\"downgradeToFreeButton\") ||\n              \"Log out and downgrade\"}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default InactiveSubscription;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/LoggedOut.jsx",
    "content": "import React from \"react\";\n\nconst LoggedOut = ({ onManageClick, onDowngradeClick }) => {\n  return (\n    <div\n      className=\"announcement\"\n      style={{ marginTop: \"50px\", paddingBottom: \"0px\" }}\n    >\n      <div className=\"announcement-wrap\">\n        <div className=\"announcement-details\">\n          <div className=\"welcome-title\" style={{ marginBottom: \"10px\" }}>\n            {chrome.i18n.getMessage(\"loggedOutTitle\") ||\n              \"You’ve been logged out\"}\n          </div>\n\n          <div\n            className=\"welcome-description\"\n            style={{\n              fontSize: \"14px\",\n              color: \"#6E7684\",\n              lineHeight: \"1.5\",\n              marginBottom: \"20px\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"loggedOutDescription\") ||\n              \"To keep your recordings synced to your account, and access premium features, you’ll need to log back in.\"}\n          </div>\n\n          <div\n            className=\"welcome-cta\"\n            style={{\n              marginBottom: \"20px\",\n              backgroundColor: \"#29292F\",\n              boxSizing: \"border-box\",\n              color: \"white\",\n              height: \"45px\",\n              width: \"100%\",\n              borderRadius: \"999px\",\n              textAlign: \"center\",\n              fontWeight: \"600\",\n              cursor: \"pointer\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontSize: \"14px\",\n            }}\n            onClick={onManageClick}\n          >\n            {chrome.i18n.getMessage(\"logBackInButton\") || \"Log back in\"}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"welcome-content\">\n        <div className=\"welcome-content-wrap\">\n          <div\n            style={{\n              marginBottom: \"12px\",\n              fontSize: \"14px\",\n              color: \"#6E7684\",\n              textAlign: \"center\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"loggedOutFreeVersion\") ||\n              \"You can keep using the extension without an account - but your recordings won’t be saved and can’t be recovered later.\"}\n          </div>\n\n          <div\n            className=\"welcome-cta\"\n            style={{\n              backgroundColor: \"white\",\n              border: \"1px solid #E5E7EB\",\n              boxSizing: \"border-box\",\n              height: \"45px\",\n              width: \"100%\",\n              borderRadius: \"999px\",\n              textAlign: \"center\",\n              fontWeight: \"600\",\n              cursor: \"pointer\",\n              color: \"#141416\",\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontSize: \"14px\",\n            }}\n            onClick={onDowngradeClick}\n          >\n            {chrome.i18n.getMessage(\"continueWithoutLogin\") ||\n              \"Continue without login\"}\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default LoggedOut;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/RecordingTab.jsx",
    "content": "import React, { useEffect, useState, useContext } from \"react\";\nimport * as Tabs from \"@radix-ui/react-tabs\";\n\nimport RecordingType from \"./RecordingType\";\nimport {\n  ScreenTabOn,\n  ScreenTabOff,\n  RegionTabOn,\n  RegionTabOff,\n  MockupTabOn,\n  MockupTabOff,\n  CameraTabIconOn,\n  CameraTabIconOff,\n  CheckWhiteIcon,\n  CloseWhiteIcon,\n} from \"../../images/popup/images\";\n\nimport { BaseSwitch } from \"../components/Switch\";\nimport TooltipWrap from \"../components/TooltipWrap\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst RecordingTab = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  const [tabRecordingDisabled, setTabRecordingDisabled] = useState(false);\n  const [showModalSoon, setShowModalSoon] = useState(false); // 👈 NEW\n\n  useEffect(() => {\n    if (tabRecordingDisabled && contentState.recordingType === \"region\") {\n      setContentState((prev) => ({\n        ...prev,\n        recordingType: \"screen\",\n      }));\n      chrome.storage.local.set({ recordingType: \"screen\" });\n\n      contentState.openToast?.(\n        chrome.i18n.getMessage(\"tabRecordingDisabledToast\"),\n        4000\n      );\n    }\n  }, [tabRecordingDisabled]);\n\n  useEffect(() => {\n    const currentUrl = window.location.href;\n    const isBlocked = currentUrl.includes(process.env.SCREENITY_APP_BASE);\n\n    setTabRecordingDisabled(isBlocked);\n\n    if (isBlocked && contentState.recordingType === \"region\") {\n      setContentState((prev) => ({\n        ...prev,\n        recordingType: \"screen\",\n      }));\n      chrome.storage.local.set({ recordingType: \"screen\" });\n\n      contentState.openToast?.(\n        chrome.i18n.getMessage(\"tabRecordingDisabledToast\"),\n        4000\n      );\n    }\n  }, [contentState.recordingType]);\n\n  const onValueChange = (tab) => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      recordingType: tab,\n    }));\n    chrome.storage.local.set({ recordingType: tab });\n\n    if (tab === \"camera\") {\n      chrome.runtime.sendMessage({ type: \"camera-only-update\" });\n    } else {\n      chrome.runtime.sendMessage({ type: \"screen-update\" });\n    }\n  };\n\n  useEffect(() => {\n    const onKey = (e) => {\n      if (e.key === \"Escape\") setShowModalSoon(false);\n    };\n    if (showModalSoon) window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [showModalSoon]);\n\n  return (\n    <div className=\"recording-ui\">\n      <Tabs.Root\n        className=\"TabsRoot\"\n        defaultValue=\"screen\"\n        onValueChange={onValueChange}\n        value={contentState.recordingType}\n      >\n        {contentState.recordingToScene && (\n          <div className=\"projectActiveBanner\">\n            <div className=\"projectActiveBannerLeft\">\n              {chrome.i18n.getMessage(\"addingToLabel\") || \"Adding to: \"}\n              {contentState.recordingProjectTitle}\n            </div>\n            {(!contentState.multiMode ||\n              contentState.multiSceneCount === 0) && (\n              <div className=\"projectActiveBannerRight\">\n                <div className=\"projectActiveBannerDivider\"></div>\n                <div\n                  className=\"projectActiveBannerClose\"\n                  onClick={() => {\n                    setContentState((prev) => ({\n                      ...prev,\n                      projectTitle: \"\",\n                      projectId: null,\n                      activeSceneId: null,\n                      recordingToScene: false,\n                      multiMode: false,\n                      multiSceneCount: 0,\n                      multiProjectId: null,\n                    }));\n\n                    // also in chrome local storage\n                    chrome.storage.local.set({\n                      recordingProjectTitle: \"\",\n                      projectId: null,\n                      activeSceneId: null,\n                      recordingToScene: false,\n                      multiMode: false,\n                      multiProjectId: null,\n                    });\n\n                    // show toast\n                    contentState.openToast(\n                      chrome.i18n.getMessage(\"projectRecordingCancelledToast\"),\n                      3000\n                    );\n                  }}\n                >\n                  <img src={CloseWhiteIcon} alt=\"Close\" />\n                </div>\n              </div>\n            )}\n          </div>\n        )}\n        <Tabs.List\n          className={\"TabsList\"}\n          aria-label=\"Manage your account\"\n          tabIndex={0}\n        >\n          <Tabs.Trigger className=\"TabsTrigger\" value=\"screen\" tabIndex={0}>\n            <div className=\"TabsTriggerLabel\">\n              <div className=\"TabsTriggerIcon\">\n                <img\n                  src={\n                    contentState.recordingType === \"screen\"\n                      ? ScreenTabOn\n                      : ScreenTabOff\n                  }\n                />\n              </div>\n              <span>{chrome.i18n.getMessage(\"screenType\")}</span>\n            </div>\n          </Tabs.Trigger>\n          <TooltipWrap\n            content={\n              tabRecordingDisabled\n                ? chrome.i18n.getMessage(\"tabRecordingDisabledTooltip\") ||\n                  \"Tab recording is disabled on this page.\"\n                : \"\"\n            }\n            side={\"bottom\"}\n          >\n            <Tabs.Trigger\n              className=\"TabsTrigger\"\n              value=\"region\"\n              tabIndex={0}\n              disabled={tabRecordingDisabled}\n              onClick={(e) => {\n                if (tabRecordingDisabled) {\n                  e.preventDefault();\n                  e.stopPropagation();\n                }\n              }}\n              style={\n                tabRecordingDisabled\n                  ? { cursor: \"not-allowed\", opacity: 0.5 }\n                  : {}\n              }\n            >\n              <div className=\"TabsTriggerLabel\">\n                <div className=\"TabsTriggerIcon\">\n                  <img\n                    src={\n                      contentState.recordingType === \"region\"\n                        ? RegionTabOn\n                        : RegionTabOff\n                    }\n                  />\n                </div>\n                <span>{chrome.i18n.getMessage(\"tabType\")}</span>\n              </div>\n            </Tabs.Trigger>\n          </TooltipWrap>\n          <Tabs.Trigger className=\"TabsTrigger\" value=\"camera\" tabIndex={0}>\n            <div className=\"TabsTriggerLabel\">\n              <div className=\"TabsTriggerIcon\">\n                <img\n                  src={\n                    contentState.recordingType === \"camera\"\n                      ? CameraTabIconOn\n                      : CameraTabIconOff\n                  }\n                />\n              </div>\n              <span>{chrome.i18n.getMessage(\"cameraType\")}</span>\n            </div>\n          </Tabs.Trigger>\n          <div className=\"TabsTriggerSpacer\"></div>\n          <div className=\"TabsTrigger\">\n            <TooltipWrap\n              content={\n                !contentState.isLoggedIn\n                  ? \"Record multiple scenes with Screenity Pro\"\n                  : \"Record scenes one after another\"\n              }\n              side={\"bottom\"}\n            >\n              <div\n                className=\"TabsTriggerLabel\"\n                style={{\n                  opacity: contentState.isLoggedIn ? 1 : 0.5,\n\n                  cursor: contentState.isLoggedIn ? \"pointer\" : \"not-allowed\",\n                }}\n                onClick={() => {\n                  // If not logged in, show the modal instead of toggling\n                  if (!contentState.isLoggedIn) {\n                    setShowModalSoon(true);\n                  }\n                }}\n              >\n                <div\n                  className=\"TabsTriggerIcon\"\n                  style={{\n                    width: \"33px\",\n                    position: \"relative\", // For the badge positioning\n                    pointerEvents: contentState.isLoggedIn ? \"auto\" : \"none\",\n                  }}\n                >\n                  {contentState.multiMode &&\n                  contentState.multiSceneCount > 0 ? (\n                    <div\n                      className=\"FinishButton\"\n                      onClick={() => {\n                        // Send finish message to background to finalize multi project\n                        chrome.runtime.sendMessage({\n                          type: \"finish-multi-recording\",\n                        });\n                        setContentState((prev) => ({\n                          ...prev,\n                          showExtension: false,\n                          hasOpenedBefore: true,\n                          showPopup: false,\n                        }));\n                      }}\n                      style={{\n                        cursor: \"pointer\",\n                        width: \"28px\",\n                        height: \"28px\",\n                        background: \"#3080F8\",\n                        borderRadius: \"50%\",\n                        display: \"flex\",\n                        alignItems: \"center\",\n                        justifyContent: \"center\",\n                      }}\n                    >\n                      <img\n                        src={CheckWhiteIcon}\n                        alt=\"Finish\"\n                        style={{ width: \"13px\", height: \"13px\" }}\n                      />\n                      <div\n                        style={{\n                          position: \"absolute\",\n                          top: \"-7px\",\n                          left: \"-7px\",\n                          background: \"#78C072\",\n                          color: \"white\",\n                          fontSize: \"12px\",\n                          fontWeight: \"bold\",\n                          borderRadius: \"50%\",\n                          width: \"18px\",\n                          height: \"18px\",\n                          display: \"flex\",\n                          alignItems: \"center\",\n                          justifyContent: \"center\",\n                        }}\n                      >\n                        {contentState.multiSceneCount}\n                      </div>\n                    </div>\n                  ) : (\n                    <BaseSwitch\n                      label={\"Multi recording\"}\n                      name=\"multiRecording\"\n                      value=\"multiMode\"\n                      checked={contentState.multiMode}\n                      onChange={(checked) => {\n                        setContentState((prevContentState) => ({\n                          ...prevContentState,\n                          multiMode: checked,\n                        }));\n                        chrome.storage.local.set({ multiMode: checked });\n\n                        if (checked) {\n                          chrome.storage.local\n                            .get([\"hasSeenMultiRecordingInfo\"])\n                            .then((res) => {\n                              if (!res.hasSeenMultiRecordingInfo) {\n                                contentState.openModal(\n                                  chrome.i18n.getMessage(\n                                    \"multiRecordingModeTitle\"\n                                  ) || \"Multi-recording mode\",\n                                  chrome.i18n.getMessage(\n                                    \"multiRecordingModeDescription\"\n                                  ) ||\n                                    \"Record multiple scenes, like your screen, camera, or both, one after another. This is great for doing multiple takes, switching views, or breaking your recording into parts. When you’re done, click Finish to open the editor with all your scenes combined in one project.\",\n                                  \"Got it\",\n                                  chrome.i18n.getMessage(\n                                    \"permissionsModalDismiss\"\n                                  ) || \"Dismiss\",\n                                  () => {},\n                                  () => {},\n                                  null,\n                                  \"\",\n                                  \"\",\n                                  true,\n                                  false\n                                );\n\n                                // Mark as seen\n                                chrome.storage.local.set({\n                                  hasSeenMultiRecordingInfo: true,\n                                });\n                              }\n                            });\n                          chrome.storage.local.set({ instantMode: false });\n                          setContentState((prevContentState) => ({\n                            ...prevContentState,\n                            instantMode: false,\n                          }));\n                        }\n                      }}\n                    />\n                  )}\n                </div>\n                <span>\n                  {contentState.multiMode && contentState.multiSceneCount > 0\n                    ? chrome.i18n.getMessage(\"finishLabelMulti\") || \"Finish\"\n                    : chrome.i18n.getMessage(\"multiLabel\") || \"Multi\"}\n                </span>\n              </div>\n            </TooltipWrap>\n          </div>\n          {/* <Tabs.Trigger\n            className=\"TabsTrigger\"\n            value=\"mockup\"\n            tabIndex={0}\n            disabled\n            style={{ pointerEvents: \"none\", opacity: 0.5 }}\n          >\n            <div className=\"TabsTriggerLabel\">\n              <div className=\"TabsTriggerIcon\">\n                <img\n                  src={\n                    contentState.recordingType === \"mockup\"\n                      ? MockupTabOn\n                      : MockupTabOff\n                  }\n                />\n              </div>\n              <span>{chrome.i18n.getMessage(\"MockupType\")}</span>\n            </div>\n          </Tabs.Trigger> */}\n        </Tabs.List>\n\n        {showModalSoon && (\n          <div\n            className=\"ModalSoon strong\"\n            style={{\n              zIndex: 999999999999,\n            }}\n          >\n            <button\n              aria-label=\"Close\"\n              onClick={() => setShowModalSoon(false)}\n              style={{\n                position: \"absolute\",\n                top: -10,\n                right: -10,\n                width: 32,\n                height: 32,\n                borderRadius: \"50%\",\n                background: \"rgb(252 252 252)\",\n                border: \"1px solid #E2E8F0\",\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                boxShadow: \"0 2px 8px rgba(0,0,0,0.12)\",\n                cursor: \"pointer\",\n              }}\n            >\n              <img\n                src={CloseWhiteIcon}\n                alt=\"\"\n                style={{ width: 14, height: 14, filter: \"invert(0.4)\" }}\n              />\n            </button>\n            {/* 👇 Embed the video here */}\n            <video\n              src={chrome.runtime.getURL(\"assets/videos/pro.mp4\")}\n              autoPlay\n              loop\n              muted\n              playsInline\n              style={{\n                width: \"100%\",\n                borderRadius: \"6px\",\n                marginBottom: \"20px\",\n              }}\n            />\n            <div className=\"ModalSoonTitle\">\n              {chrome.i18n.getMessage(\"shareModalSandboxTitle\")}\n            </div>\n\n            <div className=\"ModalSoonDescription\">\n              {chrome.i18n.getMessage(\"shareModalSandboxDescription\")}\n            </div>\n\n            <div\n              className=\"ModalSoonButton\"\n              onClick={() => {\n                chrome.runtime.sendMessage({ type: \"pricing\" });\n              }}\n            >\n              {chrome.i18n.getMessage(\"shareModalSandboxButton\")}\n            </div>\n\n            <button\n              onClick={() => {\n                chrome.runtime.sendMessage({ type: \"handle-login\" });\n              }}\n              className=\"ModalSoonSecondary\"\n              style={{\n                marginTop: 16,\n                width: \"100%\",\n                background: \"transparent\",\n                border: \"none\",\n                color: \"#6B7280\",\n                fontSize: 13,\n                textAlign: \"center\",\n                cursor: \"pointer\",\n              }}\n            >\n              {chrome.i18n.getMessage(\"shareModalSandboxLogin\")}\n            </button>\n          </div>\n        )}\n        <Tabs.Content className=\"TabsContent\" value=\"screen\">\n          <RecordingType shadowRef={props.shadowRef} />\n        </Tabs.Content>\n        <Tabs.Content className=\"TabsContent\" value=\"region\">\n          <RecordingType shadowRef={props.shadowRef} />\n        </Tabs.Content>\n        <Tabs.Content className=\"TabsContent\" value=\"camera\">\n          <RecordingType shadowRef={props.shadowRef} />\n        </Tabs.Content>\n      </Tabs.Root>\n    </div>\n  );\n};\n\nexport default RecordingTab;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/RecordingType.jsx",
    "content": "import React, { useEffect, useContext, useState, useRef } from \"react\";\n\nimport Dropdown from \"../components/Dropdown\";\nimport Switch from \"../components/Switch\";\nimport RegionDimensions from \"../components/RegionDimensions\";\nimport Settings from \"./Settings\";\nimport { contentStateContext } from \"../../context/ContentState\";\nimport { CameraOffBlue, MicOffBlue } from \"../../images/popup/images\";\nimport TooltipWrap from \"../components/TooltipWrap\";\n\nimport BackgroundEffects from \"../components/BackgroundEffects\";\n\nimport { AlertIcon, TimeIcon, NoInternet } from \"../../toolbar/components/SVG\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nconst RecordingType = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [cropActive, setCropActive] = useState(false);\n  const [time, setTime] = useState(0);\n  const [URL, setURL] = useState(\n    \"https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-the-technical-requirements-for-using-screenity/6kdB6qru6naVD8ZLFvX3m9\"\n  );\n  const [URL2, setURL2] = useState(\n    \"https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/how-to-grant-screenity-permission-to-record-your-camera-and-microphone/x6U69TnrbMjy5CQ96Er2E9\"\n  );\n\n  const buttonRef = useRef(null);\n  const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n\n  useEffect(() => {\n    const locale = chrome.i18n.getMessage(\"@@ui_locale\");\n    if (!locale.includes(\"en\")) {\n      setURL(\n        `https://translate.google.com/translate?sl=en&tl=${locale}&u=https://help.screenity.io/getting-started/77KizPC8MHVGfpKpqdux9D/what-are-the-technical-requirements-for-using-screenity/6kdB6qru6naVD8ZLFvX3m9`\n      );\n      setURL2(\n        `https://translate.google.com/translate?sl=en&tl=${locale}&u=https://help.screenity.io/troubleshooting/9Jy5RGjNrBB42hqUdREQ7W/how-to-grant-screenity-permission-to-record-your-camera-and-microphone/x6U69TnrbMjy5CQ96Er2E9`\n      );\n    }\n  }, []);\n\n  useEffect(() => {\n    // Convert seconds to mm:ss\n    let minutes = Math.floor(contentState.alarmTime / 60);\n    let seconds = contentState.alarmTime - minutes * 60;\n    if (seconds < 10) {\n      seconds = \"0\" + seconds;\n    }\n    setTime(minutes + \":\" + seconds);\n  }, []);\n\n  useEffect(() => {\n    // Convert seconds to mm:ss\n    let minutes = Math.floor(contentState.alarmTime / 60);\n    let seconds = contentState.alarmTime - minutes * 60;\n    if (seconds < 10) {\n      seconds = \"0\" + seconds;\n    }\n    setTime(minutes + \":\" + seconds);\n  }, [contentState.alarmTime]);\n\n  // Start recording\n  const startStreaming = () => {\n    contentState.startStreaming();\n  };\n\n  useEffect(() => {\n    // Check if CropTarget is null\n    if (typeof CropTarget === \"undefined\") {\n      setCropActive(false);\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        customRegion: false,\n      }));\n    } else {\n      setCropActive(true);\n    }\n  }, []);\n\n  useEffect(() => {\n    if (contentState.recording) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        pendingRecording: false,\n      }));\n    }\n  }, [contentState.recording]);\n\n  return (\n    <div>\n      {contentState.updateChrome && (\n        <div className=\"popup-warning\">\n          <div className=\"popup-warning-left\">\n            <AlertIcon />\n          </div>\n          <div className=\"popup-warning-middle\">\n            <div className=\"popup-warning-title\">\n              {chrome.i18n.getMessage(\"customAreaRecordingDisabledTitle\")}\n            </div>\n            <div className=\"popup-warning-description\">\n              {chrome.i18n.getMessage(\"customAreaRecordingDisabledDescription\")}\n            </div>\n          </div>\n          <div className=\"popup-warning-right\">\n            <a href={URL} target=\"_blank\">\n              {chrome.i18n.getMessage(\"customAreaRecordingDisabledAction\")}\n            </a>\n          </div>\n        </div>\n      )}\n      {/*contentState.offline && (\n        <div className=\"popup-warning\">\n          <div className=\"popup-warning-left\">\n            <NoInternet />\n          </div>\n          <div className=\"popup-warning-middle\">\n            <div className=\"popup-warning-title\">You are currently offline</div>\n            <div className=\"popup-warning-description\">\n              Some features are unavailable\n            </div>\n          </div>\n          <div className=\"popup-warning-right\">\n            <a href=\"#\">Try again</a>\n          </div>\n        </div>\n\t\t\t)*/}\n      {!cropActive &&\n        contentState.recordingType === \"region\" &&\n        !contentState.offline && (\n          <div className=\"popup-warning\">\n            <div className=\"popup-warning-left\">\n              <AlertIcon />\n            </div>\n            <div className=\"popup-warning-middle\">\n              <div className=\"popup-warning-title\">\n                {chrome.i18n.getMessage(\"customAreaRecordingDisabledTitle\")}\n              </div>\n              <div className=\"popup-warning-description\">\n                {chrome.i18n.getMessage(\n                  \"customAreaRecordingDisabledDescription\"\n                )}\n              </div>\n            </div>\n            <div className=\"popup-warning-right\">\n              <a\n                href=\"https://support.google.com/chrome/answer/95414?hl=en-GB&co=GENIE.Platform%3DDesktop\"\n                target=\"_blank\"\n              >\n                {chrome.i18n.getMessage(\"customAreaRecordingDisabledAction\")}\n              </a>\n            </div>\n          </div>\n        )}\n      {!contentState.cameraPermission && (\n        <button\n          className=\"permission-button\"\n          onClick={() => {\n            if (typeof contentState.openModal === \"function\") {\n              contentState.openModal(\n                chrome.i18n.getMessage(\"permissionsModalTitle\"),\n                chrome.i18n.getMessage(\"permissionsModalDescription\"),\n                chrome.i18n.getMessage(\"permissionsModalReview\"),\n                chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n                () => {\n                  chrome.runtime.sendMessage({\n                    type: \"extension-media-permissions\",\n                  });\n                },\n                () => {},\n                chrome.runtime.getURL(\"assets/helper/permissions.webp\"),\n                chrome.i18n.getMessage(\"learnMoreDot\"),\n                URL2,\n                true,\n                false\n              );\n            }\n          }}\n        >\n          <img src={CameraOffBlue} />\n          <span>{chrome.i18n.getMessage(\"allowCameraAccessButton\")}</span>\n        </button>\n      )}\n      {contentState.cameraPermission && (\n        <Dropdown type=\"camera\" shadowRef={props.shadowRef} />\n      )}\n      {contentState.cameraPermission &&\n        contentState.defaultVideoInput != \"none\" &&\n        contentState.cameraActive && (\n          <div>\n            <Switch\n              label={chrome.i18n.getMessage(\"flipCameraLabel\")}\n              name=\"flip-camera\"\n              value=\"cameraFlipped\"\n            />\n            {(!contentState.isLoggedIn || contentState.instantMode) && (\n              <div style={{ pointerEvents: \"auto\" }}>\n                <Switch\n                  label={chrome.i18n.getMessage(\"backgroundEffectsLabel\")}\n                  name=\"background-effects-active\"\n                  value=\"backgroundEffectsActive\"\n                />\n              </div>\n            )}\n\n            {contentState.backgroundEffectsActive &&\n              (!contentState.isLoggedIn || contentState.instantMode) && (\n                <BackgroundEffects />\n              )}\n          </div>\n        )}\n\n      {!contentState.microphonePermission && (\n        <button\n          className=\"permission-button\"\n          onClick={() => {\n            if (typeof contentState.openModal === \"function\") {\n              contentState.openModal(\n                chrome.i18n.getMessage(\"permissionsModalTitle\"),\n                chrome.i18n.getMessage(\"permissionsModalDescription\"),\n                chrome.i18n.getMessage(\"permissionsModalReview\"),\n                chrome.i18n.getMessage(\"permissionsModalDismiss\"),\n                () => {\n                  chrome.runtime.sendMessage({\n                    type: \"extension-media-permissions\",\n                  });\n                },\n                () => {},\n                chrome.runtime.getURL(\"assets/helper/permissions.webp\"),\n                chrome.i18n.getMessage(\"learnMoreDot\"),\n                URL2,\n                true,\n                false\n              );\n            }\n          }}\n        >\n          <img src={MicOffBlue} />\n          <span>{chrome.i18n.getMessage(\"allowMicrophoneAccessButton\")}</span>\n        </button>\n      )}\n      {contentState.microphonePermission && (\n        <Dropdown type=\"mic\" shadowRef={props.shadowRef} />\n      )}\n      {((!contentState.isLoggedIn &&\n        contentState.microphonePermission &&\n        contentState.defaultAudioInput != \"none\" &&\n        contentState.micActive) ||\n        (contentState.microphonePermission && contentState.pushToTalk)) && (\n        <div>\n          <iframe\n            style={{\n              width: \"100%\",\n              height: \"30px\",\n              zIndex: 999999,\n              position: \"relative\",\n            }}\n            allow=\"camera; microphone\"\n            src={chrome.runtime.getURL(\"waveform.html\")}\n          ></iframe>\n          <Switch\n            label={\n              isMac\n                ? chrome.i18n.getMessage(\"pushToTalkLabel\") + \" (⌥⇧U)\"\n                : chrome.i18n.getMessage(\"pushToTalkLabel\") + \" (Alt⇧U)\"\n            }\n            name=\"pushToTalk\"\n            value=\"pushToTalk\"\n          />\n        </div>\n      )}\n      {contentState.recordingType === \"region\" && cropActive && (\n        <div>\n          <div className=\"popup-content-divider\"></div>\n          <Switch\n            label={chrome.i18n.getMessage(\"customAreaLabel\")}\n            name=\"customRegion\"\n            value=\"customRegion\"\n          />\n          {contentState.customRegion && <RegionDimensions />}\n        </div>\n      )}\n      {contentState.isLoggedIn &&\n        !contentState.recordingToScene &&\n        CLOUD_FEATURES_ENABLED && (\n          <>\n            <div className=\"popup-content-divider\"></div>\n            <TooltipWrap\n              id=\"pro-onboarding-instant-mode-field\"\n              content={\n                chrome.i18n.getMessage(\"instantRecordingModeTooltip\") ||\n                \"Instant download, but camera and layout won’t be editable later.\"\n              }\n              side=\"bottom\"\n              sideOffset={2}\n            >\n              <div style={{ pointerEvents: \"auto\" }}>\n                <Switch\n                  label={\n                    chrome.i18n.getMessage(\"instantRecordingModeLabel\") ||\n                    \"Instant recording mode\"\n                  }\n                  name=\"instantMode\"\n                  value=\"instantMode\"\n                  anchorId=\"pro-onboarding-instant-mode-toggle\"\n                  rowAnchorId=\"pro-onboarding-instant-mode-toggle-row\"\n                  onChange={async (checked) => {\n                    if (checked) {\n                      contentState.openModal(\n                        chrome.i18n.getMessage(\"instantRecordingModeTitle\") ||\n                          \"Instant recording mode\",\n                        chrome.i18n.getMessage(\n                          \"instantRecordingModeDescription\"\n                        ) ||\n                          \"This records everything into one video for instant download and sharing. You won’t be able to change the camera layout afterward, but other edits are still possible.\",\n                        chrome.i18n.getMessage(\"instantRecordingModeAction\") ||\n                          \"Got it\",\n                        chrome.i18n.getMessage(\"permissionsModalDismiss\") ||\n                          \"Dismiss\",\n                        () => {},\n                        () => {},\n                        null,\n                        \"\",\n                        \"\",\n                        true,\n                        false\n                      );\n                    } else {\n                      // Turn off background effects in chrome.storage\n                      chrome.storage.local.set({\n                        backgroundEffectsActive: false,\n                      });\n\n                      // Update in memory\n                      setContentState((prev) => ({\n                        ...prev,\n                        backgroundEffectsActive: false,\n                      }));\n                    }\n                  }}\n                />\n              </div>\n            </TooltipWrap>\n          </>\n        )}\n      <button\n        role=\"button\"\n        className=\"main-button recording-button\"\n        ref={buttonRef}\n        tabIndex=\"0\"\n        onClick={startStreaming}\n        disabled={\n          contentState.pendingRecording ||\n          ((!contentState.cameraPermission || !contentState.cameraActive) &&\n            contentState.recordingType === \"camera\")\n        }\n      >\n        {contentState.alarm && contentState.alarmTime > 0 && (\n          <div className=\"alarm-time-button\">\n            <TimeIcon />\n            {time}\n          </div>\n        )}\n        <span className=\"main-button-label\">\n          {contentState.pendingRecording\n            ? chrome.i18n.getMessage(\"recordButtonInProgressLabel\")\n            : (!contentState.cameraPermission || !contentState.cameraActive) &&\n              contentState.recordingType === \"camera\"\n            ? chrome.i18n.getMessage(\"recordButtonNoCameraLabel\")\n            : contentState.multiMode && contentState.multiSceneCount > 0\n            ? chrome.i18n.getMessage(\"recordButtonMultiLabel\")\n            : chrome.i18n.getMessage(\"recordButtonLabel\")}\n        </span>\n        <span className=\"main-button-shortcut\">\n          {contentState.recordingShortcut}\n        </span>\n      </button>\n      <Settings />\n    </div>\n  );\n};\n\nexport default RecordingType;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/Settings.jsx",
    "content": "import React, { useState, useContext, useEffect } from \"react\";\nimport * as Collapsible from \"@radix-ui/react-collapsible\";\nimport { DropdownIcon } from \"../../images/popup/images\";\n\n// Components\nimport Switch from \"../components/Switch\";\nimport TimeSetter from \"../components/TimeSetter\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst Settings = () => {\n  const [open, setOpen] = useState(false);\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [chromeVersion, setChromeVersion] = useState(null);\n  // Check if Mac\n  const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n  // Set shortcut to Option+Shift+E on Mac and Alt+Shift+E on Windows, using character codes\n  const shortcut = isMac ? \"⌥⇧E\" : \"Alt⇧E\";\n\n  // Get Chrome version\n  const getChromeVersion = () => {\n    var raw = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n    return raw ? parseInt(raw[2], 10) : false;\n  };\n\n  useEffect(() => {\n    setChromeVersion(getChromeVersion());\n  }, []);\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      settingsOpen: open,\n    }));\n  }, [open]);\n\n  return (\n    <Collapsible.Root\n      className=\"CollapsibleRoot\"\n      open={open}\n      onOpenChange={setOpen}\n    >\n      <Collapsible.Trigger className=\"CollapsibleTrigger\">\n        <div className=\"CollapsibleLabel\">\n          ✨ {chrome.i18n.getMessage(\"showMoreOptionsLabel\")}{\" \"}\n          <img src={DropdownIcon} />\n        </div>\n      </Collapsible.Trigger>\n      <Collapsible.Content>\n        <Switch\n          label={chrome.i18n.getMessage(\"hideToolbarLabel\")}\n          name=\"hideUI\"\n          value=\"hideUI\"\n          anchorId=\"pro-onboarding-toolbar-toggle\"\n        />\n        <Switch\n          label={chrome.i18n.getMessage(\"countdownLabel\")}\n          name=\"countdown\"\n          value=\"countdown\"\n        />\n        <Switch\n          label={chrome.i18n.getMessage(\"alarmLabel\")}\n          name=\"alarm\"\n          value=\"alarm\"\n        />\n        {contentState.alarm && <TimeSetter />}\n        <Switch\n          label={chrome.i18n.getMessage(\"micReminderPopup\")}\n          name=\"askMicrophone\"\n          value=\"askMicrophone\"\n        />\n        {contentState.recordingType != \"region\" &&\n          contentState.recordingType != \"camera\" &&\n          !contentState.isSubscribed &&\n          (chromeVersion === null || chromeVersion >= 109) && (\n            <Switch\n              label={chrome.i18n.getMessage(\"stayInPagePopup\")}\n              name=\"offscreenRecording\"\n              value=\"offscreenRecording\"\n            />\n          )}\n        {contentState.recordingType != \"camera\" &&\n          !contentState.isSubscribed && (\n            <Switch\n              label={\n                chrome.i18n.getMessage(\"zoomToPointPopup\") +\n                \" (\" +\n                shortcut +\n                \")\"\n              }\n              name=\"zoomEnabled\"\n              value=\"zoomEnabled\"\n              experimental={true}\n            />\n          )}\n      </Collapsible.Content>\n    </Collapsible.Root>\n  );\n};\n\nexport default Settings;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/SettingsMenu.jsx",
    "content": "// Work in progress - settings for the recording\n\nimport React, { useState, useContext, useRef, useEffect } from \"react\";\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\n\nimport { MoreIconPopup } from \"../../toolbar/components/SVG\";\n\nimport TooltipWrap from \"../components/TooltipWrap\";\n\nimport { CheckWhiteIcon, DropdownGroup } from \"../../images/popup/images\";\n\nimport { buildDiagnosticZip } from \"../../../utils/buildDiagnosticZip\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\nimport {\n  probeFastRecorderSupport,\n  shouldUseFastRecorder,\n  getFastRecorderStickyState,\n} from \"../../../../media/fastRecorderGate\";\nimport { resetOnboardingSeen } from \"../onboarding/storage\";\nimport { runProPopupOnboardingIfNeeded } from \"../onboarding/proOnboarding\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nconst SettingsMenu = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [restore, setRestore] = useState(false);\n  const [cloudRestore, setCloudRestore] = useState(false);\n  const [oldChrome, setOldChrome] = useState(false);\n  const [openQuality, setOpenQuality] = useState(false);\n  const [openResize, setOpenResize] = useState(false);\n  const [openFPS, setOpenFPS] = useState(false);\n  const [RAM, setRAM] = useState(0);\n  const [width, setWidth] = useState(0);\n  const [height, setHeight] = useState(0);\n  const [fastRecorderInfo, setFastRecorderInfo] = useState({\n    status: null,\n    probe: null,\n    decision: null,\n    disabled: false,\n    disabledReason: null,\n    disabledDetails: null,\n    disabledAt: null,\n  });\n\n  useEffect(() => {\n    // Check chrome version\n    const chromeVersion = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n    const MIN_CHROME_VERSION = 110;\n\n    if (chromeVersion && parseInt(chromeVersion[2], 10) < MIN_CHROME_VERSION) {\n      setOldChrome(true);\n    }\n  }, []);\n\n  const handleTroubleshooting = () => {\n    if (typeof contentState.openModal === \"function\") {\n      contentState.openModal(\n        chrome.i18n.getMessage(\"troubleshootModalTitle\"),\n        chrome.i18n.getMessage(\"troubleshootModalDescription\"),\n        chrome.i18n.getMessage(\"troubleshootModalButton\"),\n        chrome.i18n.getMessage(\"sandboxEditorCancelButton\"),\n        async () => {\n          try {\n            const { blob, filename } = await buildDiagnosticZip({\n              source: \"popup-settings\",\n              extraConfig: {\n                defaultAudioInput: contentState.defaultAudioInput,\n                defaultAudioOutput: contentState.defaultAudioOutput,\n                defaultVideoInput: contentState.defaultVideoInput,\n                quality: contentState.quality,\n                systemAudio: contentState.systemAudio,\n                audioInput: contentState.audioInput,\n                audioOutput: contentState.audioOutput,\n                backgroundEffectsActive: contentState.backgroundEffectsActive,\n                recording: contentState.recording,\n                recordingType: contentState.recordingType,\n                askForPermissions: contentState.askForPermissions,\n                cameraPermission: contentState.cameraPermission,\n                microphonePermission: contentState.microphonePermission,\n                askMicrophone: contentState.askMicrophone,\n                cursorMode: contentState.cursorMode,\n                zoomEnabled: contentState.zoomEnabled,\n                offscreenRecording: contentState.offscreenRecording,\n                updateChrome: contentState.updateChrome,\n                permissionsChecked: contentState.permissionsChecked,\n                permissionsLoaded: contentState.permissionsLoaded,\n                hideUI: contentState.hideUI,\n                alarm: contentState.alarm,\n                alarmTime: contentState.alarmTime,\n                surface: contentState.surface,\n                blurMode: contentState.blurMode,\n              },\n            });\n            const url = window.URL.createObjectURL(blob);\n            const a = document.createElement(\"a\");\n            a.href = url;\n            a.download = filename;\n            a.click();\n            window.URL.revokeObjectURL(url);\n          } catch (err) {\n            console.error(\"[Screenity] Troubleshooting export failed:\", err);\n          }\n        },\n        () => {},\n      );\n    }\n  };\n\n  useEffect(() => {\n    // More accurate screen detection\n    const w = window.screen.availWidth * window.devicePixelRatio;\n    const h = window.screen.availHeight * window.devicePixelRatio;\n    setWidth(Math.round(w));\n    setHeight(Math.round(h));\n\n    // Fix RAM detection on macOS\n    const isMac = navigator.userAgent.includes(\"Macintosh\");\n    const ram = isMac ? 32 : Number(navigator.deviceMemory) || 4;\n    setRAM(ram);\n  }, []);\n\n  const getFastRecDebug = () => {\n    try {\n      const params = new URLSearchParams(window.location.search);\n      return params.get(\"fastRecDebug\") === \"1\";\n    } catch {\n      return false;\n    }\n  };\n\n  const hasLadderFields = (probe) => {\n    if (!probe || !probe.details) return false;\n    return (\n      Array.isArray(probe.details.attemptSummary) &&\n      probe.details.attemptSummary.length > 0 &&\n      Boolean(probe.details.selectedVideoConfig)\n    );\n  };\n\n  const runFastRecorderProbe = async (source = \"auto\") => {\n    if (!contentState) return;\n    const userSetting =\n      contentState.useWebCodecsRecorder === true\n        ? true\n        : contentState.useWebCodecsRecorder === false\n        ? false\n        : null;\n    const sticky = await getFastRecorderStickyState();\n    const probe = await probeFastRecorderSupport();\n    const useFast = shouldUseFastRecorder(userSetting, probe, sticky);\n    const decision = {\n      useFast,\n      why:\n        userSetting === false\n          ? \"user_disabled\"\n          : sticky?.disabled && userSetting !== true\n          ? \"sticky_disabled\"\n          : probe.ok\n          ? \"probe_ok\"\n          : \"probe_failed\",\n      at: Date.now(),\n    };\n    const status = {\n      userSetting,\n      probe: { ...probe, at: probe.at || Date.now() },\n        decision,\n        disabled: Boolean(sticky?.disabled),\n        disabledReason: sticky?.reason || null,\n        disabledDetails: sticky?.details || null,\n        disabledAt: null,\n        updatedAt: Date.now(),\n      };\n    try {\n      await chrome.storage.local.set({ fastRecorderStatus: status });\n    } catch {}\n    setFastRecorderInfo({\n      status,\n      probe: status.probe,\n      decision,\n      disabled: status.disabled,\n      disabledReason: status.disabledReason,\n      disabledDetails: status.disabledDetails,\n      disabledAt: status.disabledAt,\n    });\n    if (getFastRecDebug()) {\n      console.log(\"[FastRecorderStatus]\", { source, status });\n    }\n    return status;\n  };\n\n  const migrateFastRecorderStatus = async () => {\n    const existing = await chrome.storage.local.get([\"fastRecorderStatus\"]);\n    if (existing.fastRecorderStatus) {\n      const status = existing.fastRecorderStatus;\n      if (!hasLadderFields(status?.probe)) {\n        await runFastRecorderProbe(\"stale-status\");\n        const refreshed = await chrome.storage.local.get([\"fastRecorderStatus\"]);\n        return refreshed.fastRecorderStatus || status;\n      }\n      return status;\n    }\n    const legacy = await chrome.storage.local.get([\n      \"fastRecorderBeta\",\n      \"fastRecorderProbe\",\n      \"fastRecorderDecision\",\n      \"fastRecorderDisabledForDevice\",\n      \"fastRecorderDisabledReason\",\n      \"fastRecorderDisabledDetails\",\n      \"fastRecorderDisabledAt\",\n    ]);\n    const status = {\n      userSetting:\n        legacy.fastRecorderBeta === true\n          ? true\n          : legacy.fastRecorderBeta === false\n          ? false\n          : null,\n      probe: legacy.fastRecorderProbe || null,\n      decision: legacy.fastRecorderDecision || null,\n      disabled: Boolean(legacy.fastRecorderDisabledForDevice),\n      disabledReason: legacy.fastRecorderDisabledReason || null,\n      disabledDetails: legacy.fastRecorderDisabledDetails || null,\n      disabledAt: legacy.fastRecorderDisabledAt || null,\n      updatedAt: Date.now(),\n    };\n    try {\n      await chrome.storage.local.set({ fastRecorderStatus: status });\n    } catch {}\n    if (!hasLadderFields(status.probe)) {\n      await runFastRecorderProbe(\"stale-legacy\");\n      const refreshed = await chrome.storage.local.get([\"fastRecorderStatus\"]);\n      return refreshed.fastRecorderStatus || status;\n    }\n    return status;\n  };\n\n  useEffect(() => {\n    let canceled = false;\n    (async () => {\n      const status = await migrateFastRecorderStatus();\n      if (canceled) return;\n      setFastRecorderInfo((prev) => ({\n        ...prev,\n        status,\n        probe: status?.probe || null,\n        decision: status?.decision || null,\n        disabled: Boolean(status?.disabled),\n        disabledReason: status?.disabledReason || null,\n        disabledDetails: status?.disabledDetails || null,\n        disabledAt: status?.disabledAt || null,\n      }));\n      const staleHours = 12;\n      const probeAgeMs =\n        status?.probe?.at && Number.isFinite(status.probe.at)\n          ? Date.now() - status.probe.at\n          : Infinity;\n      const shouldRun =\n        !status?.probe ||\n        probeAgeMs > staleHours * 60 * 60 * 1000 ||\n        !hasLadderFields(status?.probe);\n      if (shouldRun) {\n        await runFastRecorderProbe(\"effect\");\n      }\n    })();\n    return () => {\n      canceled = true;\n    };\n  }, [contentState?.useWebCodecsRecorder]);\n\n  return (\n    <DropdownMenu.Root\n      open={props.open}\n      onOpenChange={(open) => {\n        props.setOpen(open);\n\n        chrome.runtime\n          .sendMessage({ type: \"check-restore\" })\n          .then((response) => {\n            setRestore(response.restore);\n          });\n\n        if (CLOUD_FEATURES_ENABLED && contentState.isSubscribed) {\n          chrome.runtime\n            .sendMessage({ type: \"check-cloud-restore\" })\n            .then((response) => {\n              setCloudRestore(response?.cloudRestore ?? false);\n            })\n            .catch(() => setCloudRestore(false));\n        }\n\n        chrome.storage.local.get([\"fastRecorderStatus\"], (result) => {\n          const status = result.fastRecorderStatus || null;\n          setFastRecorderInfo((prev) => ({\n            ...prev,\n            status,\n            probe: status?.probe || null,\n            decision: status?.decision || null,\n            disabled: Boolean(status?.disabled),\n            disabledReason: status?.disabledReason || null,\n            disabledDetails: status?.disabledDetails || null,\n            disabledAt: status?.disabledAt || null,\n          }));\n        });\n      }}\n    >\n      <DropdownMenu.Trigger asChild>\n        <button className=\"IconButton\" aria-label=\"Customise options\">\n          <MoreIconPopup />\n        </button>\n      </DropdownMenu.Trigger>\n\n      <DropdownMenu.Portal\n        container={props.shadowRef.current.shadowRoot.querySelector(\n          \".container\"\n        )}\n      >\n        <DropdownMenu.Content className=\"DropdownMenuContent\" sideOffset={5}>\n          {!contentState.isSubscribed && !contentState.isLoggedIn && (\n            <DropdownMenu.Sub\n              open={openResize}\n              onOpenChange={(open) => {\n                if (open) {\n                  setOpenFPS(false);\n                  setOpenQuality(false);\n                }\n                setOpenResize(open);\n              }}\n            >\n              <DropdownMenu.SubTrigger className=\"DropdownMenuItem\">\n                {chrome.i18n.getMessage(\"resizeWindowLabel\")}\n                <div className=\"ItemIndicatorArrow\">\n                  <img src={DropdownGroup} />\n                </div>\n              </DropdownMenu.SubTrigger>\n              <DropdownMenu.Portal>\n                <DropdownMenu.SubContent\n                  className=\"ScreenityDropdownMenuContent\"\n                  sideOffset={0}\n                  alignOffset={-3}\n                >\n                  <TooltipWrap\n                    content={\n                      width < 3840 || height < 2160\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 3840,\n                          height: 2160,\n                        });\n                      }}\n                      disabled={width < 3840 || height < 2160}\n                    >\n                      3840 x 2160 (4k)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                  <TooltipWrap\n                    content={\n                      width < 1920 || height < 1080\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 1920,\n                          height: 1080,\n                        });\n                      }}\n                      disabled={width < 1920 || height < 1080}\n                    >\n                      1920 x 1080 (1080p)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                  <TooltipWrap\n                    content={\n                      width < 1280 || height < 720\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 1280,\n                          height: 720,\n                        });\n                      }}\n                      disabled={width < 1280 || height < 720}\n                    >\n                      1280 x 720 (720p)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                  <TooltipWrap\n                    content={\n                      width < 640 || height < 480\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 640,\n                          height: 480,\n                        });\n                      }}\n                      disabled={width < 640 || height < 480}\n                    >\n                      640 x 480 (480p)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                  <TooltipWrap\n                    content={\n                      width < 480 || height < 360\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 480,\n                          height: 360,\n                        });\n                      }}\n                      disabled={width < 480 || height < 360}\n                    >\n                      480 x 360 (360p)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                  <TooltipWrap\n                    content={\n                      width < 320 || height < 240\n                        ? chrome.i18n.getMessage(\"screenTooSmallTooltip\")\n                        : \"\"\n                    }\n                  >\n                    <DropdownMenu.Item\n                      className=\"ScreenityDropdownMenuItem\"\n                      onClick={(e) => {\n                        chrome.runtime.sendMessage({\n                          type: \"resize-window\",\n                          width: 320,\n                          height: 240,\n                        });\n                      }}\n                      disabled={width < 320 || height < 240}\n                    >\n                      320 x 240 (240p)\n                    </DropdownMenu.Item>\n                  </TooltipWrap>\n                </DropdownMenu.SubContent>\n              </DropdownMenu.Portal>\n            </DropdownMenu.Sub>\n          )}\n          {!contentState.isSubscribed && !contentState.isLoggedIn && (\n            <DropdownMenu.Sub\n              open={openQuality}\n              onOpenChange={(open) => {\n                if (open) {\n                  setOpenFPS(false);\n                  setOpenResize(false);\n                }\n                setOpenQuality(open);\n              }}\n            >\n              <DropdownMenu.SubTrigger className=\"DropdownMenuItem\">\n                {chrome.i18n.getMessage(\"maxResolutionLabel\") +\n                  \" (\" +\n                  contentState.qualityValue +\n                  \")\"}\n                <div className=\"ItemIndicatorArrow\">\n                  <img src={DropdownGroup} />\n                </div>\n              </DropdownMenu.SubTrigger>\n              <DropdownMenu.Portal>\n                <DropdownMenu.SubContent\n                  className=\"ScreenityDropdownMenuContent\"\n                  sideOffset={0}\n                  alignOffset={-3}\n                >\n                  <DropdownMenu.RadioGroup\n                    value={contentState.qualityValue}\n                    onValueChange={(value) => {\n                      setContentState((prevContentState) => ({\n                        ...prevContentState,\n                        qualityValue: value,\n                      }));\n                      chrome.storage.local.set({\n                        qualityValue: value,\n                      });\n                    }}\n                  >\n                    <TooltipWrap\n                      content={\n                        RAM < 8 || width < 3840 || height < 2160\n                          ? chrome.i18n.getMessage(\"maxResolutionTooltip\")\n                          : \"\"\n                      }\n                    >\n                      <DropdownMenu.RadioItem\n                        className=\"ScreenityDropdownMenuItem\"\n                        value=\"4k\"\n                        disabled={RAM < 8 || width < 3840 || height < 2160}\n                      >\n                        4k\n                        <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                          <img src={CheckWhiteIcon} />\n                        </DropdownMenu.ItemIndicator>\n                      </DropdownMenu.RadioItem>\n                    </TooltipWrap>\n                    <TooltipWrap\n                      content={\n                        RAM < 4 || width < 1920 || height < 1080\n                          ? chrome.i18n.getMessage(\"maxResolutionTooltip\")\n                          : \"\"\n                      }\n                    >\n                      <DropdownMenu.RadioItem\n                        className=\"ScreenityDropdownMenuItem\"\n                        value=\"1080p\"\n                        disabled={RAM < 4 || width < 1920 || height < 1080}\n                      >\n                        1080p\n                        <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                          <img src={CheckWhiteIcon} />\n                        </DropdownMenu.ItemIndicator>\n                      </DropdownMenu.RadioItem>\n                    </TooltipWrap>\n                    <TooltipWrap\n                      content={\n                        RAM < 2 || width < 1280 || height < 720\n                          ? chrome.i18n.getMessage(\"maxResolutionTooltip\")\n                          : \"\"\n                      }\n                    >\n                      <DropdownMenu.RadioItem\n                        className=\"ScreenityDropdownMenuItem\"\n                        value=\"720p\"\n                        disabled={RAM < 2 || width < 1280 || height < 720}\n                      >\n                        720p\n                        <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                          <img src={CheckWhiteIcon} />\n                        </DropdownMenu.ItemIndicator>\n                      </DropdownMenu.RadioItem>\n                    </TooltipWrap>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"480p\"\n                    >\n                      480p\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"360p\"\n                    >\n                      360p\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"240p\"\n                    >\n                      240p\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                  </DropdownMenu.RadioGroup>\n                </DropdownMenu.SubContent>\n              </DropdownMenu.Portal>\n            </DropdownMenu.Sub>\n          )}\n          {!contentState.isSubscribed && !contentState.isLoggedIn && (\n            <DropdownMenu.Sub\n              open={openFPS}\n              onOpenChange={(open) => {\n                if (open) {\n                  setOpenQuality(false);\n                  setOpenResize(false);\n                }\n                setOpenFPS(open);\n              }}\n            >\n              <DropdownMenu.SubTrigger className=\"DropdownMenuItem\">\n                {chrome.i18n.getMessage(\"maxFPSLabel\") +\n                  \" (\" +\n                  contentState.fpsValue +\n                  \" fps)\"}\n                <div className=\"ItemIndicatorArrow\">\n                  <img src={DropdownGroup} />\n                </div>\n              </DropdownMenu.SubTrigger>\n              <DropdownMenu.Portal>\n                <DropdownMenu.SubContent\n                  className=\"ScreenityDropdownMenuContent\"\n                  sideOffset={0}\n                  alignOffset={-3}\n                >\n                  <DropdownMenu.RadioGroup\n                    value={contentState.fpsValue}\n                    onValueChange={(value) => {\n                      setContentState((prevContentState) => ({\n                        ...prevContentState,\n                        fpsValue: value,\n                      }));\n                      chrome.storage.local.set({\n                        fpsValue: value,\n                      });\n                    }}\n                  >\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"60\"\n                    >\n                      60 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"30\"\n                    >\n                      30 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"24\"\n                    >\n                      24 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"15\"\n                    >\n                      15 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"10\"\n                    >\n                      10 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                    <DropdownMenu.RadioItem\n                      className=\"ScreenityDropdownMenuItem\"\n                      value=\"5\"\n                    >\n                      5 fps\n                      <DropdownMenu.ItemIndicator className=\"ScreenityItemIndicator\">\n                        <img src={CheckWhiteIcon} />\n                      </DropdownMenu.ItemIndicator>\n                    </DropdownMenu.RadioItem>\n                  </DropdownMenu.RadioGroup>\n                </DropdownMenu.SubContent>\n              </DropdownMenu.Portal>\n            </DropdownMenu.Sub>\n          )}\n          <DropdownMenu.CheckboxItem\n            className=\"DropdownMenuItem\"\n            onSelect={(e) => {\n              e.preventDefault();\n            }}\n            onCheckedChange={(checked) => {\n              setContentState((prevContentState) => ({\n                ...prevContentState,\n                systemAudio: checked,\n              }));\n              chrome.storage.local.set({\n                systemAudio: checked,\n              });\n            }}\n            checked={contentState.systemAudio}\n          >\n            {chrome.i18n.getMessage(\"systemAudioLabel\")}\n            <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n              <img src={CheckWhiteIcon} />\n            </DropdownMenu.ItemIndicator>\n          </DropdownMenu.CheckboxItem>\n          {!contentState.isSubscribed &&\n            !contentState.isLoggedIn &&\n            fastRecorderInfo?.probe?.ok === true &&\n            fastRecorderInfo?.probe?.details?.selectedVideoConfig && (\n              <DropdownMenu.CheckboxItem\n                className=\"DropdownMenuItem\"\n                onSelect={(e) => {\n                  e.preventDefault();\n                }}\n                onCheckedChange={(checked) => {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    useWebCodecsRecorder: checked,\n                  }));\n                  chrome.storage.local.set({\n                    useWebCodecsRecorder: checked,\n                    ...(checked\n                      ? {\n                          lastWebCodecsFailureAt: null,\n                          lastWebCodecsFailureCode: null,\n                        }\n                      : {}),\n                  });\n                }}\n                checked={contentState.useWebCodecsRecorder === true}\n              >\n                {chrome.i18n.getMessage(\"webcodecsToggleLabel\")}\n                <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                  <img src={CheckWhiteIcon} />\n                </DropdownMenu.ItemIndicator>\n              </DropdownMenu.CheckboxItem>\n            )}\n          {!oldChrome &&\n            !contentState.isSubscribed &&\n            !contentState.isLoggedIn && (\n              <DropdownMenu.CheckboxItem\n                className=\"DropdownMenuItem\"\n                onSelect={(e) => {\n                  e.preventDefault();\n                }}\n                onCheckedChange={(checked) => {\n                  if (!checked) {\n                    chrome.runtime.sendMessage({ type: \"close-backup-tab\" });\n                  }\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    backup: checked,\n                    backupSetup: false,\n                  }));\n                  chrome.storage.local.set({\n                    backup: checked,\n                    backupSetup: false,\n                  });\n                }}\n                checked={contentState.backup}\n              >\n                {chrome.i18n.getMessage(\"backupsToggle\")}\n                <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                  <img src={CheckWhiteIcon} />\n                </DropdownMenu.ItemIndicator>\n              </DropdownMenu.CheckboxItem>\n            )}\n          {!contentState.isSubscribed && !contentState.isLoggedIn && (\n            <DropdownMenu.Item\n              className=\"DropdownMenuItem\"\n              onSelect={(e) => {\n                e.preventDefault();\n                chrome.runtime.sendMessage({ type: \"restore-recording\" });\n              }}\n              disabled={!restore}\n            >\n              {chrome.i18n.getMessage(\"restoreRecording\")}\n            </DropdownMenu.Item>\n          )}\n          {!contentState.isSubscribed && !contentState.isLoggedIn && (\n            <DropdownMenu.Item\n              className=\"DropdownMenuItem\"\n              onSelect={(e) => {\n                e.preventDefault();\n                handleTroubleshooting();\n              }}\n            >\n              {chrome.i18n.getMessage(\"downloadForTroubleshootingOption\")}\n            </DropdownMenu.Item>\n          )}\n\n          {contentState.isLoggedIn && !CLOUD_FEATURES_ENABLED && (\n            <DropdownMenu.Item\n              className=\"DropdownMenuItem\"\n              onSelect={(e) => {\n                e.preventDefault();\n                chrome.runtime.sendMessage({ type: \"open-account-settings\" });\n              }}\n            >\n              {chrome.i18n.getMessage(\"accountSettingsOption\")}\n            </DropdownMenu.Item>\n          )}\n          {contentState.isLoggedIn && !CLOUD_FEATURES_ENABLED && (\n            <DropdownMenu.Item\n              className=\"DropdownMenuItem\"\n              onSelect={(e) => {\n                e.preventDefault();\n                chrome.runtime.sendMessage({\n                  type: \"open-support\",\n                  name: contentState.screenityUser?.name || \"\",\n                  email: contentState.screenityUser?.email || \"\",\n                });\n              }}\n            >\n              {chrome.i18n.getMessage(\"supportSettingsOption\")}\n            </DropdownMenu.Item>\n          )}\n          {CLOUD_FEATURES_ENABLED && (\n            <>\n              {contentState.isLoggedIn && contentState.isSubscribed && (\n                <DropdownMenu.Item\n                  className=\"DropdownMenuItem\"\n                  onSelect={async (e) => {\n                    e.preventDefault();\n                    await resetOnboardingSeen([\"proPopupCore\", \"proCameraInfo\"]);\n                    props.setOpen(false);\n                    runProPopupOnboardingIfNeeded({\n                      rootContext: props.shadowRef?.current?.shadowRoot || document,\n                      isPro: Boolean(\n                        contentState.isLoggedIn && contentState.isSubscribed\n                      ),\n                      isLoggedIn: Boolean(contentState.isLoggedIn),\n                      popupOpen: Boolean(\n                        contentState.showPopup && contentState.showExtension\n                      ),\n                      cameraEnabled: Boolean(contentState.cameraActive),\n                      pendingRecording: Boolean(contentState.pendingRecording),\n                      preparingRecording: Boolean(contentState.preparingRecording),\n                      recording: Boolean(contentState.recording),\n                      countdownActive: Boolean(contentState.countdownActive),\n                      isCountdownVisible: Boolean(contentState.isCountdownVisible),\n                      forceStart: true,\n                    });\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"resetOnboardingOption\") ||\n                    \"Reset onboarding\"}\n                </DropdownMenu.Item>\n              )}\n              {contentState.isLoggedIn && contentState.isSubscribed && (\n                <DropdownMenu.Item\n                  className=\"DropdownMenuItem\"\n                  onSelect={(e) => {\n                    e.preventDefault();\n                    chrome.runtime.sendMessage({\n                      type: \"restore-cloud-recording\",\n                    });\n                  }}\n                  disabled={!cloudRestore}\n                >\n                  {chrome.i18n.getMessage(\"recoverLastRecordingOption\")}\n                </DropdownMenu.Item>\n              )}\n            </>\n          )}\n          {CLOUD_FEATURES_ENABLED && (\n            <DropdownMenu.Item\n              className=\"DropdownMenuItem\"\n              onSelect={(e) => {\n                e.preventDefault();\n                if (contentState.isLoggedIn) {\n                  // Log out flow\n                  chrome.runtime.sendMessage({ type: \"handle-logout\" });\n                  setContentState((prev) => ({\n                    ...prev,\n                    isLoggedIn: false,\n                    wasLoggedIn: true,\n                    isSubscribed: false,\n                    screenityUser: null,\n                    proSubscription: null,\n                    bigTab: \"record\",\n                  }));\n                  contentState.openToast(\n                    chrome.i18n.getMessage(\"loggedOutToastTitle\"),\n                    () => {},\n                    2000\n                  );\n                } else {\n                  // Log in flow (open login page)\n                  chrome.runtime.sendMessage({ type: \"handle-login\" });\n                }\n                props.setOpen(false); // Close the menu after action\n              }}\n            >\n              {contentState.isLoggedIn\n                ? chrome.i18n.getMessage(\"logoutButtonLabel\") || \"Log out\"\n                : chrome.i18n.getMessage(\"loginButtonLabel\") ||\n                  \"Log in or sign up\"}\n            </DropdownMenu.Item>\n          )}\n        </DropdownMenu.Content>\n      </DropdownMenu.Portal>\n    </DropdownMenu.Root>\n  );\n};\n\nexport default SettingsMenu;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/VideosTab.jsx",
    "content": "import React, { useState, useEffect, useRef, useCallback } from \"react\";\nimport * as Tabs from \"@radix-ui/react-tabs\";\nimport VideoItem from \"../components/VideoItem\";\nimport { PlaceholderThumb } from \"../../images/popup/images\";\nimport { useContext } from \"react\";\nimport { contentStateContext } from \"../../context/ContentState\";\n\nimport * as DropdownMenu from \"@radix-ui/react-dropdown-menu\";\nimport { DropdownIcon, CheckWhiteIcon } from \"../../images/popup/images\";\n\nimport {\n  TempTwitter,\n  TempFigma,\n  TempDesignSystem,\n  TempMarketing,\n  TempSubstack,\n} from \"../../images/popup/images\";\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nconst VideosTab = (props) => {\n  const [videos, setVideos] = useState([]);\n  const [loading, setLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [hasMore, setHasMore] = useState(true);\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const fetchedPagesRef = useRef(new Set()); // key = `${sortBy}-${page}`\n  const videoCacheRef = useRef({}); // key: `${sortBy}-${page}` → video[]\n  const VIDEO_CACHE_STORAGE_KEY = \"cachedVideosBySort\";\n\n  const pageRef = useRef(0); // Track page here without triggering re-renders\n  const observerRef = useRef();\n\n  const PAGE_SIZE = 8;\n  const sortBy = contentState.sortBy || \"newest\";\n  const filter = \"all\";\n\n  const lastFetchTimeRef = useRef(0);\n  const FETCH_COOLDOWN_MS = 1500;\n\n  useEffect(() => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n\n    chrome.storage.local.get(VIDEO_CACHE_STORAGE_KEY, (result) => {\n      if (result?.[VIDEO_CACHE_STORAGE_KEY]) {\n        videoCacheRef.current = result[VIDEO_CACHE_STORAGE_KEY];\n\n        // Hydrate fetchedPagesRef with cached keys\n        fetchedPagesRef.current = new Set(Object.keys(videoCacheRef.current));\n\n        // Collect all cached videos for current sort\n        const matchingKeys = Object.keys(videoCacheRef.current).filter((key) =>\n          key.startsWith(`${sortBy}-`)\n        );\n\n        // Sort by page number (e.g. newest-0, newest-1, newest-2...)\n        const sortedKeys = matchingKeys.sort((a, b) => {\n          const aPage = parseInt(a.split(\"-\")[1], 10);\n          const bPage = parseInt(b.split(\"-\")[1], 10);\n          return aPage - bPage;\n        });\n\n        const allCachedVideos = sortedKeys.flatMap(\n          (key) => videoCacheRef.current[key] || []\n        );\n\n        setVideos(allCachedVideos);\n\n        // Update pageRef to next page\n        pageRef.current = sortedKeys.length;\n\n        // If the last page is smaller than PAGE_SIZE, we're done\n        const lastPageKey = sortedKeys[sortedKeys.length - 1];\n        const lastPage = videoCacheRef.current[lastPageKey] || [];\n        setHasMore(lastPage.length === PAGE_SIZE);\n      } else {\n        // No cache found, do initial fetch\n        pageRef.current = 0;\n        setHasMore(true);\n        fetchVideos();\n      }\n    });\n  }, []);\n\n  useEffect(() => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n\n    if (contentState.isSubscribed) {\n      setVideos([]);\n      pageRef.current = 0;\n      setHasMore(true);\n      fetchedPagesRef.current = new Set();\n      fetchVideos();\n    }\n  }, [sortBy]);\n\n  const fetchVideos = useCallback(async () => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n\n    const now = Date.now();\n\n    if (\n      !contentState.isSubscribed ||\n      loading ||\n      !hasMore ||\n      now - lastFetchTimeRef.current < FETCH_COOLDOWN_MS\n    ) {\n      return;\n    }\n\n    lastFetchTimeRef.current = now;\n\n    const cacheKey = `${sortBy}-${pageRef.current}`;\n    if (fetchedPagesRef.current.has(cacheKey)) return;\n    fetchedPagesRef.current.add(cacheKey);\n    setLoading(true);\n\n    try {\n      if (videoCacheRef.current[cacheKey]) {\n        const cachedVideos = videoCacheRef.current[cacheKey];\n        setVideos((prev) => [...prev, ...cachedVideos]);\n\n        if (cachedVideos.length < PAGE_SIZE) {\n          setHasMore(false);\n        } else {\n          pageRef.current += 1;\n        }\n\n        return;\n      }\n\n      const response = await chrome.runtime.sendMessage({\n        type: \"fetch-videos\",\n        page: pageRef.current,\n        pageSize: PAGE_SIZE,\n        sort: sortBy,\n        filter,\n      });\n\n      if (!response?.success) {\n        console.error(\"❌ Failed to fetch videos:\", response?.error);\n        setError(response?.error || \"Failed to load videos\");\n        setHasMore(false);\n        return;\n      }\n\n      const newVideos = response.videos || [];\n      setVideos((prev) => [...prev, ...newVideos]);\n\n      if (newVideos.length > 0) {\n        videoCacheRef.current[cacheKey] = newVideos;\n        chrome.storage.local.set({\n          [VIDEO_CACHE_STORAGE_KEY]: videoCacheRef.current,\n        });\n      }\n\n      if (newVideos.length < PAGE_SIZE) {\n        setHasMore(false);\n      } else {\n        pageRef.current += 1;\n      }\n    } catch (err) {\n      console.error(\"❌ Unexpected error:\", err);\n      setError(\"Failed to load videos\");\n    } finally {\n      setLoading(false);\n    }\n  }, [loading, hasMore, contentState.isSubscribed, sortBy]);\n\n  // useEffect(() => {\n  //   if (contentState.isSubscribed) {\n  //     fetchVideos();\n  //   }\n  // }, []); // Run once on mount\n\n  useEffect(() => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n    if (!observerRef.current || !hasMore || !contentState.isSubscribed) return;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries[0].isIntersecting) {\n          fetchVideos();\n        }\n      },\n      { threshold: 1.0 }\n    );\n\n    observer.observe(observerRef.current);\n\n    return () => observer.disconnect();\n  }, [fetchVideos, hasMore]);\n\n  const handleVideoClick = (videoId) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n    const url = process.env.SCREENITY_APP_BASE + `/editor/${videoId}/edit`;\n    window.open(url, \"_blank\");\n  };\n\n  const handleCopyLink = (videoId) => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n    const link = process.env.SCREENITY_APP_BASE + `/view/${videoId}`;\n    navigator.clipboard\n      .writeText(link)\n      .then(() => {\n        contentState.openToast(\n          chrome.i18n.getMessage(\"copiedToClipboardToast\"),\n          3000\n        );\n      })\n      .catch((err) => {\n        console.error(\"❌ Failed to copy:\", err);\n        contentState.openToast(\n          chrome.i18n.getMessage(\"failedToCopyToClipboardToast\"),\n          3000\n        );\n      });\n  };\n\n  useEffect(() => {\n    if (!CLOUD_FEATURES_ENABLED) {\n      // show only local placeholder videos\n      return;\n    }\n    chrome.storage.local.get([\"sortBy\"], (result) => {\n      if (result.sortBy && !contentState.sortBy) {\n        setContentState((prev) => ({ ...prev, sortBy: result.sortBy }));\n      }\n    });\n  }, []);\n\n  const sortLabelMap = {\n    newest: chrome.i18n.getMessage(\"newestSortLabel\"),\n    oldest: chrome.i18n.getMessage(\"oldestSortLabel\"),\n    alphabetical: \"A–Z\",\n    \"reverse-alphabetical\": \"Z–A\",\n  };\n\n  return (\n    <div\n      className={contentState.isSubscribed ? \"video-ui\" : \"video-ui blurred\"}\n    >\n      {!contentState.isSubscribed && (\n        <div className=\"ModalSoon\">\n          {/* 👇 Embed the video here */}\n          <video\n            src={chrome.runtime.getURL(\"assets/videos/pro.mp4\")}\n            autoPlay\n            loop\n            muted\n            playsInline\n            style={{\n              width: \"100%\",\n              borderRadius: \"6px\",\n              marginBottom: \"20px\",\n            }}\n          />\n          <div className=\"ModalSoonTitle\">\n            {chrome.i18n.getMessage(\"shareModalSandboxTitle\")}\n          </div>\n\n          <div className=\"ModalSoonDescription\">\n            {chrome.i18n.getMessage(\"shareModalSandboxDescription\")}\n          </div>\n\n          <div\n            className=\"ModalSoonButton\"\n            onClick={() => {\n              chrome.runtime.sendMessage({ type: \"pricing\" });\n            }}\n          >\n            {chrome.i18n.getMessage(\"shareModalSandboxButton\")}\n          </div>\n\n          <button\n            onClick={() => {\n              chrome.runtime.sendMessage({ type: \"handle-login\" });\n            }}\n            className=\"ModalSoonSecondary\"\n            style={{\n              marginTop: 16,\n              width: \"100%\",\n              background: \"transparent\",\n              border: \"none\",\n              color: \"#6B7280\",\n              fontSize: 13,\n              textAlign: \"center\",\n              cursor: \"pointer\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"shareModalSandboxLogin\")}\n          </button>\n        </div>\n      )}\n      <Tabs.Root className=\"TabsRoot\" defaultValue=\"personal\">\n        <Tabs.List className=\"TabsList\" aria-label=\"Manage your account\">\n          <div className=\"TabsTriggerWrap\">\n            <Tabs.Trigger className=\"TabsTrigger\" value=\"personal\">\n              <div className=\"TabsTriggerLabel\">\n                <span>{chrome.i18n.getMessage(\"allVideosHeading\")}</span>\n              </div>\n            </Tabs.Trigger>\n            {/* <Tabs.Trigger className=\"TabsTrigger\" value=\"team\">\n              <div className=\"TabsTriggerLabel\">\n                <span>Team</span>\n              </div>\n            </Tabs.Trigger>\n            <Tabs.Trigger className=\"TabsTrigger\" value=\"shared\">\n              <div className=\"TabsTriggerLabel\">\n                <span>Shared</span>\n              </div>\n            </Tabs.Trigger> */}\n          </div>\n          <DropdownMenu.Root>\n            <DropdownMenu.Trigger asChild>\n              <button className=\"TabsSort\" aria-label=\"Sort videos\">\n                <div className=\"TabsSortLabel\">\n                  {sortLabelMap[sortBy] || \"Sort\"} <img src={DropdownIcon} />\n                </div>\n              </button>\n            </DropdownMenu.Trigger>\n\n            <DropdownMenu.Portal\n              container={props.shadowRef.current.shadowRoot.querySelector(\n                \".container\"\n              )}\n            >\n              <DropdownMenu.Content\n                className=\"DropdownMenuContent\"\n                sideOffset={4}\n                align=\"end\"\n              >\n                <DropdownMenu.RadioGroup\n                  value={sortBy}\n                  onValueChange={(value) => {\n                    setContentState((prev) => ({ ...prev, sortBy: value }));\n                    chrome.storage.local.set({ sortBy: value });\n                  }}\n                >\n                  <DropdownMenu.RadioItem\n                    className=\"DropdownMenuItem\"\n                    value=\"newest\"\n                  >\n                    {chrome.i18n.getMessage(\"newestSortLabel\")}\n                    <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                      <img src={CheckWhiteIcon} />\n                    </DropdownMenu.ItemIndicator>\n                  </DropdownMenu.RadioItem>\n\n                  <DropdownMenu.RadioItem\n                    className=\"DropdownMenuItem\"\n                    value=\"oldest\"\n                  >\n                    {chrome.i18n.getMessage(\"oldestSortLabel\")}\n                    <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                      <img src={CheckWhiteIcon} />\n                    </DropdownMenu.ItemIndicator>\n                  </DropdownMenu.RadioItem>\n\n                  <DropdownMenu.RadioItem\n                    className=\"DropdownMenuItem\"\n                    value=\"alphabetical\"\n                  >\n                    A–Z\n                    <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                      <img src={CheckWhiteIcon} />\n                    </DropdownMenu.ItemIndicator>\n                  </DropdownMenu.RadioItem>\n\n                  <DropdownMenu.RadioItem\n                    className=\"DropdownMenuItem\"\n                    value=\"reverse-alphabetical\"\n                  >\n                    Z–A\n                    <DropdownMenu.ItemIndicator className=\"ItemIndicator\">\n                      <img src={CheckWhiteIcon} />\n                    </DropdownMenu.ItemIndicator>\n                  </DropdownMenu.RadioItem>\n                </DropdownMenu.RadioGroup>\n              </DropdownMenu.Content>\n            </DropdownMenu.Portal>\n          </DropdownMenu.Root>\n        </Tabs.List>\n\n        <Tabs.Content className=\"TabsContent\" value=\"personal\">\n          <div className=\"videos-list\">\n            {error && <p>{error}</p>}\n            {videos.length === 0 &&\n              !loading &&\n              !error &&\n              contentState.isSubscribed && (\n                <div className=\"empty-state\">\n                  <div style={{ fontSize: \"32px\", marginBottom: \"10px\" }}>\n                    👻\n                  </div>\n                  <span>{chrome.i18n.getMessage(\"noVideosFound\")}</span>\n                </div>\n              )}\n            {(contentState.isSubscribed\n              ? videos\n              : [\n                  {\n                    title: \"Bug report\",\n                    createdAt: \"3 minutes ago\",\n                    data: { thumbnail: TempTwitter },\n                  },\n                  {\n                    title: \"Figma async review\",\n                    createdAt: \"1 hour ago\",\n                    data: { thumbnail: TempFigma },\n                  },\n                  {\n                    title: \"Design systems onboarding\",\n                    createdAt: \"4 days ago\",\n                    data: { thumbnail: TempDesignSystem },\n                  },\n                  {\n                    title: \"Cool SaaS resources\",\n                    createdAt: \"Feb 12\",\n                    data: { thumbnail: TempMarketing },\n                  },\n                  {\n                    title: \"Newsletter promo\",\n                    createdAt: \"Jan 23\",\n                    data: { thumbnail: TempSubstack },\n                  },\n                  {\n                    title: \"Product demo\",\n                    createdAt: \"Jan 15\",\n                    data: { thumbnail: PlaceholderThumb },\n                  },\n                ]\n            ).map((video, i) => (\n              <VideoItem\n                key={i}\n                title={video.title}\n                date={video.createdAt}\n                thumbnail={\n                  video.signedThumbnail ||\n                  (video.data?.useCustomThumbnail &&\n                    video.data?.customThumbnail) ||\n                  video.data?.tempThumbnail ||\n                  PlaceholderThumb\n                }\n                onOpen={\n                  contentState.isSubscribed\n                    ? () => handleVideoClick(video._id)\n                    : undefined\n                }\n                onCopyLink={\n                  contentState.isSubscribed\n                    ? () => handleCopyLink(video._id)\n                    : undefined\n                }\n              />\n            ))}\n            {loading && (\n              <div className=\"spinner-container\">\n                <div className=\"spinner\" />\n                <span>{chrome.i18n.getMessage(\"loadingVideosLabel\")}</span>\n              </div>\n            )}\n            <div ref={observerRef} style={{ height: \"1px\" }} />\n          </div>\n\n          <div className=\"bottom-section\">\n            <a\n              href={process.env.SCREENITY_APP_BASE}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              role=\"button\"\n              className=\"main-button dashboard-button\"\n              tabIndex=\"0\"\n              style={{\n                zIndex: 99,\n              }}\n            >\n              <span className=\"main-button-label\">\n                {chrome.i18n.getMessage(\"goToDashboardButtonLabel\")}\n              </span>\n              {/* <span className=\"main-button-shortcut\">Ctrl+D</span> */}\n            </a>\n          </div>\n        </Tabs.Content>\n\n        <Tabs.Content className=\"TabsContent\" value=\"team\"></Tabs.Content>\n        <Tabs.Content className=\"TabsContent\" value=\"shared\"></Tabs.Content>\n      </Tabs.Root>\n    </div>\n  );\n};\n\nexport default VideosTab;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/Welcome.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport SoloDev from \"../../../../assets/solo-dev.png\";\n// import EditorPreview from \"../../../../assets/editor-preview.png\"; // replace with actual screenshot file\n\nconst Welcome = (props) => {\n  const isUpdated = props.isBack;\n  const clearBack = props.clearBack;\n  const [learntAboutPro, setLearntAboutPro] = useState(false);\n\n  useEffect(() => {\n    // if (isUpdated) {\n    chrome.storage.local.get(\"learntAboutPro\", (res) => {\n      if (res.learntAboutPro) setLearntAboutPro(true);\n    });\n    // }\n  }, []);\n\n  return (\n    <div\n      className=\"announcement\"\n      style={{\n        marginTop: \"50px\",\n        paddingBottom: \"0px\",\n      }}\n    >\n      <div className=\"announcement-wrap\">\n        <div className=\"announcement-details\">\n          <div className=\"welcome-title\">\n            {!isUpdated\n              ? chrome.i18n.getMessage(\"welcomePopupTitle\")\n              : chrome.i18n.getMessage(\"welcomeBackPopupTitle\")}\n          </div>\n          <div className=\"welcome-description\">\n            {chrome.i18n.getMessage(\"welcomePopupDescriptionTop\")}\n            <br />\n            {chrome.i18n.getMessage(\"welcomePopupDescriptionBottom\")}\n          </div>\n          <div\n            className=\"welcome-cta\"\n            style={{\n              marginBottom: \"30px\",\n            }}\n            onClick={() => {\n              if (isUpdated) clearBack();\n              props.setOnboarding(false);\n              props.setContentState((prev) => ({\n                ...prev,\n                onboarding: false,\n              }));\n              chrome.storage.local.set({ onboarding: false });\n            }}\n          >\n            👋 {chrome.i18n.getMessage(\"welcomePopupCTA\")}\n          </div>\n        </div>\n      </div>\n\n      <div className=\"welcome-content\">\n        <div className=\"welcome-content-wrap\">\n          <div className=\"welcome-content-title\">\n            {!isUpdated\n              ? chrome.i18n.getMessage(\"welcomeProTitle\")\n              : chrome.i18n.getMessage(\"welcomeBackProTitle\") ||\n                \"Want to do more with your recordings?\"}\n          </div>\n\n          <div className=\"welcome-video\">\n            <div\n              className=\"video-wrapper\"\n              style={{\n                overflow: \"hidden\",\n                display: \"inline-block\",\n                borderRadius: \"10px\",\n              }}\n            >\n              <video\n                src={chrome.runtime.getURL(\"assets/videos/pro.mp4\")}\n                autoPlay\n                loop\n                muted\n                playsInline\n                style={{\n                  display: \"block\",\n                  width: \"calc(100% + 2px)\", // +1 left, +1 right\n                  height: \"calc(100% + 3px)\", // +1 top, +2 bottom\n                  transform: \"translate(-1px, -1px)\", // shift left + top\n                }}\n              />\n            </div>\n          </div>\n\n          <p\n            className=\"welcome-content-description\"\n            style={{\n              color: \"#6E7684\",\n              textAlign: \"center\",\n            }}\n          >\n            {learntAboutPro\n              ? chrome.i18n.getMessage(\"welcomeProDescription\") ||\n                \"Sign in to save your videos to the cloud, share with a link, and access advanced editing features.\"\n              : chrome.i18n.getMessage(\"welcomeBackProDescription\") ||\n                \"Sign in to save your videos to the cloud, share with a link, and access advanced editing features.\"}\n          </p>\n\n          {/* <div\n            onClick={() => {\n              if (!isUpdated || learntAboutPro) {\n                chrome.runtime.sendMessage({ type: \"handle-login\" });\n              } else {\n                chrome.storage.local.set({ learntAboutPro: true });\n                chrome.runtime.sendMessage({ type: \"pricing\" });\n                setLearntAboutPro(true);\n              }\n            }}\n            role=\"button\"\n            className=\"main-button dashboard-button\"\n            tabIndex=\"0\"\n            style={{\n              zIndex: 99,\n              marginTop: \"25px\",\n            }}\n          >\n            <span className=\"main-button-label\">\n              {!isUpdated || learntAboutPro\n                ? chrome.i18n.getMessage(\"welcomeProButton\") ||\n                  \"Sign in to unlock paid features\"\n                : !learntAboutPro\n                ? chrome.i18n.getMessage(\"welcomeBackProCTA\") ||\n                  \"Learn more about Screenity Pro\"\n                : chrome.i18n.getMessage(\"welcomeBackProCTAAfterLearn\") ||\n                  \"Sign in to unlock paid features\"}\n            </span>\n          </div> */}\n\n          <div\n            onClick={() => {\n              if (learntAboutPro) {\n                // After they've seen the Pro info, trigger sign in\n                chrome.runtime.sendMessage({ type: \"handle-login\" });\n              } else {\n                // First click: show pricing page and mark as \"learnt\"\n                chrome.storage.local.set({ learntAboutPro: true });\n                chrome.runtime.sendMessage({ type: \"pricing\" });\n                setLearntAboutPro(true);\n              }\n            }}\n            role=\"button\"\n            className=\"main-button dashboard-button\"\n            tabIndex=\"0\"\n            style={{\n              zIndex: 99,\n              marginTop: \"25px\",\n            }}\n          >\n            <span className=\"main-button-label\">\n              {learntAboutPro\n                ? chrome.i18n.getMessage(\"welcomeProButton\") ||\n                  \"Sign in to unlock paid features\"\n                : chrome.i18n.getMessage(\"welcomeBackProCTA\") ||\n                  \"Learn more about Screenity Pro\"}\n            </span>\n          </div>\n          <a\n            className=\"welcome-support\"\n            href=\"https://alyssax.substack.com/\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            style={{\n              cursor: \"pointer\",\n            }}\n          >\n            {chrome.i18n.getMessage(\"welcomeProSupport\") ||\n              \"Support development by a solo indie maker \"}\n            <img src={chrome.runtime.getURL(SoloDev)} alt=\"Alyssa X\" />\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Welcome;\n"
  },
  {
    "path": "src/pages/Content/popup/layout/WelcomeAlternate.jsx",
    "content": "import React, { useState, useEffect } from \"react\";\nimport CheckBlueIcon from \"../../../../assets/check-blue.svg\";\nimport SoloDev from \"../../../../assets/solo-dev.png\";\n\nconst FeatureItem = ({ text }) => {\n  // render with a checkmark icon on left and text on right\n  return (\n    <div className=\"welcome-feature-item\">\n      <div className=\"welcome-feature-icon\">\n        <img src={chrome.runtime.getURL(CheckBlueIcon)} alt=\"Checkmark\" />\n      </div>\n      <span className=\"welcome-feature-item-text\">{text}</span>\n    </div>\n  );\n};\n\nconst Welcome = (props) => {\n  return (\n    <div\n      className=\"announcement\"\n      style={{\n        marginTop: \"50px\",\n        paddingBottom: \"0px\",\n      }}\n    >\n      <div className=\"announcement-wrap\">\n        {/* <div className=\"announcement-hero\">\n\t\t\t\t\t<img src={chrome.runtime.getURL(\"assets/helper/hero.png\")} />\n\t\t\t\t</div> */}\n        <div className=\"announcement-details\">\n          <div className=\"welcome-title\">\n            {chrome.i18n.getMessage(\"welcomePopupTitle\")}\n          </div>\n          <div className=\"welcome-description\">\n            {chrome.i18n.getMessage(\"welcomePopupDescriptionTop\")}\n            <br />\n            {chrome.i18n.getMessage(\"welcomePopupDescriptionBottom\")}\n          </div>\n          <div\n            className=\"welcome-cta\"\n            style={{\n              marginBottom: \"30px\",\n            }}\n            onClick={() => {\n              props.setOnboarding(false);\n              chrome.storage.local.set({ updatingFromOld: false });\n            }}\n          >\n            👋 {chrome.i18n.getMessage(\"welcomePopupCTA\")}\n          </div>\n        </div>\n      </div>\n      <div className=\"welcome-content\">\n        <div className=\"welcome-content-wrap\">\n          <div className=\"welcome-content-title\">\n            {chrome.i18n.getMessage(\"welcomePopupCloudTitle\")}\n          </div>\n          <div className=\"welcome-video\"></div>\n          <div className=\"welcome-feature-list\">\n            <FeatureItem\n              text={chrome.i18n.getMessage(\"welcomePopupCloudFeature1\")}\n            />\n            <FeatureItem\n              text={chrome.i18n.getMessage(\"welcomePopupCloudFeature2\")}\n            />\n            <FeatureItem\n              text={chrome.i18n.getMessage(\"welcomePopupCloudFeature3\")}\n            />\n            <FeatureItem\n              text={chrome.i18n.getMessage(\"welcomePopupCloudFeature4\")}\n            />\n            <FeatureItem\n              text={chrome.i18n.getMessage(\"welcomePopupCloudFeature5\")}\n            />\n          </div>\n          <a\n            href={process.env.SCREENITY_APP_BASE}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            role=\"button\"\n            className=\"main-button dashboard-button\"\n            tabIndex=\"0\"\n            style={{\n              zIndex: 99,\n              marginTop: \"25px\",\n            }}\n          >\n            <span className=\"main-button-label\">\n              {chrome.i18n.getMessage(\"welcomePopupCTA\")}\n            </span>\n            <span\n              className=\"main-button-shortcut\"\n              style={{\n                fontSize: \"14px\",\n              }}\n            >\n              $8/mo\n            </span>\n          </a>\n          <a\n            className=\"welcome-support\"\n            href=\"https://https://alyssax.substack.com/\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            {chrome.i18n.getMessage(\"welcomePopupSupport\")}{\" \"}\n            <img src={chrome.runtime.getURL(SoloDev)} alt=\"Alyssa X\" />\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Welcome;\n"
  },
  {
    "path": "src/pages/Content/popup/onboarding/proOnboarding.js",
    "content": "import { driver } from \"driver.js\";\nimport \"driver.js/dist/driver.css\";\nimport { hasSeenOnboarding, markOnboardingSeen } from \"./storage\";\n\nconst CORE_KEY = \"proPopupCore\";\nconst CAMERA_KEY = \"proCameraInfo\";\nconst POPOVER_CLASS = \"ScreenityOnboardingPopover onboarding-popover\";\nconst DRIVER_STYLE_ID = \"screenity-driver-onboarding-style\";\nconst TOOLBAR_HELP_URL =\n  \"https://help.screenity.io/recording/how-to-hide-the-toolbar\";\nconst IDLE_START_DELAY_MS = 420;\n\nconst STEP_IDS = {\n  WELCOME: \"welcome\",\n  TOOLBAR: \"toolbar\",\n  CAMERA: \"camera\",\n  INSTANT: \"instant\",\n};\n\nconst TOOLBAR_SELECTORS = [\n  \"#pro-onboarding-recording-toolbar-root\",\n  \"#pro-onboarding-recording-toolbar .ToolbarRoot\",\n  \"#pro-onboarding-recording-toolbar\",\n  \"#pro-onboarding-recording-toolbar-controls\",\n  \".react-draggable .ToolbarRoot\",\n];\nconst INSTANT_PRIMARY_SELECTORS = [\n  \"#pro-onboarding-instant-mode-toggle-row\",\n  \"#pro-onboarding-instant-mode-field\",\n  \"#pro-onboarding-instant-mode-toggle\",\n];\nconst INSTANT_FALLBACK_SELECTORS = [\n  \"#pro-onboarding-popup-container\",\n  \".popup-container\",\n];\nconst CAMERA_SELECTORS = [\n  \".camera-page .camera-draggable\",\n  \".camera-draggable\",\n  \".camera-page\",\n];\nconst CONFLICT_SELECTORS = [\n  \".AlertDialogContent\",\n  \".AlertDialogOverlay\",\n  \".countdown-overlay\",\n  \".countdown-circle\",\n  \".recording-countdown\",\n];\nconst START_CANCEL_EVENTS = [\"mousedown\", \"click\", \"keydown\"];\n\nconst DEBUG =\n  typeof window !== \"undefined\" && Boolean(window.SCREENITY_DEBUG_ONBOARDING);\n\nlet activeDriver = null;\nlet activeRun = null;\nlet restoreRootStyle = null;\nlet pendingStartTimer = null;\nlet cancelPendingStartListeners = null;\nlet onboardingInProgress = false;\nlet pendingStartToken = 0;\n\nconst logDebug = (event, payload = {}) => {\n  if (!DEBUG) return;\n  // eslint-disable-next-line no-console\n  console.debug(\"[Screenity][Onboarding]\", event, payload);\n};\n\nconst getRoot = (context) => (context?.querySelector ? context : document);\n\nconst t = (key, fallback) => {\n  try {\n    return chrome.i18n.getMessage(key) || fallback;\n  } catch {\n    return fallback;\n  }\n};\n\nconst getOnboardingText = () => ({\n  welcomeTitle: t(\n    \"proOnboardingWelcomeTitle\",\n    \"Welcome to the Screenity Pro extension\",\n  ),\n  welcomeDescription: t(\n    \"proOnboardingWelcomeDescription\",\n    \"A few quick tips before you record.<br/>Auto-zooms only work for clicks inside <strong>Chrome tabs</strong>. You can still add zooms manually later.\",\n  ),\n  toolbarTitle: t(\"proOnboardingToolbarTitle\", \"Recording toolbar & effects\"),\n  toolbarDescription: t(\n    \"proOnboardingToolbarDescription\",\n    \"Use this toolbar for drawing, cursor effects, blur, and recording controls.<br/><br/><strong>This toolbar is included in the video</strong> unless you hide it.\",\n  ),\n  cameraTitle: t(\"proOnboardingCameraTitle\", \"Camera is captured separately\"),\n  cameraDescription: t(\n    \"proOnboardingCameraDescription\",\n    \"Your camera may <strong>hide or move to PiP</strong> while recording, that’s normal.<br/><br/>It’s captured separately so you can position it later.\",\n  ),\n  instantTitle: t(\"proOnboardingInstantTitle\", \"Instant mode\"),\n  instantDescription: t(\n    \"proOnboardingInstantDescription\",\n    \"Best for quick sharing with <strong>unlimited downloads</strong>.<br/><br/>Advanced layouts/editor options aren’t available in this mode.\",\n  ),\n  doneBtnText: t(\"proOnboardingDone\", \"Got it\"),\n  nextBtnText: t(\"proOnboardingNext\", \"Next\"),\n  prevBtnText: t(\"proOnboardingBack\", \"Back\"),\n  learnMoreLabel: t(\"proOnboardingToolbarLearnMore\", \"Learn more\"),\n});\n\nconst find = (root, selector) => {\n  try {\n    return root.querySelector(selector);\n  } catch {\n    return null;\n  }\n};\n\nconst findFirst = (root, selectors = []) => {\n  for (const selector of selectors) {\n    const el = find(root, selector);\n    if (el) return el;\n  }\n  return null;\n};\n\nconst isElementVisible = (el) => {\n  if (!el || !el.isConnected) return false;\n  if (\n    typeof el.getClientRects === \"function\" &&\n    el.getClientRects().length === 0\n  ) {\n    return false;\n  }\n  const style = window.getComputedStyle(el);\n  return style.display !== \"none\" && style.visibility !== \"hidden\";\n};\n\nconst waitForElement = async (root, selectors, timeoutMs = 1200) => {\n  const start = Date.now();\n  while (Date.now() - start < timeoutMs) {\n    const el = findFirst(root, selectors);\n    if (isElementVisible(el)) return el;\n    await new Promise((resolve) => setTimeout(resolve, 60));\n  }\n  return null;\n};\n\nconst clearScheduledStart = () => {\n  if (pendingStartTimer) {\n    clearTimeout(pendingStartTimer);\n    pendingStartTimer = null;\n  }\n  if (typeof cancelPendingStartListeners === \"function\") {\n    cancelPendingStartListeners();\n    cancelPendingStartListeners = null;\n  }\n};\n\nconst clearDriverUiState = (root) => {\n  document.documentElement.classList.remove(\"screenity-driver-modal-step\");\n  document.documentElement.classList.remove(\"screenity-driver-active\");\n  setToolbarCloseVisible(root, false);\n  if (typeof restoreRootStyle === \"function\") restoreRootStyle();\n  restoreRootStyle = null;\n};\n\nconst hasConflictSelectorsVisible = (root) => {\n  for (const selector of CONFLICT_SELECTORS) {\n    if (isElementVisible(find(root, selector))) return true;\n    if (root !== document && isElementVisible(find(document, selector))) {\n      return true;\n    }\n  }\n  return false;\n};\n\nconst hasBlockingFlags = (state = {}) =>\n  Boolean(\n    state.pendingRecording ||\n      state.preparingRecording ||\n      state.recording ||\n      state.countdownActive ||\n      state.isCountdownVisible,\n  );\n\nconst shouldSkip = ({ root, popupOpen, isPro, isLoggedIn, state = {} }) => {\n  if (!popupOpen || !isPro || !isLoggedIn) return true;\n  if (document.hidden) return true;\n  if (hasBlockingFlags(state)) return true;\n  if (hasConflictSelectorsVisible(root)) return true;\n  return false;\n};\n\nconst isRecordingNow = async () => {\n  try {\n    const result = await chrome.storage.local.get([\n      \"recording\",\n      \"pendingRecording\",\n      \"preparingRecording\",\n      \"countdownActive\",\n      \"isCountdownVisible\",\n    ]);\n    return Boolean(\n      result.recording ||\n        result.pendingRecording ||\n        result.preparingRecording ||\n        result.countdownActive ||\n        result.isCountdownVisible,\n    );\n  } catch {\n    return false;\n  }\n};\n\nconst describeNode = (node) => {\n  if (!node) return null;\n  const rootNode = node.getRootNode?.();\n  const rootType =\n    rootNode === document\n      ? \"document\"\n      : rootNode?.host\n      ? \"shadow-root\"\n      : \"other\";\n  let rect = null;\n  try {\n    const r = node.getBoundingClientRect();\n    rect = { x: r.x, y: r.y, width: r.width, height: r.height };\n  } catch {}\n  return {\n    tag: node.tagName,\n    id: node.id || null,\n    className: node.className || null,\n    isConnected: Boolean(node.isConnected),\n    rootType,\n    parentTag: node.parentElement?.tagName || null,\n    parentClass: node.parentElement?.className || null,\n    rect,\n  };\n};\n\nconst getDriverDomSnapshot = () => {\n  const overlay = document.querySelector(\".driver-overlay\");\n  const stage = document.querySelector(\".driver-stage\");\n  const popover = document.querySelector(\".driver-popover\");\n  return {\n    overlay: describeNode(overlay),\n    stage: describeNode(stage),\n    popover: describeNode(popover),\n  };\n};\n\nconst resolveStepElement = (step) => {\n  const raw = step?.element;\n  if (!raw) return null;\n  if (typeof raw === \"function\") {\n    try {\n      return raw();\n    } catch {\n      return null;\n    }\n  }\n  if (typeof raw === \"string\") {\n    try {\n      return document.querySelector(raw);\n    } catch {\n      return null;\n    }\n  }\n  return raw;\n};\n\nconst logStepState = (event, step, options) => {\n  const state = options?.state || activeDriver?.getState?.() || {};\n  const activeStep =\n    step || state.activeStep || activeDriver?.getActiveStep?.();\n  const target = resolveStepElement(activeStep);\n  logDebug(event, {\n    activeIndex: state.activeIndex ?? activeDriver?.getActiveIndex?.(),\n    title: activeStep?.popover?.title || null,\n    target: describeNode(target),\n    targetVisible: isElementVisible(target),\n    targetRootType:\n      target?.getRootNode?.()?.host != null ? \"shadow-root\" : \"document\",\n    driverDom: getDriverDomSnapshot(),\n  });\n};\n\nconst ensureDriverStyles = () => {\n  if (document.getElementById(DRIVER_STYLE_ID)) return;\n  // Driver computes coordinates in document viewport space.\n  // Keeping driver DOM in document.body avoids shadow-root stage/popover drift.\n  const style = document.createElement(\"style\");\n  style.id = DRIVER_STYLE_ID;\n  style.textContent = `\n    .driver-overlay { z-index: 2147483645 !important; }\n    .driver-stage { z-index: 2147483646 !important; }\n    .driver-popover.ScreenityOnboardingPopover,\n    .ScreenityOnboardingPopover {\n      z-index: 2147483647 !important;\n      border-radius: 30px !important;\n      max-width: 340px !important;\n      background: var(--color-background, #f9fafb) !important;\n      color: var(--color-text-primary, #1f2430) !important;\n      font-family: \"Satoshi-Medium\", sans-serif !important;\n      box-shadow: 0px 4px 30px rgba(30, 31, 37, 0.12) !important;\n      padding: 20px !important;\n      font-size: 14px !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-title,\n    .ScreenityOnboardingPopover .driver-popover-title {\n      font-size: 1rem !important;\n      font-family: \"Satoshi-Medium\", sans-serif !important;\n      font-weight: 500 !important;\n      margin-bottom: 12px !important;\n      color: var(--color-text-primary, #1f2430) !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-description,\n    .ScreenityOnboardingPopover .driver-popover-description {\n      font-size: 14px !important;\n      font-family: \"Satoshi-Medium\", sans-serif !important;\n      font-weight: 500 !important;\n      color: var(--color-text-secondary, #667085) !important;\n      line-height: 1.5 !important;\n      margin-bottom: 18px !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-close-btn,\n    .ScreenityOnboardingPopover .driver-popover-close-btn {\n      display: none !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-description a,\n    .ScreenityOnboardingPopover .driver-popover-description a {\n      color: #3b82f6 !important;\n      text-decoration: none !important;\n      cursor: pointer !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-progress-text,\n    .ScreenityOnboardingPopover .driver-popover-progress-text {\n      font-size: 12px !important;\n      font-family: \"Satoshi-Medium\", sans-serif !important;\n      color: var(--color-text-secondary, #667085) !important;\n      opacity: 0.7 !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns,\n    .ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns {\n      gap: 6px !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n    .ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n    .ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn {\n      border-radius: 30px !important;\n      padding: 10px 14px !important;\n      font-size: 14px !important;\n      text-shadow: none !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n    .ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn {\n      background-color: var(--color-primary, #3b82f6) !important;\n      color: white !important;\n      border: none !important;\n    }\n    .driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n    .ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn {\n      background-color: transparent !important;\n      color: var(--color-text-primary, #1f2430) !important;\n      border: 1px solid var(--color-border, #d0d5dd) !important;\n    }\n\t\t\t/* Modal step: hide stage cutout + center popover */\n.screenity-driver-modal-step .driver-stage,\n.screenity-driver-modal-step .driver-stage-wrapper {\n  display: none !important;\n}\n\n.screenity-driver-modal-step .driver-popover-arrow {\n  display: none !important;\n}\n\n.screenity-driver-modal-step .driver-popover.ScreenityOnboardingPopover,\n.screenity-driver-modal-step .ScreenityOnboardingPopover {\n  position: fixed !important;\n  top: 50% !important;\n  left: 50% !important;\n  transform: translate(-50%, -50%) !important;\n  margin: 0 !important;\n}\n\t.screenity-driver-modal-step .driver-popover.ScreenityOnboardingPopover,\n.screenity-driver-modal-step .ScreenityOnboardingPopover {\n  max-width: 420px !important;\n  padding: 26px !important;\n}\n\t/* Welcome icon */\n.screenity-driver-modal-step .screenity-welcome-emoji {\n  font-size: 24px;\n  line-height: 1;\n  margin-bottom: 10px;\n}\n  `;\n  document.head.appendChild(style);\n};\n\nconst lowerRootContainerZIndex = (root) => {\n  const container =\n    find(root, \"#screenity-root-container\") ||\n    root?.host ||\n    document.getElementById(\"screenity-root-container\");\n  if (!container) return () => {};\n  const prevValue = container.style.zIndex;\n  const prevPriority = container.style.getPropertyPriority(\"z-index\");\n  container.style.setProperty(\"z-index\", \"10000\", \"important\");\n  return () => {\n    if (prevValue) {\n      container.style.setProperty(\"z-index\", prevValue, prevPriority || \"\");\n    } else {\n      container.style.removeProperty(\"z-index\");\n    }\n  };\n};\n\nconst setToolbarCloseVisible = (root, visible) => {\n  if (!root?.querySelectorAll) return;\n  root.querySelectorAll(\".toolbar-controls\").forEach((node) => {\n    if (visible) node.classList.add(\"open\");\n    else node.classList.remove(\"open\");\n  });\n};\n\nconst buildWelcomeStep = (copy) => ({\n  id: STEP_IDS.WELCOME,\n  // No element => no cutout highlight (acts like a modal when centered in onPopoverRender)\n  popover: {\n    title: copy.welcomeTitle,\n    showButtons: [\"next\"],\n    description: copy.welcomeDescription,\n  },\n});\n\nconst buildToolbarStep = (element, copy) => ({\n  id: STEP_IDS.TOOLBAR,\n  element,\n  popover: {\n    title: copy.toolbarTitle,\n    side: \"top\",\n    align: \"center\",\n    showButtons: [\"previous\", \"next\"],\n    description: copy.toolbarDescription,\n  },\n});\n\nconst buildCameraStep = (element, copy) => ({\n  id: STEP_IDS.CAMERA,\n  element,\n  popover: {\n    title: copy.cameraTitle,\n    side: \"top\",\n    align: \"center\",\n    showButtons: [\"previous\", \"next\"],\n    description: copy.cameraDescription,\n  },\n});\n\nconst buildInstantStep = (element, copy) => ({\n  id: STEP_IDS.INSTANT,\n  element,\n  popover: {\n    title: copy.instantTitle,\n    side: \"left\",\n    align: \"center\",\n    showButtons: [\"previous\", \"next\"],\n    description: copy.instantDescription,\n  },\n});\n\nconst shouldMarkSeen = (abortReason) =>\n  abortReason == null || abortReason === \"dismissed\";\n\nconst destroyActive = (abortReason = \"external\") => {\n  if (!activeRun) return;\n  if (!activeRun.abortReason) activeRun.abortReason = abortReason;\n  if (!activeDriver) {\n    clearDriverUiState(activeRun.root);\n    if (typeof activeRun.stopObserver === \"function\") activeRun.stopObserver();\n    activeRun = null;\n    onboardingInProgress = false;\n    return;\n  }\n  try {\n    activeDriver.destroy();\n  } catch {}\n};\n\nconst setModalStep = (enabled) => {\n  document.documentElement.classList.toggle(\n    \"screenity-driver-modal-step\",\n    enabled,\n  );\n};\n\nconst startBlockingObserver = ({ root, getState }) => {\n  const observer = new MutationObserver(() => {\n    if (!activeDriver) return;\n    const runtimeState = getState?.() || {};\n    if (\n      hasConflictSelectorsVisible(root) ||\n      hasBlockingFlags(runtimeState) ||\n      document.hidden\n    ) {\n      destroyActive(\"conflict\");\n    }\n  });\n  observer.observe(document.documentElement, {\n    childList: true,\n    subtree: true,\n    attributes: true,\n  });\n  return () => observer.disconnect();\n};\n\nconst addInteractionCancelListeners = (root, onCancel) => {\n  const popup =\n    find(root, \"#pro-onboarding-popup-container\") ||\n    find(root, \".popup-container\");\n  const target = popup || document;\n  const handler = (event) => {\n    if (event.type === \"keydown\" && event.key === \"Tab\") return;\n    onCancel();\n  };\n  START_CANCEL_EVENTS.forEach((eventName) => {\n    target.addEventListener(eventName, handler, { capture: true });\n  });\n  return () => {\n    START_CANCEL_EVENTS.forEach((eventName) => {\n      target.removeEventListener(eventName, handler, { capture: true });\n    });\n  };\n};\n\nconst resolveTourSteps = ({\n  toolbarEl,\n  cameraEl,\n  instantEl,\n  copy,\n  includeCamera = false,\n}) => {\n  const steps = [buildWelcomeStep(copy)];\n\n  if (isElementVisible(toolbarEl)) steps.push(buildToolbarStep(toolbarEl, copy));\n  if (includeCamera && isElementVisible(cameraEl)) {\n    steps.push(buildCameraStep(cameraEl, copy));\n  }\n  if (isElementVisible(instantEl)) steps.push(buildInstantStep(instantEl, copy));\n\n  return steps;\n};\n\nconst startDriver = ({ steps, root, getState, onFinish, copy }) => {\n  if (!steps.length) return;\n  ensureDriverStyles();\n  restoreRootStyle = lowerRootContainerZIndex(root);\n  document.documentElement.classList.add(\"screenity-driver-active\");\n\n  activeRun = {\n    root,\n    cameraStepShown: false,\n    abortReason: null,\n    stopObserver: startBlockingObserver({ root, getState }),\n    onFinish,\n  };\n\n  const d = driver({\n    allowClose: true,\n    overlayClickBehavior: \"close\",\n    showProgress: true,\n    popoverOffset: 18,\n    stagePadding: 12,\n    popoverClass: POPOVER_CLASS,\n    doneBtnText: copy.doneBtnText,\n    nextBtnText: copy.nextBtnText,\n    prevBtnText: copy.prevBtnText,\n    steps,\n    onCloseClick: () => {\n      destroyActive(\"dismissed\");\n    },\n    onHighlightStarted: (element, step) => {\n      const isWelcome = step?.id === STEP_IDS.WELCOME;\n      setModalStep(isWelcome);\n\n      const isToolbarStep = step?.id === STEP_IDS.TOOLBAR;\n      setToolbarCloseVisible(root, isToolbarStep);\n    },\n    onHighlighted: (element, step, options) => {\n      if (step?.id === STEP_IDS.CAMERA && activeRun) {\n        activeRun.cameraStepShown = true;\n      }\n      logStepState(\"highlighted\", step, options);\n    },\n    onPopoverRender: (popover, options) => {\n      const step = options?.state?.activeStep || {};\n\n      if (step.id === STEP_IDS.WELCOME && popover?.title) {\n        // Only insert once\n        if (\n          !popover.title.parentElement?.querySelector(\n            \".screenity-welcome-emoji\",\n          )\n        ) {\n          const emoji = document.createElement(\"div\");\n          emoji.className = \"screenity-welcome-emoji\";\n          emoji.textContent = \"👋\";\n          popover.title.parentElement.insertBefore(emoji, popover.title);\n        }\n      }\n      if (\n        step.id === STEP_IDS.TOOLBAR &&\n        popover?.description &&\n        !popover.description.querySelector(\"a\")\n      ) {\n        const desc = popover.description;\n        desc.appendChild(document.createTextNode(\" \"));\n        const link = document.createElement(\"a\");\n        link.href = TOOLBAR_HELP_URL;\n        link.target = \"_blank\";\n        link.rel = \"noopener noreferrer\";\n        link.textContent = copy.learnMoreLabel;\n        desc.appendChild(link);\n      }\n\n      logStepState(\"popover_render\", options?.state?.activeStep, options);\n    },\n    onDestroyed: async (element, step, options) => {\n      const run = activeRun;\n      clearDriverUiState(root);\n      if (run && typeof run.stopObserver === \"function\") run.stopObserver();\n\n      activeDriver = null;\n      activeRun = null;\n      onboardingInProgress = false;\n\n      logStepState(\"destroyed\", step, options);\n      run?.onFinish?.({\n        cameraStepShown: Boolean(run?.cameraStepShown),\n        shouldMarkSeen: shouldMarkSeen(run?.abortReason),\n      });\n    },\n  });\n\n  activeDriver = d;\n  d.drive();\n  logStepState(\"drive_started\", steps[0], { state: d.getState?.() || {} });\n};\n\nexport const runProPopupOnboardingIfNeeded = async ({\n  rootContext = null,\n  isPro = false,\n  isLoggedIn = false,\n  popupOpen = false,\n  cameraEnabled = false,\n  pendingRecording = false,\n  preparingRecording = false,\n  recording = false,\n  countdownActive = false,\n  isCountdownVisible = false,\n  forceStart = false,\n} = {}) => {\n  const root = getRoot(rootContext);\n  const state = {\n    pendingRecording,\n    preparingRecording,\n    recording,\n    countdownActive,\n    isCountdownVisible,\n  };\n  const copy = getOnboardingText();\n\n  if (!popupOpen || !isPro || !isLoggedIn) {\n    clearScheduledStart();\n    if (activeDriver) destroyActive(\"popup-closed\");\n    return;\n  }\n\n  if (\n    shouldSkip({\n      root,\n      popupOpen,\n      isPro,\n      isLoggedIn,\n      state,\n    })\n  ) {\n    clearScheduledStart();\n    if (\n      activeDriver &&\n      (hasBlockingFlags(state) || hasConflictSelectorsVisible(root))\n    ) {\n      destroyActive(\"conflict\");\n    }\n    return;\n  }\n\n  if (await hasSeenOnboarding(CORE_KEY)) return;\n  if (pendingStartTimer || onboardingInProgress || activeDriver) return;\n\n  const startDelayMs = forceStart ? 0 : IDLE_START_DELAY_MS;\n  const token = ++pendingStartToken;\n  if (!forceStart) {\n    cancelPendingStartListeners = addInteractionCancelListeners(root, () => {\n      clearScheduledStart();\n    });\n  }\n\n  pendingStartTimer = setTimeout(async () => {\n    if (token !== pendingStartToken) return;\n    clearScheduledStart();\n\n    const latestSkip = shouldSkip({\n      root,\n      popupOpen,\n      isPro,\n      isLoggedIn,\n      state,\n    });\n    if (latestSkip) return;\n    if (await isRecordingNow()) return;\n    if (await hasSeenOnboarding(CORE_KEY)) return;\n\n    onboardingInProgress = true;\n\n    const toolbarEl = await waitForElement(root, TOOLBAR_SELECTORS, 900);\n    const cameraEl = await waitForElement(root, CAMERA_SELECTORS, 700);\n    const instantPrimaryEl = await waitForElement(\n      root,\n      INSTANT_PRIMARY_SELECTORS,\n      900,\n    );\n    const instantEl =\n      instantPrimaryEl ||\n      (await waitForElement(root, INSTANT_FALLBACK_SELECTORS, 250));\n\n    const includeCamera =\n      cameraEnabled &&\n      !(await hasSeenOnboarding(CAMERA_KEY)) &&\n      isElementVisible(cameraEl);\n\n    const steps = resolveTourSteps({\n      toolbarEl,\n      cameraEl,\n      instantEl,\n      copy,\n      includeCamera,\n    });\n\n    if (!steps.length) {\n      onboardingInProgress = false;\n      return;\n    }\n\n    startDriver({\n      steps,\n      root,\n      copy,\n      getState: () => state,\n      onFinish: async ({ cameraStepShown, shouldMarkSeen }) => {\n        if (!shouldMarkSeen) return;\n        await markOnboardingSeen(CORE_KEY);\n        if (includeCamera || cameraStepShown) {\n          await markOnboardingSeen(CAMERA_KEY);\n        }\n      },\n    });\n  }, startDelayMs);\n};\n\nexport const runProCameraOnboardingIfNeeded = async ({\n  rootContext = null,\n  isPro = false,\n  isLoggedIn = false,\n  popupOpen = false,\n  cameraEnabled = false,\n  pendingRecording = false,\n  preparingRecording = false,\n  recording = false,\n  countdownActive = false,\n  isCountdownVisible = false,\n} = {}) => {\n  const root = getRoot(rootContext);\n  const state = {\n    pendingRecording,\n    preparingRecording,\n    recording,\n    countdownActive,\n    isCountdownVisible,\n  };\n  const copy = getOnboardingText();\n\n  if (\n    shouldSkip({\n      root,\n      popupOpen,\n      isPro,\n      isLoggedIn,\n      state,\n    })\n  ) {\n    if (\n      activeDriver &&\n      (hasBlockingFlags(state) || hasConflictSelectorsVisible(root))\n    ) {\n      destroyActive(\"conflict\");\n    }\n    return;\n  }\n  if (!cameraEnabled) return;\n  if (await hasSeenOnboarding(CAMERA_KEY)) return;\n  if (pendingStartTimer || onboardingInProgress || activeDriver) return;\n\n  const cameraEl = await waitForElement(root, CAMERA_SELECTORS, 900);\n  if (!isElementVisible(cameraEl)) return;\n\n  setTimeout(() => {\n    if (\n      shouldSkip({\n        root,\n        popupOpen,\n        isPro,\n        isLoggedIn,\n        state,\n      })\n    ) {\n      return;\n    }\n    onboardingInProgress = true;\n    startDriver({\n      steps: [\n        {\n          id: STEP_IDS.CAMERA,\n          element: cameraEl,\n          popover: {\n            title: copy.cameraTitle,\n            side: \"top\",\n            align: \"center\",\n            showButtons: [\"next\"],\n            description: copy.cameraDescription,\n          },\n        },\n      ],\n      root,\n      copy,\n      getState: () => state,\n      onFinish: async ({ shouldMarkSeen }) => {\n        if (!shouldMarkSeen) return;\n        await markOnboardingSeen(CAMERA_KEY);\n      },\n    });\n  }, 260);\n};\n"
  },
  {
    "path": "src/pages/Content/popup/onboarding/storage.js",
    "content": "const ONBOARDING_SEEN_KEY = \"onboardingSeen\";\n\nexport const hasSeenOnboarding = async (key) => {\n  try {\n    const result = await chrome.storage.sync.get([ONBOARDING_SEEN_KEY]);\n    const seen = result?.[ONBOARDING_SEEN_KEY];\n    return Boolean(seen && typeof seen === \"object\" && seen[key] === true);\n  } catch {\n    return false;\n  }\n};\n\nexport const markOnboardingSeen = async (key) => {\n  try {\n    const result = await chrome.storage.sync.get([ONBOARDING_SEEN_KEY]);\n    const current =\n      result?.[ONBOARDING_SEEN_KEY] &&\n      typeof result[ONBOARDING_SEEN_KEY] === \"object\"\n        ? result[ONBOARDING_SEEN_KEY]\n        : {};\n    await chrome.storage.sync.set({\n      [ONBOARDING_SEEN_KEY]: {\n        ...current,\n        [key]: true,\n      },\n    });\n  } catch {}\n};\n\nexport const resetOnboardingSeen = async (keys = []) => {\n  try {\n    const result = await chrome.storage.sync.get([ONBOARDING_SEEN_KEY]);\n    const current =\n      result?.[ONBOARDING_SEEN_KEY] &&\n      typeof result[ONBOARDING_SEEN_KEY] === \"object\"\n        ? { ...result[ONBOARDING_SEEN_KEY] }\n        : {};\n\n    if (Array.isArray(keys) && keys.length > 0) {\n      keys.forEach((key) => {\n        delete current[key];\n      });\n    } else {\n      Object.keys(current).forEach((key) => {\n        delete current[key];\n      });\n    }\n\n    await chrome.storage.sync.set({\n      [ONBOARDING_SEEN_KEY]: current,\n    });\n  } catch {}\n};\n"
  },
  {
    "path": "src/pages/Content/popup/styles/_Popup.scss",
    "content": "@use \"../../styles/_variables\" as *;\n\n@use \"./layout/_PopupContainer.scss\";\n@use \"./layout/_Settings.scss\";\n@use \"./layout/_VideosTab.scss\";\n@use \"./layout/_Announcement.scss\";\n@use \"./layout/_Welcome\";\n@use \"./layout/_SettingsMenu.scss\";\n@use \"./components/_Dropdown.scss\";\n@use \"./components/_Tabs.scss\";\n@use \"./components/_Switch\";\n@use \"./components/_VideoItem\";\n@use \"./components/_MainButton\";\n@use \"./components/_BackgroundEffects\";\n@use \"./components/_RegionDimensions\";\n@use \"./components/_TimeSetter\";\n@use \"./components/_Tooltip\";\n"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_BackgroundEffects.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.background-effects-toggle-group {\n\tdisplay: flex;\n\theight: 40px;\n\twidth: 100%;\n\tgap: 8px;\n\tmargin-bottom: 8px;\n\tmargin-top: 8px;\n}\n.background-effect {\n\tdisplay: flex;\n\twidth: 40px;\n\theight: 40px;\n\talign-items: center;\n\tjustify-content: center;\n\tborder-radius: 30px;\n\tposition: relative;\n\tcolor: #FFF;\n\n\tspan {\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tfont-size: 12px;\n\t\tfont-weight: 600;\n\t\tz-index: 99999;\n\t\tfont-weight: 500;\n\t}\n\t&[data-state='on']::after {\n\t\tcontent: \"\";\n\t\tborder-radius: 50%;\n\t\tdisplay: block;\n\t\twidth: 46px;\n\t\theight: 46px;\n\t\tposition: absolute;\n\t\tborder: 2px solid $color-primary;\n\t\tbox-sizing: border-box;\n\t}\n\timg {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tborder-radius: 50%;\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t}\n\t&:hover:not([data-state='on']) {\n\t\tcursor: pointer;\n\t\t&::after {\n\t\t\tcontent: \"\";\n\t\tborder-radius: 50%;\n\t\tdisplay: block;\n\t\twidth: 46px;\n\t\theight: 46px;\n\t\tposition: absolute;\n\t\tborder: 2px solid $color-primary;\n\t\topacity: .5;\n\t\tbox-sizing: border-box;\n\t\t}\n\t}\n\t&:focus-visible {\n\t\tbox-shadow: $focus-border;\n\n\t}\n}"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_Dropdown.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SelectTrigger {\n  display: inline-flex;\n  align-items: center;\n  justify-content: space-between;\n  border-radius: $container-border-radius;\n  line-height: 1;\n  height: 44px;\n  gap: 5px;\n  background-color: $color-background;\n  color: $color-text-primary;\n  width: 100%;\n  box-sizing: border-box;\n  margin-top: $spacing-02;\n  margin-bottom: $spacing-02;\n}\n.SelectTrigger:hover {\n  box-shadow: $container-shadow-focus;\n  cursor: pointer;\n}\n.SelectTrigger:focus {\n  box-shadow: $focus-border !important;\n}\n.SelectTrigger[data-placeholder] {\n  color: var(--violet9);\n}\n.SelectTrigger[data-state=\"open\"] {\n  box-shadow: $focus-border;\n}\n\n.SelectValue {\n  text-align: left;\n  flex: 1;\n  display: block;\n  width: 100%;\n  height: 100%;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  box-sizing: border-box;\n\n  span {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    height: 100%;\n    line-height: 44px;\n  }\n}\n\n.SelectIconDrop,\n.SelectIconType {\n  text-align: center;\n}\n.SelectIconType {\n  padding-left: 6px;\n  padding-right: 0px;\n}\n.SelectIconDrop {\n  padding-right: $spacing-05;\n}\n.SelectTrigger[data-state=\"open\"] .SelectIconDrop img {\n  transform: rotate(180deg);\n}\n\n.SelectContent {\n  overflow: hidden;\n  z-index: $z-index-max;\n  width: var(--radix-select-trigger-width);\n  max-height: var(--radix-select-content-available-height);\n  font-family: $font-medium;\n  background-color: white;\n  border-radius: 15px;\n  margin-top: $spacing-02;\n  box-shadow: $container-shadow-focus;\n}\n.SelectItem {\n  font-size: $font-size-normal;\n  line-height: 1;\n  color: var(--violet11);\n  display: flex;\n  align-items: center;\n  height: 44px;\n  padding-left: $spacing-05;\n  padding-right: $spacing-05;\n  color: $color-text-primary;\n  position: relative;\n  user-select: none;\n}\n.SelectItem[data-disabled] {\n  color: $color-text-secondary;\n  pointer-events: none;\n}\n.SelectItem[data-highlighted] {\n  background: $color-light-grey;\n  outline: none !important;\n}\n.SelectItem:hover {\n  background: $color-light-grey;\n  cursor: pointer;\n}\n\n.SelectSeparator {\n  height: 1px;\n  background-color: $color-border;\n  width: calc(100% - $spacing-04 * 2);\n  margin: auto;\n  border-radius: $container-border-radius;\n  margin-top: $spacing-02;\n  margin-bottom: $spacing-02;\n}\n\n.SelectItemIndicator {\n  position: absolute;\n  right: $spacing-04;\n  width: 24px;\n  height: 24px;\n  background: $color-primary;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.SelectScrollButton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 25px;\n  background-color: white;\n  color: var(--violet11);\n  cursor: default;\n}\n\n.SelectOff {\n  background: $color-red-light;\n  color: $color-red;\n  padding-left: $spacing-04;\n  padding-right: $spacing-04;\n  padding-top: $spacing-03;\n  padding-bottom: $spacing-03;\n  margin-right: $spacing-02;\n  border-radius: $container-border-radius;\n  font-size: $font-size-small;\n  font-weight: $font-weight-bold;\n}\n.SelectIconButton {\n  border-radius: $container-border-radius;\n  position: relative;\n  padding: 8px;\n  &:hover {\n    background-color: $color-light-grey;\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_MainButton.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n\n.main-button {\n\twidth: 100%;\n\theight: 45px;\n\tborder-radius: $container-border-radius;\n\tdisplay: flex;\n\tflex-direction: row;\n\tjustify-content: center;\n\talign-items: center;\n\tposition: relative;\n\tbox-sizing: border-box;\n\n\n\t.main-button-label {\n\t\tcolor: $color-text-contrast;\n\t\ttext-align: center;\n\t\tvertical-align: middle;\n\t\talign-items: center;\n\t}\n\t.main-button-shortcut {\n\t\tposition: absolute;\n\t\tfont-size: $font-size-small;\n\t\tright: $spacing-05;\n\t\tcolor: $color-text-contrast;\n\t\topacity: .7;\n\t}\n\t&:hover {\n\t\tcursor: pointer;\n\t}\n\t&:disabled {\n\t\tcursor: not-allowed;\n\t\topacity: .5;\n\t}\n}\n.main-button:focus {\n\tbox-shadow: $focus-border!important;\n}\n\n@property --x {\n  syntax: '<percentage>';\n  inherits: false;\n  initial-value: 35.44%;\n}\n@property --y {\n  syntax: '<percentage>';\n  inherits: false;\n  initial-value: 0%;\n}\n\n.recording-button {\n\tmargin-top: $spacing-03;\n\tfilter: $gradient-shadow-primary;\n\tbackground:radial-gradient(127.41% 127.78% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n\tanimation: 0;\n\tanimation: background-size 6s ease-in-out infinite;\n\tanimation-play-state: paused;\n\tposition: relative;\n\tz-index: 2;\n}\n@keyframes background-size {\n\t/* Animate scale and position in and out looping */\n\t0% {\n\t\tbackground-size: 100% 100%;\n\t\tbackground-position: 0% 0%;\n\t}\n\t50% {\n\t\tbackground-size: 150% 150%;\n\t\tbackground-position: 100% 0%;\n\n\t}\n\t100% {\n\t\tbackground-size: 100% 100%;\n\t\tbackground-position: 0% 0%;\n\t}\n}\n\n.recording-button:hover {\n\tanimation-play-state: running!important;\n}\n.recording-button:before {\n\tcontent: \"\";\n\tposition: absolute;\n\tdisplay: block;\n\ttop: 0px;\n\tleft: 0px;\n\twidth: 100%;\n\theight: 100%;\n\tbox-sizing: border-box;\n\tborder-radius: $container-border-radius;\n\ttransition: all .25s ease-in-out;\n}\n.recording-button:hover:before {\n\tbox-shadow: 0px 0px 0px 4px rgba(52, 138, 247, 0.25);\n}\n@keyframes pulse-animation {\n\t0% {\n\t\tbox-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n\t}\n\t25% {\n\t\tbox-shadow: 0px 0px 0px 6px rgba(52, 138, 247, 0.25);\n\t}\n\t50% {\n\t\tbox-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n\t}\n\t100% {\n\t\tbox-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n\t}\n}\n@keyframes gradient-animation {\n\t0% {\n\t\t--x: 35.44%;\n\t\t--y: 0%;\n\t}\n\t25% {\n\t\t--x: 100%;\n\t\t--y: 30%;\n\t}\n\t50% {\n\t\t--x: 70%;\n\t\t--y: 100%;\n\t}\n\t75% {\n\t\t--x: 30%;\n\t\t--y: 90%;\n\t}\n\t100% {\n\t\t--x: 35.44%;\n\t\t--y: 0%;\n\t}\n}\n\n.dashboard-button {\n\tbackground: $color-text-primary;\n\tbox-shadow: 0px 0px 0px 0px rgba(41, 41, 47, 0.25);\n\ttransition: all .25s ease-in-out;\n}\n.dashboard-button:hover {\n\tbox-shadow: 0px 0px 0px 4px rgba(41, 41, 47, 0.25);\n}\n\n.alarm-time-button {\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tborder-radius: 15px;\n\tpadding: 4px 8px;\n\tposition: absolute;\n\tcolor: $color-text-contrast;\n\topacity: .7;\n\tfont-family: $font-medium;\n\tfont-size: 12px;\n\tleft: 6px;\n\n\tsvg {\n\t\tmargin-top: 4px;\n\t\tmargin-right: 4px;\n\t\twidth: 14px;\n\t}\n}"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_RegionDimensions.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.region-dimensions {\n\twidth: 100%;\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 10px;\n\tmargin-bottom: 10px;\n}\n.region-input {\n\tflex: 1;\n\tposition: relative;\n\n\tinput {\n\t\tcolor: $color-text-primary!important;\n\t\tborder-radius: $container-border-radius;\n\t\theight: 40px;\n\t\tbox-sizing: border-box;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\tpadding-left: 18px;\n\t\tpadding-right: 40px;\n\t\tfont-family: $font-medium;\n\t\tbackground-color: $color-background;\n\n\t\t&:focus-visible {\n\t\t\toutline: none;\n\t\t\tbox-shadow: $focus-border;\n\t\t}\n\t}\n\tspan {\n\t\tcolor: $color-text-secondary;\n\t\tfont-family: $font-medium;\n\t\tposition: absolute;\n\t\tright: 18px;\n\t\tbottom: 12px;\n\t\tuser-select: none;\n\t}\n}"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_Switch.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SwitchRow {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 40px;\n\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  -o-user-select: none;\n}\n.SwitchRow * {\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  -o-user-select: none;\n}\n\n.SwitchRoot {\n  width: 34px;\n  height: 22px;\n  background-color: $color-border;\n  border-radius: 9999px;\n  position: relative;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n.SwitchRoot[disabled] {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n.SwitchRoot:focus {\n  box-shadow: $focus-border;\n}\n.SwitchRoot[data-state=\"checked\"] {\n  background-color: $color-primary;\n}\n.SwitchRoot:hover {\n  cursor: pointer;\n}\n\n.SwitchThumb {\n  display: block;\n  width: 14px;\n  height: 14px;\n  background-color: white;\n  border-radius: 9999px;\n  box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1);\n  transition: transform 100ms;\n  transform: translateX(4px);\n  will-change: transform;\n}\n.SwitchThumb[data-state=\"checked\"] {\n  transform: translateX(16px);\n}\n\n.Label {\n  color: $color-text-secondary;\n  display: inline-block !important;\n  /* Ellipsis */\n  text-overflow: clip;\n  white-space: nowrap;\n\n  &:hover {\n    text-overflow: clip;\n  }\n}\n\n.ExperimentalLabel {\n  color: $color-text-contrast;\n  font-size: 12px;\n  background-color: $color-primary;\n  border-radius: 15px;\n  padding: 2px 8px;\n  display: inline-block !important;\n  margin-left: 8px;\n}\n\n.labelDropdownWrap {\n  display: inline-block;\n  vertical-align: middle;\n  position: relative;\n  border-radius: $container-border-radius;\n  box-sizing: border-box;\n\n  img {\n    display: inline-block;\n    margin-left: 6px;\n  }\n  .labelDropdown {\n    display: inline-block;\n  }\n  &:hover {\n    cursor: pointer;\n  }\n  &::after {\n    content: \"\";\n    display: block;\n    position: absolute;\n    top: 0px;\n    left: 0px;\n    width: 100%;\n    height: 100%;\n    border-radius: $container-border-radius;\n    border-left: 8px solid transparent;\n    border-right: 8px solid transparent;\n    border-top: 4px solid transparent;\n    border-bottom: 4px solid transparent;\n    margin-top: -4px;\n    margin-left: -8px;\n  }\n  &:hover::after {\n    border-color: #fff;\n  }\n  &:hover {\n    background-color: #fff;\n  }\n}\n.labelDropdownActive {\n  .labelDropdownContent {\n    display: block !important;\n  }\n\n  img {\n    transform: rotate(180deg);\n  }\n}\n.labelDropdownContent {\n  position: absolute;\n  background-color: $color-background;\n  min-width: 160px;\n  box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);\n  z-index: 9999999999;\n  border-radius: 15px;\n  padding: 8px 0px;\n  margin-top: 4px;\n  border: 1px solid $color-border;\n  display: none;\n\n  .labelDropdownContentItem {\n    color: $color-text-primary;\n    padding: 12px 16px;\n    text-decoration: none;\n    display: block;\n    &:hover {\n      background-color: $color-light-grey;\n      cursor: pointer;\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_Tabs.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n/* Radix tabs navigation */\n/* reset */\nbutton,\nfieldset,\ninput {\n  all: unset;\n}\n\n.TabsRoot {\n  width: 100%;\n  margin: auto;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n}\n.TabsList {\n  margin: auto;\n  flex-shrink: 0;\n  display: flex;\n  width: fit-content;\n  align-items: center;\n}\n.hiddenTabs {\n  display: none !important;\n  pointer-events: none !important;\n  user-select: none !important;\n  opacity: 0 !important;\n  height: 0 !important;\n  overflow: hidden !important;\n  transition: opacity 0.3s ease !important;\n}\n.TabsTrigger {\n  padding-left: 14px;\n  padding-right: 14px;\n  color: $color-text-secondary;\n  user-select: none;\n  cursor: pointer;\n}\n.TabsTrigger[data-state=\"active\"] {\n  color: $color-text-primary;\n}\n.TabsTrigger:focus-visible {\n  position: relative;\n  box-shadow: $focus-border !important;\n}\n.TabsTriggerSpacer {\n  height: 50px;\n  width: 1px;\n  background: $color-border;\n  flex-shrink: 0;\n  margin-left: 8px;\n  margin-right: 8px;\n}\n\n/* Content of the Radix tabs */\n.TabsContent {\n  width: 100%;\n  display: block;\n  height: 100%;\n  box-sizing: border-box;\n  flex: 1 1 auto;\n\n  &::after {\n    content: \"\";\n    display: block;\n    clear: both;\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    width: 100%;\n    height: 30px;\n    background: linear-gradient(\n      to bottom,\n      transparent 0%,\n      rgba(255, 255, 255, 0.5) 100%\n    );\n    pointer-events: none; /* Allow content behind the gradient to be clickable */\n  }\n}\n.TabsContent:focus {\n  outline: none;\n}\n.TabsContent:focus-visible {\n  box-shadow: $focus-inner-border;\n}\n.TabsContent[data-state=\"inactive\"] {\n  display: none;\n}\n\n/* Pill animation */\n.pill-anim {\n  position: absolute;\n  height: 32px;\n  top: 0px;\n  bottom: 0px;\n  margin-top: auto;\n  margin-bottom: auto;\n  border-radius: 30px;\n  background: $color-background;\n  box-shadow: $container-shadow-focus;\n  transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n}\n/*\n.TabsList[data-value=\"record\"] {\n\t.pill-anim {\n\t\tleft: $spacing-03;\n\t\twidth: 102px;\n\t}\n}\n.TabsList[data-value=\"dashboard\"] {\n\t.pill-anim {\n\t\tleft: 109px;\n\t\twidth: 132px;\n\t}\n}\n*/\n\n/* Specific to the top level tabs */\n.TabsRoot.tl {\n  height: calc(100% - 40px);\n  margin-top: 40px;\n}\n.TabsList.tl {\n  border-radius: 30px;\n  background: $color-light-grey;\n  padding: 6px;\n  font-family: $font-bold;\n  position: relative;\n}\n.TabsTrigger.tl {\n  border-radius: 30px;\n  background: transparent;\n  height: 32px;\n  display: flex;\n  align-items: center;\n  padding-left: 16px;\n  padding-right: 17px;\n  z-index: 2;\n  position: relative;\n}\n.TabsTrigger.tl[data-state=\"inactive\"]:hover :before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  box-sizing: border-box;\n  height: 100%;\n  width: calc(100% - 10px);\n  margin-left: 5px;\n  background: #edeef3;\n  z-index: -2;\n  left: 0px;\n  border-radius: $container-border-radius;\n}\n.TabsTriggerIcon {\n  width: 20px;\n  height: 20px;\n  text-align: center;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  margin-right: $spacing-02;\n}\n\n/* Specific to recording tab context */\n.recording-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  .TabsRoot {\n    margin-top: $spacing-03;\n  }\n  .TabsList {\n    width: 100%;\n    border-bottom: $container-border;\n    margin: auto;\n    justify-content: center;\n  }\n  .TabsTrigger {\n    padding-top: $spacing-03;\n    padding-bottom: $spacing-04;\n    box-sizing: border-box;\n    position: relative;\n    display: block;\n    padding-left: 16px;\n    padding-right: 16px;\n  }\n  .TabsTrigger:hover {\n    background: $color-light-grey;\n    border-top-right-radius: 15px;\n    border-top-left-radius: 15px;\n  }\n  .TabsTrigger:focus-visible {\n    border-radius: 10px 10px 0px 0px !important;\n  }\n  .TabsTrigger[data-state=\"active\"]::after {\n    content: \"\";\n    display: block;\n    position: absolute;\n    width: 80%;\n    left: 0px;\n    right: 0px;\n    bottom: 0px;\n    margin: auto;\n    height: 2px;\n    border-radius: 30px;\n    background: $color-primary;\n  }\n  .TabsTrigger[data-state=\"active\"] > .TabsTriggerLabel {\n    color: $color-text-primary !important;\n  }\n\n  .TabsTriggerLabel {\n    text-align: center;\n  }\n  .TabsTriggerIcon {\n    width: 20px;\n    height: 20px;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n    margin: auto;\n    margin-bottom: $spacing-03;\n    border-radius: 30px;\n  }\n  .TabsContent {\n    background: $color-light-grey;\n    padding: $spacing-05;\n    border-bottom-left-radius: $container-border-radius;\n    border-bottom-right-radius: $container-border-radius;\n    max-height: calc(95vh - 200px);\n    overflow-y: overlay;\n  }\n  span {\n    display: block;\n  }\n}\n\n.video-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  .TabsRoot {\n    margin-top: $spacing-03;\n  }\n  .TabsList {\n    width: 100%;\n    border-bottom: $container-border;\n    margin: auto;\n    justify-content: space-between;\n    padding-left: $spacing-04;\n    padding-right: $spacing-04;\n    box-sizing: border-box;\n  }\n  .TabsTriggerWrap {\n    display: flex !important;\n    align-items: center;\n    flex-direction: row;\n    justify-content: left;\n    position: relative;\n    display: block;\n    box-sizing: border-box;\n  }\n  .TabsTrigger {\n    padding-top: $spacing-03;\n    padding-bottom: $spacing-04;\n    box-sizing: border-box;\n    position: relative;\n    display: block;\n    padding-left: 20px;\n    padding-right: 20px;\n  }\n  .TabsTrigger:hover {\n    background: $color-light-grey;\n    border-top-right-radius: 15px;\n    border-top-left-radius: 15px;\n  }\n  .TabsTrigger:focus-visible {\n    border-radius: 10px 10px 0px 0px !important;\n  }\n  .TabsTrigger[data-state=\"active\"]::after {\n    content: \"\";\n    display: block;\n    position: absolute;\n    width: 80%;\n    left: 0px;\n    right: 0px;\n    bottom: 0px;\n    margin: auto;\n    height: 2px;\n    border-radius: 30px;\n    background: $color-primary;\n  }\n  .TabsTrigger[data-state=\"active\"] > .TabsTriggerLabel {\n    color: $color-text-primary !important;\n  }\n  .TabsTriggerLabel {\n    text-align: center;\n  }\n\n  .TabsContent {\n    background: $color-light-grey;\n    border-bottom-left-radius: $container-border-radius;\n    border-bottom-right-radius: $container-border-radius;\n  }\n  span {\n    display: block;\n  }\n\n  .TabsSort {\n    margin-right: $spacing-04;\n    border-radius: $container-border-radius;\n    padding-left: $spacing-03;\n    padding-right: $spacing-03;\n    padding-top: $spacing-03;\n    padding-bottom: $spacing-03;\n    margin-bottom: 5px;\n  }\n  .TabsSort:hover {\n    cursor: pointer;\n    background: $color-light-grey;\n  }\n  .TabsSortLabel {\n    display: flex;\n    flex-direction: row;\n    justify-content: right;\n    align-items: center;\n    color: $color-text-secondary;\n    white-space: nowrap;\n  }\n  .TabsSortLabel img {\n    margin-left: $spacing-03;\n  }\n  .TabsSort:focus-visible {\n    box-shadow: $focus-border;\n    outline: none !important;\n  }\n}\n\n.projectActiveBanner {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n\n  background-color: #29292f; /* dark background */\n  color: white;\n  border-radius: 100px; /* pill shape */\n  padding: 10px 16px;\n  font-weight: 600;\n  font-size: 15px;\n  line-height: 1.3;\n  max-width: 80%;\n  user-select: none;\n  cursor: default;\n  gap: 16px;\n  top: 41px;\n  z-index: 999999;\n  margin: auto;\n  left: 0;\n  right: 0;\n  position: absolute;\n}\n\n.projectActiveBannerLeft {\n  /* text container */\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.projectActiveBannerRight {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.projectActiveBannerDivider {\n  width: 1px;\n  height: 24px;\n  background-color: #44444d; /* subtle divider color */\n}\n\n.projectActiveBannerClose {\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.projectActiveBannerClose img {\n  width: 14px;\n  height: 14px;\n  opacity: 0.6;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_TimeSetter.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.time-set-parent {\n\twidth: 100%;\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin-top: 10px;\n\tmargin-bottom: 10px;\n}\n.time-set-input {\n\tflex: 1;\n\tposition: relative;\n\n\tinput {\n\t\tborder-radius: $container-border-radius;\n\t\theight: 40px;\n\t\tbox-sizing: border-box;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\tpadding-left: 18px;\n\t\tpadding-right: 40px;\n\t\tfont-family: $font-medium;\n\t\tbackground-color: $color-background;\n\t\t-webkit-appearance: textfield;\n     -moz-appearance: textfield;\n          appearance: textfield;\n\n\t\t\t\t\t&:focus-visible {\n\t\t\t\t\t\toutline: none;\n\t\t\t\t\t\tbox-shadow: $focus-border;\n\t\t\t\t\t}\n\n\t}\n\tspan {\n\t\tcolor: $color-text-secondary;\n\t\tfont-family: $font-medium;\n\t\tposition: absolute;\n\t\tright: 18px;\n\t\tbottom: 12px;\n\t\tuser-select: none;\n\t}\n}"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_Tooltip.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.TooltipContent {\n  border-radius: $container-border-radius;\n  background-color: $color-text-primary;\n  padding: 10px 15px;\n  font-size: 12px;\n  line-height: 1;\n  font-family: $font-medium;\n  z-index: 99999999 !important;\n  color: $color-text-contrast;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  user-select: none;\n  transition: opacity 0.3 ease-in-out !important;\n  will-change: transform, opacity;\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.hide-tooltip {\n  display: none !important;\n}\n\n.tooltip-tall {\n  margin-bottom: 20px;\n}\n\n.tooltip-small {\n  margin-bottom: 5px;\n}\n\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"top\"] {\n  animation-name: slideDownAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"right\"] {\n  animation-name: slideLeftAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"bottom\"] {\n  animation-name: slideUpAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"left\"] {\n  animation-name: slideRightAndFade;\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n#screenity-ui [data-radix-popper-content-wrapper] {\n  z-index: $z-index-max !important;\n}\n\n.override {\n  display: none !important;\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/components/_VideoItem.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.video-item-root {\n  width: calc(100% - 2 * #{$spacing-03});\n  border-radius: 15px;\n  padding: $spacing-03;\n  display: block;\n  .video-item {\n    width: 100%;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    justify-content: space-between;\n  }\n  .video-item-left {\n    display: flex;\n    flex-grow: 1;\n    flex-direction: row;\n    align-items: center;\n    min-width: 0;\n    justify-content: left;\n  }\n  .video-item-thumbnail {\n    min-width: 48px;\n    width: 48px;\n    height: 38px;\n    background: grey;\n    border-radius: 5px;\n    margin-right: $spacing-04;\n  }\n  .video-item-info {\n    display: block;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    padding-right: $spacing-02;\n  }\n  .video-item-info-title {\n    color: $color-text-primary;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n  .video-item-info-date {\n    margin-top: $spacing-02;\n    color: $color-text-secondary;\n    font-size: $font-size-small;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n}\n\n.video-item-root:hover {\n  cursor: pointer;\n  background: #e9eaee;\n}\n.video-item-root:focus-visible {\n  box-shadow: $focus-border;\n  outline: none !important;\n}\n\n/* Actions */\n.video-item-right {\n  display: flex;\n  align-items: center;\n  justify-content: right;\n  gap: $spacing-03;\n  min-width: max-content;\n  opacity: 0;\n\n  .copy-link {\n    background: $color-background;\n    height: 32px;\n    width: 32px;\n    border-radius: 10px;\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n    z-index: 999999;\n  }\n  .copy-link:focus-visible {\n    box-shadow: $focus-border;\n    outline: none !important;\n  }\n  .more-actions {\n    height: 32px;\n    width: 32px;\n    background: $color-background;\n    text-align: center;\n    border-radius: 10px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n  .more-actions:focus-visible {\n    box-shadow: $focus-border;\n    outline: none !important;\n  }\n}\n\n.video-item-root:hover,\n.video-item-root:focus-visible {\n  .video-item-right {\n    opacity: 1;\n  }\n}\n\n// .video-item-right:focus-within {\n//   opacity: 1;\n// }\n"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_Announcement.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.announcement {\n  width: 100%;\n  top: 0px;\n  left: 0px;\n  padding-bottom: 24px;\n}\n.announcement-wrap {\n  width: 85%;\n  margin: auto;\n}\n.announcement-hero {\n  width: 100%;\n  margin-bottom: 16px;\n  margin-top: 44px;\n\n  img {\n    width: 100%;\n    border-radius: 15px;\n  }\n}\n.announcement-details {\n  text-align: center;\n}\n.announcement-title {\n  font-family: $font-bold;\n  font-size: 16px;\n  color: $color-text-primary;\n  letter-spacing: -0.8px;\n  margin-bottom: 12px;\n  text-align: center;\n}\n.announcement-description {\n  font-family: $font-medium;\n  font-size: 14px;\n  color: $color-text-secondary;\n  text-align: center;\n  margin-bottom: 24px;\n  line-height: 1.6;\n  letter-spacing: -0.6px;\n\n  a {\n    text-decoration: none !important;\n    color: $color-primary !important;\n    cursor: pointer !important;\n  }\n}\n.announcement-cta {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  width: 100%;\n  height: 44px;\n  border-radius: $container-border-radius;\n  background: $gradient-primary;\n  color: $color-text-contrast;\n  font-family: $font-medium;\n  font-size: 14px;\n  cursor: pointer;\n  transition: all 0.2s ease-in-out;\n  animation: background-size 6s ease-in-out infinite;\n  animation-play-state: paused;\n  filter: $gradient-shadow-primary;\n\n  &:hover {\n    animation-play-state: running !important;\n  }\n}\n@keyframes background-size {\n  /* Animate scale and position in and out looping */\n  0% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n  50% {\n    background-size: 150% 150%;\n    background-position: 100% 0%;\n  }\n  100% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_PopupContainer.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.popup-container:hover .popup-controls {\n  opacity: 1;\n}\n.open {\n  opacity: 1 !important;\n}\n.popup-drag-head {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100px;\n  z-index: 1;\n  border-radius: 30px 30px 0px 0px;\n  opacity: 0;\n}\n.popup-controls {\n  opacity: 0;\n  position: absolute;\n  top: -10px;\n  right: -10px;\n  box-sizing: border-box;\n  border-radius: $container-border-radius;\n  border: 1px solid $color-border;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  z-index: 999999999;\n  background: rgba(240, 238, 238, 1);\n  backdrop-filter: blur(10px);\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 6px;\n  padding-bottom: 6px;\n  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n\n  .popup-control {\n    svg {\n      color: $color-icon;\n      margin-bottom: -2px;\n    }\n  }\n  .popup-grab {\n    cursor: grab;\n  }\n\n  .popup-close {\n    cursor: pointer;\n  }\n}\n\n.tempimg {\n  height: 100%;\n  opacity: 1;\n  position: fixed;\n  right: 0px;\n  top: 20px;\n}\n\n.container {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  z-index: 999999999;\n  font-family: $font-medium;\n  font-size: $font-size-normal;\n}\n.ToolbarDragging .popup-container {\n  transform: scale(1.02);\n\n  filter: drop-shadow(0px 20px 50px rgba(0, 0, 0, 0.4)) !important;\n}\n\n/* Recording popup parent */\n.popup-container {\n  width: 356px;\n  position: fixed;\n  top: 32px;\n  right: 28px;\n  z-index: $z-index-max;\n  filter: $container-shadow;\n  pointer-events: all;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96),\n    filter 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n}\n.popup-container::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  transition: 2s;\n  background: $color-background;\n  background-clip: content-box;\n  -webkit-mask-image: radial-gradient(\n    circle at center top,\n    transparent 31px,\n    #000 31px\n  );\n  mask-image: radial-gradient(\n    circle at center top,\n    transparent 31px,\n    #000 31px\n  );\n  background-position: center bottom 50px;\n  border-radius: $container-border-radius;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n}\n\n/*\n.popup-shape {\n\twidth: 100%;\n\theight: 100%;\n\tbackground: $color-background;\n\tbackground-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  background-position: center bottom 50px;\n\tborder-radius: $container-border-radius;\n\tposition: relative;\n}\n*/\n\n.popup-cutout {\n  width: 44px;\n  height: 44px;\n  border-radius: 50%;\n  text-align: center;\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  top: -22px;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n}\n.popup-cutout img {\n  text-align: center;\n  margin: auto;\n  display: inline-block;\n  width: 100%;\n  border-radius: 50%;\n}\n\n/* Recording nav area */\n.popup-nav {\n  width: 100%;\n  position: relative;\n}\n\n/* Recording content area */\n.popup-content {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  border-radius: $container-border-radius;\n  overflow: hidden;\n}\n\n.waveform {\n  width: 100%;\n  margin-top: $spacing-04;\n  margin-bottom: $spacing-04;\n}\n\n.popup-content-divider {\n  width: 100%;\n  height: 1px;\n  background: $color-border;\n  margin-top: $spacing-04;\n  margin-bottom: $spacing-04;\n}\n\n.popup-warning {\n  display: flex;\n  width: calc(100% + 32px);\n  height: 80px;\n  justify-content: space-between;\n  align-items: center;\n  position: relative;\n  overflow: hidden;\n  background-color: rgba(56, 126, 247, 0.1);\n  margin-left: -16px;\n  margin-top: -16px;\n  margin-bottom: 8px;\n\n  .popup-warning-right {\n    color: $color-primary;\n    font-family: $font-medium;\n    width: 90px;\n  }\n}\n.popup-warning-left,\n.popup-warning-right {\n  width: 50px;\n  display: flex;\n  align-items: center;\n  text-align: center;\n  height: 100%;\n  justify-content: center;\n\n  svg {\n    color: $color-primary;\n  }\n}\n.popup-warning-right {\n  cursor: pointer;\n}\n.popup-warning-middle {\n  flex: 1;\n\n  .popup-warning-title {\n    font-family: $font-bold;\n    color: $color-text-primary;\n  }\n  .popup-warning-description {\n    font-family: $font-medium;\n    color: $color-text-secondary;\n    margin-top: 4px;\n  }\n}\n.permission-button {\n  background: rgba(48, 128, 248, 0.1);\n  border-radius: $container-border-radius;\n  color: $color-primary;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 44px;\n  gap: 8px;\n  margin-top: $spacing-03;\n  margin-bottom: $spacing-03;\n\n  &:first-child {\n    margin-top: $spacing-02 !important;\n  }\n  &:last-child {\n    margin-bottom: $spacing-02 !important;\n  }\n  &:hover {\n    background: rgba(48, 128, 248, 0.15);\n    cursor: pointer;\n  }\n\n  svg {\n    color: $color-primary;\n  }\n}\n\n.HelpSection {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  bottom: -40px;\n  background: #edeef2;\n  border-radius: 30px;\n  padding: 4px 12px;\n  text-align: center;\n  font-family: $font-medium;\n  color: $color-text-secondary;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  width: fit-content;\n\n  .HelpIcon {\n    margin-top: 3px;\n  }\n\n  &:hover {\n    cursor: pointer;\n    background: #fefeff;\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_Settings.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.CollapsibleTrigger {\n\tmargin-top: $spacing-04;\n\tfont-weight: $font-bold;\n\tpadding-top: $spacing-02;\n\tpadding-bottom: $spacing-02;\n\tpadding-left: $spacing-04;\n\tpadding-right: $spacing-04;\n\tmargin-left: auto;\n\tmargin-right: auto;\n\ttext-align: center;\n\tdisplay: block;\n\tborder-radius: $container-border-radius;\n}\n.CollapsibleTrigger:focus-visible {\n\tbox-shadow: $focus-border;\n}\n.CollapsibleLabel {\n\tcolor: $color-text-secondary;\n\tfont-weight: $font-bold;\n\ttext-align: center;\n\tdisplay: inline-block;\n}\n.CollapsibleLabel img {\n\tmargin-left: 4px;\n}\n.CollapsibleTrigger:hover {\n\tcursor: pointer;\n\tbackground: #FFF;\n}\n.CollapsibleRoot[data-state='open'] > .CollapsibleTrigger > .CollapsibleLabel img {\n\ttransform: scaleY(-1);\n\tmargin-bottom: 2px;\n}"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_SettingsMenu.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.DropdownMenuContent,\n.DropdownMenuSubContent {\n  min-width: 200px;\n  background-color: white;\n  margin-top: 4px;\n  margin-right: 8px;\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-radius: 15px;\n  z-index: 99999;\n  font-family: $font-medium;\n  color: $color-text-primary;\n  box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),\n    0px 10px 20px -15px rgba(22, 23, 24, 0.2);\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n.DropdownMenuContent[data-side=\"top\"],\n.DropdownMenuSubContent[data-side=\"top\"] {\n  animation-name: slideDownAndFade;\n}\n.DropdownMenuContent[data-side=\"right\"],\n.DropdownMenuSubContent[data-side=\"right\"] {\n  animation-name: slideLeftAndFade;\n}\n.DropdownMenuContent[data-side=\"bottom\"],\n.DropdownMenuSubContent[data-side=\"bottom\"] {\n  animation-name: slideUpAndFade;\n}\n.DropdownMenuContent[data-side=\"left\"],\n.DropdownMenuSubContent[data-side=\"left\"] {\n  animation-name: slideRightAndFade;\n}\n.ItemIndicator,\n.ItemIndicatorArrow {\n  position: absolute;\n  right: $spacing-04;\n  width: 18px;\n  height: 18px;\n  background: $color-primary;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n.ItemIndicatorArrow {\n  background: transparent !important;\n}\n\n.DropdownMenuItem,\n.DropdownMenuCheckboxItem,\n.DropdownMenuRadioItem,\n.DropdownMenuSubTrigger {\n  font-size: 14px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  height: 40px;\n  padding: 0 5px;\n  position: relative;\n  padding-left: 22px;\n  padding-right: 22px;\n  user-select: none;\n  outline: none;\n\n  &:hover {\n    background-color: $color-light-grey !important;\n    cursor: pointer;\n  }\n}\n.DropdownMenuSubTrigger[data-state=\"open\"] {\n  background-color: var(--violet-4);\n  color: var(--violet-11);\n}\n.DropdownMenuItem[data-disabled],\n.DropdownMenuCheckboxItem[data-disabled],\n.DropdownMenuRadioItem[data-disabled],\n.DropdownMenuSubTrigger[data-disabled] {\n  color: $color-text-secondary !important;\n  cursor: not-allowed;\n  background-color: $color-light-grey !important;\n}\n.DropdownMenuItem[data-highlighted],\n.DropdownMenuCheckboxItem[data-highlighted],\n.DropdownMenuRadioItem[data-highlighted],\n.DropdownMenuSubTrigger[data-highlighted] {\n  background-color: var(--violet-9);\n  color: var(--violet-1);\n}\n\n.DropdownMenuLabel {\n  padding-left: 25px;\n  font-size: 12px;\n  line-height: 25px;\n  color: var(--mauve-11);\n}\n\n.DropdownMenuSeparator {\n  height: 1px;\n  background-color: var(--violet-6);\n  margin: 5px;\n}\n\n.DropdownMenuItemIndicator {\n  position: absolute;\n  left: 0;\n  width: 25px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.DropdownMenuArrow {\n  fill: white;\n}\n\n.IconButton {\n  font-family: inherit;\n  border-radius: 100%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  margin-top: 4px;\n  cursor: pointer;\n\n  svg {\n    color: #9797a4;\n  }\n}\n.IconButton:hover {\n  background-color: var(--violet-3);\n}\n.IconButton:focus {\n}\n\n.RightSlot {\n  margin-left: auto;\n  padding-left: 20px;\n  color: var(--mauve-11);\n}\n[data-highlighted] > .RightSlot {\n  color: white;\n}\n[data-disabled] .RightSlot {\n  color: var(--mauve-8);\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_VideosTab.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.video-ui {\n  position: relative;\n  /* Blur the background */\n  // &:before {\n  // \tcontent: '';\n  // \tposition: absolute;\n  // \tbackdrop-filter: blur(5px);\n  // \ttop: 0px;\n  // \tleft: 0px;\n  // \twidth: 100%;\n  // \theight: 100%;\n  // \tbackground: rgba(255, 255, 255, 0.5);\n  // \tz-index: 999;\n  // }\n}\n.blurred {\n  /* Blur the background */\n  &:before {\n    content: \"\";\n    position: absolute;\n    backdrop-filter: blur(5px);\n    top: 0px;\n    left: 0px;\n    width: 100%;\n    height: 100%;\n    background: rgba(255, 255, 255, 0.5);\n    z-index: 999;\n  }\n}\n.videos-list {\n  padding-bottom: 10px !important;\n  max-height: calc(96vh - 260px);\n  overflow-y: overlay;\n  padding: $spacing-05;\n}\n.bottom-section {\n  width: 100%;\n  bottom: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  padding-top: $spacing-03 !important;\n  padding-bottom: $spacing-04 !important;\n  z-index: 999999;\n  padding: $spacing-05;\n}\n.ModalSoon {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: fit-content;\n  width: 70%;\n  padding: 20px $spacing-05;\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: $container-border-radius;\n  background: white;\n  box-shadow: 0px 4px 100px 0px rgba(0, 0, 0, 0.15);\n  z-index: 9999999;\n}\n.strong {\n  box-shadow: 0px 4px 100px -20px rgba(0, 0, 0, 0.75) !important;\n}\n.ModalSoonEmoji {\n  font-size: 20px;\n  margin-bottom: $spacing-03;\n}\n.ModalSoonTitle {\n  font-weight: 700;\n  color: $color-text-primary;\n  margin-bottom: $spacing-03;\n  text-align: center;\n}\n.ModalSoonDescription {\n  text-align: center;\n  color: $color-text-secondary;\n  text-align: center;\n  margin-bottom: $spacing-03;\n}\n.ModalSoonButton {\n  margin-top: $spacing-03;\n  color: $color-text-contrast;\n  text-align: center;\n  border-radius: 30px;\n  background: $gradient-primary;\n  padding: $spacing-03 $spacing-05;\n\n  &:hover {\n    cursor: pointer;\n  }\n}\n.spinner-container,\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 60px 0;\n  color: #6e7684;\n  font-size: 14px;\n}\n\n.spinner {\n  width: 32px;\n  height: 32px;\n  border: 3px solid rgba(0, 0, 0, 0.1);\n  border-top-color: #6e7684;\n  border-radius: 50%;\n  animation: spin 0.8s linear infinite;\n  margin-bottom: 10px;\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/popup/styles/layout/_Welcome.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.welcome-title {\n  font-family: $font-bold;\n  font-size: 18px;\n  color: $color-text-primary;\n  letter-spacing: -0.8px;\n  margin-bottom: 12px;\n  text-align: center;\n}\n.welcome-description {\n  font-family: $font-medium;\n  font-size: 16px;\n  color: $color-text-secondary;\n  text-align: center;\n  margin-bottom: 20px;\n  line-height: 1.6;\n  letter-spacing: -0.6px;\n\n  a {\n    text-decoration: none !important;\n    color: $color-primary !important;\n    cursor: pointer !important;\n  }\n}\n.welcome-cta {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  width: 100%;\n  height: 44px;\n  border-radius: $container-border-radius;\n  background: white;\n  border: 1px solid $color-border;\n  color: $color-text-primary;\n  font-family: $font-medium;\n  font-size: 14px;\n  cursor: pointer;\n  transition: all 0.2s ease-in-out;\n  filter: drop-shadow(0px 1px 2px rgba(53, 87, 98, 0.1));\n}\n.welcome-content {\n  background: $color-light-grey;\n  border-bottom-left-radius: $container-border-radius;\n  border-bottom-right-radius: $container-border-radius;\n  max-height: calc(95vh - 300px);\n  overflow-y: overlay;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  border-top: 1px solid #e8e8e8;\n}\n.welcome-content-wrap {\n  width: 85%;\n  margin: auto;\n}\n.welcome-content-title {\n  font-family: $font-bold;\n  font-size: 14px;\n  color: $color-text-primary;\n  margin-bottom: 8px;\n  margin-top: 8px;\n  text-align: center;\n  line-height: 1.4;\n}\n.welcome-feature-list {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 12px;\n}\n.welcome-feature-item {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n.welcome-feature-icon {\n  width: 20px;\n  height: 20px;\n  background: rgba(48, 128, 248, 0.1);\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.welcome-feature-icon-text {\n  font-size: 14px;\n  color: $color-text-secondary;\n  font-family: $font-medium;\n}\n.welcome-video {\n  margin: 25px 0 !important;\n  width: 100%;\n}\n.video-wrapper {\n  border-radius: 10px;\n}\n.welcome-support {\n  font-family: $font-medium;\n  font-size: 12px;\n  color: $color-text-secondary !important;\n  text-align: center;\n  margin-top: 20px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  text-decoration: none !important;\n\n  img {\n    width: 20px;\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/region/Region.jsx",
    "content": "import React, { useRef, useContext, useEffect } from \"react\";\nimport { Rnd } from \"react-rnd\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst ResizableBox = () => {\n  const regionRef = useRef(null);\n  const parentRef = useRef(null);\n  const cropRef = useRef(null);\n  const recordingRef = useRef(null);\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  useEffect(() => {\n    recordingRef.current = contentState.recording;\n  }, [contentState.recording]);\n\n  // Check for contentState.regionDimensions to update the Rnd component width and height\n  useEffect(() => {\n    if (contentState.recordingType != \"region\") return;\n    if (!contentState.customRegion) return;\n    if (regionRef.current === null) return;\n    if (\n      contentState.regionWidth === 0 ||\n      contentState.regionWidth === undefined\n    )\n      return;\n    if (\n      contentState.regionHeight === 0 ||\n      contentState.regionHeight === undefined\n    )\n      return;\n    if (contentState.regionX === undefined) return;\n    if (contentState.regionY === undefined) return;\n    if (contentState.fromRegion) return;\n\n    // Get parent element dimensions\n    const parentWidth = parentRef.current.offsetWidth;\n    const parentHeight = parentRef.current.offsetHeight;\n\n    // Calculate maximum size that fits within parent element\n    const maxWidth = parentWidth - contentState.regionX;\n    const maxHeight = parentHeight - contentState.regionY;\n    const newWidth = Math.min(contentState.regionWidth, maxWidth);\n    const newHeight = Math.min(contentState.regionHeight, maxHeight);\n\n    // Update content state with new size\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionWidth: newWidth,\n      regionHeight: newHeight,\n      fromRegion: true,\n    }));\n\n    chrome.storage.local.set({\n      regionWidth: newWidth,\n      regionHeight: newHeight,\n    });\n\n    regionRef.current.updateSize({\n      width: newWidth,\n      height: newHeight,\n      x: contentState.regionX,\n      y: contentState.regionY,\n    });\n    setCropTarget();\n  }, [\n    contentState.recordingType,\n    contentState.customRegion,\n    contentState.regionWidth,\n    contentState.regionHeight,\n    contentState.regionX,\n    contentState.regionY,\n  ]);\n\n  const setCropTarget = async () => {\n    const target = await CropTarget.fromElement(cropRef.current);\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      cropTarget: target,\n    }));\n  };\n\n  const handleResize = (e, direction, ref, delta, position) => {\n    // Get numeric values of width and height\n    const width = parseInt(ref.style.width, 10);\n    const height = parseInt(ref.style.height, 10);\n\n    // Update content state\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionWidth: width,\n      regionHeight: height,\n      regionX: position.x,\n      regionY: position.y,\n      fromRegion: true,\n    }));\n\n    chrome.storage.local.set({\n      regionWidth: width,\n      regionHeight: height,\n      regionX: position.x,\n      regionY: position.y,\n    });\n    setCropTarget();\n  };\n\n  const handleMove = (e, d) => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      regionX: d.x,\n      regionY: d.y,\n      fromRegion: true,\n    }));\n    chrome.storage.local.set({\n      regionX: d.x,\n      regionY: d.y,\n    });\n    setCropTarget();\n  };\n\n  useEffect(() => {\n    setCropTarget();\n  }, []);\n\n  useEffect(() => {\n    const parent = parentRef.current;\n    if (!parent) return;\n\n    const handleContextMenu = (e) => {\n      if (e.target.className.includes(\"resize-handle\")) {\n        e.preventDefault();\n      }\n    };\n\n    parent.addEventListener(\"contextmenu\", handleContextMenu);\n    return () => {\n      parent.removeEventListener(\"contextmenu\", handleContextMenu);\n    };\n  }, []);\n\n  // Shadow DOM event forwarding: mouse events from inside the Shadow DOM\n  // get retargeted to the shadow host and stop at `document` — they never\n  // reach `window`. re-resizable listens on `window`, so we forward the\n  // events that originate from our shadow container.\n  useEffect(() => {\n    const shadowHostId = \"screenity-root-container\";\n\n    const forwardToWindow = (e) => {\n      if (\n        e.target?.id === shadowHostId ||\n        e.target?.closest?.(\"#\" + shadowHostId)\n      ) {\n        window.dispatchEvent(\n          new MouseEvent(e.type, {\n            bubbles: false,\n            cancelable: true,\n            clientX: e.clientX,\n            clientY: e.clientY,\n            screenX: e.screenX,\n            screenY: e.screenY,\n            button: e.button,\n            buttons: e.buttons,\n          })\n        );\n      }\n    };\n\n    document.addEventListener(\"mouseup\", forwardToWindow);\n    document.addEventListener(\"mousemove\", forwardToWindow);\n    return () => {\n      document.removeEventListener(\"mouseup\", forwardToWindow);\n      document.removeEventListener(\"mousemove\", forwardToWindow);\n    };\n  }, []);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        width: \"100%\",\n        height: \"100%\",\n        zIndex: -1,\n        pointerEvents:\n          recordingRef.current ||\n          contentState.drawingMode ||\n          contentState.blurMode\n            ? \"none\"\n            : \"auto\",\n      }}\n      className={recordingRef.current ? \"region-recording\" : \"\"}\n      onClick={(e) => {\n        // showExtension false, as long as not clicking on the region\n        if (\n          e.target.className.indexOf(\"resize-handle\") === -1 &&\n          e.target.className.indexOf(\"react-draggable\") === -1 &&\n          e.target.className.indexOf(\"region-rect\") === -1\n        ) {\n          // setContentState((prevContentState) => ({\n          //   ...prevContentState,\n          //   showExtension: false,\n          // }));\n        }\n      }}\n      ref={parentRef}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          width: \"100%\",\n          height: \"100%\",\n          zIndex: 1,\n          pointerEvents:\n            recordingRef.current ||\n            contentState.drawingMode ||\n            contentState.blurMode\n              ? \"none\"\n              : \"auto\",\n        }}\n      >\n        <div className=\"box-hole\" />\n      </div>\n      <Rnd\n        ref={regionRef}\n        style={{\n          position: \"relative\",\n          zIndex: 2,\n          pointerEvents:\n            recordingRef.current ||\n            contentState.drawingMode ||\n            contentState.blurMode\n              ? \"none\"\n              : \"auto\",\n        }}\n        default={{\n          x: contentState.regionX,\n          y: contentState.regionY,\n          width: contentState.regionWidth,\n          height: contentState.regionHeight,\n        }}\n        minWidth={50}\n        minHeight={50}\n        cancel=\".resize-handle-wrapper\"\n        resizeHandleWrapperClass=\"resize-handle-wrapper\"\n        resizeHandleComponent={{\n          topLeft: <div className=\"resize-handle top-left\" />,\n          top: <div className=\"resize-handle top\" />,\n          topRight: <div className=\"resize-handle top-right\" />,\n          right: <div className=\"resize-handle right\" />,\n          bottomRight: <div className=\"resize-handle bottom-right\" />,\n          bottom: <div className=\"resize-handle bottom\" />,\n          bottomLeft: <div className=\"resize-handle bottom-left\" />,\n          left: <div className=\"resize-handle left\" />,\n        }}\n        bounds=\"parent\"\n        onResizeStop={handleResize}\n        onDragStop={handleMove}\n        disableDragging={\n          contentState.recording ||\n          contentState.drawingMode ||\n          contentState.blurMode\n        } // Disable dragging when recording\n        enableResizing={\n          !contentState.recording &&\n          !contentState.drawingMode &&\n          !contentState.blurMode\n        } // Disable resizing when recording\n      >\n        <div\n          ref={cropRef}\n          className=\"region-rect\"\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            outline: recordingRef.current ? \"none\" : \"2px dashed #D9D9D9\",\n            outlineOffset: \"2px\", // Pushes it inside the box to avoid it being visible in recordings\n            boxShadow: \"0 0 0 9999px rgba(0, 0, 0, 0.2)\",\n            borderRadius: \"5px\",\n            zIndex: 2,\n            boxSizing: \"border-box\",\n            pointerEvents:\n              recordingRef.current ||\n              contentState.drawingMode ||\n              contentState.blurMode\n                ? \"none\"\n                : \"auto\",\n          }}\n        />\n      </Rnd>\n    </div>\n  );\n};\n\nexport default ResizableBox;\n"
  },
  {
    "path": "src/pages/Content/region/components/RegionHandles.jsx",
    "content": "import React, { useState, useContext, useRef, useEffect } from \"react\";\nimport { Rnd } from \"react-rnd\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst ResizableBox = () => {\n  return (\n    <div style={{ position: \"relative\" }}>\n      <div\n        style={{\n          position: \"fixed\",\n          top: 0,\n          left: 0,\n          width: \"100%\",\n          height: \"100%\",\n          backgroundColor: \"rgba(0, 0, 0, 0.5)\",\n          zIndex: 999999999,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            top: 0,\n            left: 0,\n            width: \"100%\",\n            height: \"100%\",\n            zIndex: 9999999999,\n          }}\n          className=\"box-hole\"\n        />\n      </div>\n      <Rnd\n        ref={regionRef}\n        style={{ position: \"fixed\", zIndex: 9999999999 }}\n        default={{\n          x: 0,\n          y: 0,\n          width: 200,\n          height: 200,\n        }}\n        minWidth={100}\n        minHeight={100}\n        resizeHandleWrapperClass=\"resize-handle-wrapper\"\n        resizeHandleComponent={{\n          topLeft: (\n            <div\n              style={{\n                position: \"absolute\",\n                top: \"0px\",\n                left: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"nwse-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          top: (\n            <div\n              style={{\n                position: \"absolute\",\n                top: \"0px\",\n                left: \"50%\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"ns-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          topRight: (\n            <div\n              style={{\n                position: \"absolute\",\n                top: \"0px\",\n                right: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"nesw-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          right: (\n            <div\n              style={{\n                position: \"absolute\",\n                top: \"50%\",\n                right: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"ew-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          bottomRight: (\n            <div\n              style={{\n                position: \"absolute\",\n                bottom: \"0px\",\n                right: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"nwse-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          bottom: (\n            <div\n              style={{\n                position: \"absolute\",\n                bottom: \"0px\",\n                left: \"50%\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"ns-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          bottomLeft: (\n            <div\n              style={{\n                position: \"absolute\",\n                bottom: \"0px\",\n                left: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"nesw-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n          left: (\n            <div\n              style={{\n                position: \"absolute\",\n                top: \"50%\",\n                left: \"0px\",\n                width: \"10px\",\n                height: \"10px\",\n                borderRadius: \"50%\",\n                backgroundColor: \"white\",\n                border: \"2px solid black\",\n                cursor: \"ew-resize\",\n                boxSizing: \"border-box\",\n              }}\n            />\n          ),\n        }}\n      >\n        <div\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            border: \"2px dashed white\",\n            borderRadius: \"5px\",\n            zIndex: 9999999999,\n            boxSizing: \"border-box\",\n          }}\n        />\n      </Rnd>\n    </div>\n  );\n};\n\nexport default ResizableBox;\n"
  },
  {
    "path": "src/pages/Content/region/layout/CameraToolbar.jsx",
    "content": "import React, { useState, useEffect, useContext } from \"react\";\n\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\nimport { CameraCloseIcon, CameraMoreIcon } from \"../../toolbar/components/SVG\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst CameraToolbar = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <Toolbar.Root className=\"camera-toolbar\">\n      <Toolbar.Button\n        className=\"CameraToolbarButton\"\n        onClick={() => {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            cameraActive: false,\n          }));\n          chrome.storage.local.set({ cameraActive: false });\n        }}\n      >\n        <CameraCloseIcon />\n      </Toolbar.Button>\n      <Toolbar.Button className=\"CameraToolbarButton CameraMore\">\n        <CameraMoreIcon />\n      </Toolbar.Button>\n    </Toolbar.Root>\n  );\n};\n\nexport default CameraToolbar;\n"
  },
  {
    "path": "src/pages/Content/region/layout/CameraWrap.jsx",
    "content": "import React, { useEffect, useContext, useRef, useState } from \"react\";\n\nimport { Rnd } from \"react-rnd\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nimport CameraToolbar from \"./CameraToolbar\";\nimport ResizeHandle from \"../components/ResizeHandle\";\n\nconst CameraWrap = (props) => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n  const cameraRef = React.useRef();\n  const [cx, setCx] = useState(200);\n  const [cy, setCy] = useState(200);\n  const [w, setW] = useState(200);\n  const [h, setH] = useState(200);\n\n  const updateUIPosition = () => {\n    const ref =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\");\n    const circleCenterX =\n      ref.getBoundingClientRect().left + ref.getBoundingClientRect().width / 2;\n    const circleCenterY =\n      ref.getBoundingClientRect().top + ref.getBoundingClientRect().height / 2;\n    const circleRadius = ref.getBoundingClientRect().width / 2;\n    const squareBottomRightX =\n      ref.getBoundingClientRect().left + ref.getBoundingClientRect().width;\n    const squareBottomRightY =\n      ref.getBoundingClientRect().top + ref.getBoundingClientRect().height;\n    const handle =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-resize\");\n    const toolbar =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-toolbar\");\n\n    // Calculate 'r' using the formula we derived earlier\n    const c = Math.sqrt(\n      Math.pow(circleCenterX - squareBottomRightX, 2) +\n        Math.pow(circleCenterY - squareBottomRightY, 2)\n    );\n    const a = circleRadius / Math.sqrt(2);\n    const r = (c + Math.sqrt(c ** 2 + 16 * a ** 2)) / 4;\n\n    // Calculate the handle position\n    const x = r - r / Math.sqrt(2);\n    const y = r - r / Math.sqrt(2);\n\n    // Position the handle element to the calculated coordinates\n    handle.style.bottom = `${y - handle.getBoundingClientRect().width / 2}px`;\n    handle.style.right = `${x - handle.getBoundingClientRect().height / 2}px`;\n    toolbar.style.top = `${y - toolbar.getBoundingClientRect().width / 2}px`;\n    toolbar.style.left = `${x - toolbar.getBoundingClientRect().height / 2}px`;\n  };\n\n  const saveDimensions = () => {\n    const ref =\n      props.shadowRef.current.shadowRoot.querySelector(\".camera-draggable\");\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      cameraDimensions: {\n        size: ref.getBoundingClientRect().width,\n        x: ref.getBoundingClientRect().x,\n        y: ref.getBoundingClientRect().y,\n      },\n    }));\n    chrome.storage.local.set({\n      cameraDimensions: {\n        size: ref.getBoundingClientRect().width,\n        x: ref.getBoundingClientRect().x,\n        y: ref.getBoundingClientRect().y,\n      },\n    });\n  };\n\n  useEffect(() => {\n    if (!cameraRef.current) return;\n    if (!props.shadowRef.current.shadowRoot.querySelector(\".camera-resize\"))\n      return;\n    if (!props.shadowRef.current.shadowRoot.querySelector(\".camera-toolbar\"))\n      return;\n\n    updateUIPosition();\n  }, [cameraRef.current]);\n\n  return (\n    <div>\n      <Rnd\n        default={{\n          x: contentState.cameraDimensions.x,\n          y: contentState.cameraDimensions.y,\n          width: contentState.cameraDimensions.size,\n          height: contentState.cameraDimensions.size,\n        }}\n        ref={cameraRef}\n        className=\"camera-draggable\"\n        dragHandleClassName=\"camera-grab\"\n        resizeHandleComponent={{\n          bottomRight: <ResizeHandle />,\n        }}\n        minHeight={150}\n        minWidth={150}\n        enableResizing={{\n          bottom: false,\n          bottomRight: true,\n          bottomLeft: false,\n          left: false,\n          right: false,\n          top: false,\n          topRight: false,\n          topLeft: false,\n        }}\n        onResize={(e, direction, ref, delta, position) => {\n          updateUIPosition();\n        }}\n        onResizeStop={(e, direction, ref, delta, position) => {\n          saveDimensions();\n        }}\n        onDragStop={(node, x, y) => {\n          saveDimensions();\n        }}\n        lockAspectRatio={1}\n        bounds={\"window\"}\n      >\n        <div className=\"camera-grab\"></div>\n        <CameraToolbar />\n        <iframe\n          style={{\n            width: \"100%\",\n            height: \"100%\",\n            borderRadius: \"50%\",\n            outline: \"none\",\n            border: \"none\",\n            pointerEvents: \"none\",\n          }}\n          className={contentState.cameraFlipped ? \"camera-flipped\" : \"\"}\n          src={chrome.runtime.getURL(\"camera.html\")}\n          allow=\"camera; microphone\"\n        ></iframe>\n      </Rnd>\n    </div>\n  );\n};\n\nexport default CameraWrap;\n"
  },
  {
    "path": "src/pages/Content/region/styles/_Region.scss",
    "content": "@use '../../styles/_variables' as *;\n\n.box-hole {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 9999999999;\n}\n.resize-handle {\n  position: absolute;\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background-color: white;\n  border: 2px solid rgba(0, 0, 0, .5);\n  box-sizing: border-box;\n}\n.resize-handle.top-left {\n\tbottom: 0;\n  left: 0;\n\ttop: 0;\n\tright: 0;\n\tmargin: auto;\n  cursor: nwse-resize;\n}\n.resize-handle.top {\n  top: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n.resize-handle.top-right {\n\tbottom: 0;\n  left: 0;\n\ttop: 0;\n\tright: 0;\n\tmargin: auto;\n  cursor: nesw-resize;\n}\n.resize-handle.right {\n  top: 50%;\n  right: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n.resize-handle.bottom-right {\n\tbottom: 0;\n  left: 0;\n\ttop: 0;\n\tright: 0;\n\tmargin: auto;\n  cursor: nwse-resize;\n}\n.resize-handle.bottom {\n  bottom: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n.resize-handle.bottom-left {\n  bottom: 0;\n  left: 0;\n\ttop: 0;\n\tright: 0;\n\tmargin: auto;\n  cursor: nesw-resize;\n}\n.resize-handle.left {\n  top: 50%;\n  left: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n.region-recording * {\n\tpointer-events: none!important;\n}"
  },
  {
    "path": "src/pages/Content/region/styles/layout/_CameraToolbar.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.camera-toolbar {\n\tdisplay: flex;\n\talign-items: center;\n  padding-left: 4px;\n\tpadding-right: 4px;\n\ttransition: opacity .25s cubic-bezier(.61,.11,.08,.96);\n\tmin-width: max-content;\n\tbackground-color: rgba(30, 30, 30, .8);\n\tbox-shadow: 0 2px 10px rgba(0, 0, 0, .15);\n\theight: 28px;\n\tposition: absolute;\n\tleft: 10px;\n\ttop: 10px;\n\tborder-radius: $container-border-radius;\n\tbackdrop-filter: blur(10px);\n\tz-index: $z-index-max;\n\topacity: 0;\n\tborder: 3px solid rgba(255, 255, 255, .2);\n}\n.camera-draggable:hover {\n\t.camera-toolbar, .camera-resize {\n\t\topacity: 1!important;\n\t}\n}\n.camera-toolbar:hover, .camera-resize:hover {\n\topacity: 1!important;\n}\n\n.CameraToolbarSeparator {\n  width: 1px;\n\theight: 18px;\n  background-color: rgba(255, 255, 255, .3);\n  margin: 0 4px;\n}\n.CameraToggleItem, .CameraToolbarButton {\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tcolor: #000;\n  height: 22px;\n\twidth: 22px;\n\ttext-align: center;\n  font-size: 13px;\n  line-height: 1;\n\tborder-radius: 50%;\n\ttransition: background-color .25s ease-in-out;\n\tbackground-color: rgba(124, 139, 165, 0);\n\n\tsvg {\n\t\tcolor: $color-icon;\n\t}\n\n\t&:hover {\n\t\tbackground-color: rgba(124, 139, 165, 0.2)!important;\n\t\tcursor: pointer;\n\t}\n\n\t&:disabled {\n\t\topacity: 0.5;\n\t\tpointer-events: none;\n\t}\n\n\t&[data-state='on'] {\n\t\tcolor: #FFF;\n\n\t\tsvg {\n\t\t\tcolor: #FFF;\n\t\t}\n\n\t}\n}\n\n\n.CameraToggleItem:hover, .CameraToggleButton:hover {\n\tcursor: pointer;\n}\n.CameraToggleItem:focus-visible, .CameraToggleButton:focus-visible {\n  position: relative;\n  box-shadow: $focus-border;\n}\n\n.CameraToggleGroup {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.CameraToggleGroup, .CameraToolbarSeparator {\n\tdisplay: none;\n}"
  },
  {
    "path": "src/pages/Content/region/styles/layout/_CameraWrap.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.camera-draggable {\n\twidth: 100%;\n\theight: 100%;\n\ttransform-origin: left top;\n\tborder-radius: 50%;\n}\n.camera-grab {\n\twidth: 100%;\n\theight: 100%;\n\tposition: absolute;\n\tborder-radius: 50%;\n\tz-index: 99999999!important;\n\tcursor: grab;\n}\n.camera-flipped {\n\ttransform: scaleX(-1);\n}"
  },
  {
    "path": "src/pages/Content/shortcuts/Shortcuts.jsx",
    "content": "import React, { useEffect, useContext, useRef } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\nimport { undoCanvas, redoCanvas, saveCanvas } from \"../canvas/modules/History\";\n\nconst Shortcuts = ({ shortcuts }) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const contentStateRef = useRef(contentState);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n  /* For the record, this is the shortcuts object:\n\tshortcuts: {\n      \"start-recording\": \"ctrl+shift+1\",\n      \"stop-recording\": \"ctrl+shift+2\",\n      \"pause-recording\": \"ctrl+shift+3\",\n      \"resume-recording\": \"ctrl+shift+4\",\n      \"dismiss-recording\": \"ctrl+shift+5\",\n      \"restart-recording\": \"ctrl+shift+6\",\n      \"toggle-drawing-mode\": \"ctrl+shift+7\",\n    },\n\t*/\n\n  // Set up all the hotkeys programmatically from the shortcuts object, without using useEffect\n\n  /*\n  useHotkeys(shortcuts[\"toggle-drawing-mode\"], () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      drawingMode: !prevContentState.drawingMode,\n    }));\n  });\n  */\n\n  // in-app shortcuts for screenity tools\n  useEffect(() => {\n    const getDeepActiveElement = () => {\n      let active = document.activeElement;\n      while (active && active.shadowRoot && active.shadowRoot.activeElement) {\n        active = active.shadowRoot.activeElement;\n      }\n      return active;\n    };\n\n    const isEditableElement = (element) => {\n      if (!element) return false;\n      if (element.isContentEditable) return true;\n      const tag = element.tagName ? element.tagName.toLowerCase() : \"\";\n      return tag === \"input\" || tag === \"textarea\" || tag === \"select\";\n    };\n\n    const isTextEditingActive = () => {\n      const state = contentStateRef.current;\n      if (state?.tool !== \"text\") return false;\n      const activeObj = state?.canvas?.getActiveObject?.();\n      return Boolean(activeObj && activeObj.isEditing);\n    };\n\n    const shouldHandleShortcut = (event) => {\n      const state = contentStateRef.current;\n      if (!state?.drawingMode && !state?.blurMode) return false;\n      if (event.altKey || event.ctrlKey || event.metaKey) return false;\n\n      const active = getDeepActiveElement();\n      if (isEditableElement(active)) return false;\n      if (isTextEditingActive()) return false;\n\n      return true;\n    };\n\n    const getDigitKey = (event) => {\n      if (event.code && event.code.startsWith(\"Digit\")) {\n        return event.code.replace(\"Digit\", \"\");\n      }\n      if (event.code && event.code.startsWith(\"Numpad\")) {\n        return event.code.replace(\"Numpad\", \"\");\n      }\n      if (event.key >= \"0\" && event.key <= \"9\") {\n        return event.key;\n      }\n      return null;\n    };\n\n    const setTool = (tool, nextShape) => {\n      setContentState((prev) => ({\n        ...prev,\n        tool,\n        ...(nextShape ? nextShape : {}),\n      }));\n    };\n\n    const clearDrawings = () => {\n      const state = contentStateRef.current;\n      if (!state?.canvas) return;\n      state.canvas.clear();\n      state.canvas.renderAll();\n      state.canvas.requestRenderAll();\n      saveCanvas(state, setContentState);\n    };\n\n    const clearBlurMasks = () => {\n      const blurredElements = document.querySelectorAll(\".screenity-blur\");\n      blurredElements.forEach((element) => {\n        element.classList.remove(\"screenity-blur\");\n      });\n    };\n\n    const getShadowRoot = () =>\n      contentStateRef.current?.shadowRef?.shadowRoot || null;\n\n    const toggleColorPicker = () => {\n      const shadowRoot = getShadowRoot();\n      const trigger = shadowRoot\n        ? shadowRoot.querySelector(\"[data-color-trigger]\")\n        : document.querySelector(\"[data-color-trigger]\");\n      if (trigger) {\n        trigger.click();\n      }\n    };\n\n    const openImagePicker = () => {\n      const shadowRoot = getShadowRoot();\n      const fileInput = shadowRoot\n        ? shadowRoot.querySelector('[data-image-upload=\"true\"]')\n        : document.querySelector('[data-image-upload=\"true\"]');\n      if (fileInput) {\n        fileInput.click();\n      }\n    };\n\n    const deriveCursorMode = (effects, fallback) => {\n      if (effects.length === 0) return \"none\";\n      if (effects.length === 1) return effects[0];\n      return fallback || effects[0] || \"none\";\n    };\n\n    const clearCursorEffects = () => {\n      setContentState((prev) => ({\n        ...prev,\n        cursorEffects: [],\n        cursorMode: \"none\",\n      }));\n      chrome.storage.local.set({\n        cursorEffects: [],\n        cursorMode: \"none\",\n      });\n    };\n\n    const toggleCursorEffect = (effect) => {\n      const state = contentStateRef.current;\n      const current = Array.isArray(state.cursorEffects)\n        ? state.cursorEffects\n        : [];\n      const next = current.includes(effect)\n        ? current.filter((item) => item !== effect)\n        : [...current, effect];\n      const nextMode = deriveCursorMode(next, effect);\n\n      setContentState((prev) => ({\n        ...prev,\n        cursorEffects: next,\n        cursorMode: nextMode,\n      }));\n      chrome.storage.local.set({\n        cursorEffects: next,\n        cursorMode: nextMode,\n      });\n    };\n\n    const handleKeyDown = (event) => {\n      const state = contentStateRef.current;\n      if (state?.drawingMode && (event.ctrlKey || event.metaKey)) {\n        const key = event.key.toLowerCase();\n        if (key === \"z\" && !event.shiftKey) {\n          event.preventDefault();\n          event.stopPropagation();\n          event.stopImmediatePropagation();\n          undoCanvas(state, setContentState);\n          return;\n        }\n        if ((key === \"z\" && event.shiftKey) || key === \"y\") {\n          event.preventDefault();\n          event.stopPropagation();\n          event.stopImmediatePropagation();\n          redoCanvas(state, setContentState);\n          return;\n        }\n      }\n\n      if (!shouldHandleShortcut(event)) return;\n      const digitKey = getDigitKey(event);\n      if (!digitKey) return;\n\n      let handled = true;\n      if (state?.drawingMode) {\n        switch (digitKey) {\n          case \"1\":\n            setTool(\"select\");\n            break;\n          case \"2\":\n            setTool(\"pen\");\n            break;\n          case \"3\":\n            setTool(\"highlighter\");\n            break;\n          case \"4\":\n            setTool(\"eraser\");\n            break;\n          case \"5\":\n            toggleColorPicker();\n            break;\n          case \"6\":\n            setTool(\"text\");\n            break;\n          case \"7\":\n            setTool(\"shape\", { shape: \"rectangle\", shapeFill: false });\n            break;\n          case \"8\":\n            setTool(\"arrow\");\n            break;\n          case \"9\":\n            openImagePicker();\n            break;\n          case \"0\":\n            clearDrawings();\n            break;\n          default:\n            handled = false;\n        }\n      } else {\n        switch (digitKey) {\n          case \"0\":\n            if (state?.blurMode) {\n              clearBlurMasks();\n            }\n            clearCursorEffects();\n            break;\n          case \"1\":\n            toggleCursorEffect(\"target\");\n            break;\n          case \"2\":\n            toggleCursorEffect(\"highlight\");\n            break;\n          case \"3\":\n            toggleCursorEffect(\"spotlight\");\n            break;\n          default:\n            handled = false;\n        }\n      }\n      if (handled) {\n        event.preventDefault();\n        event.stopPropagation();\n        event.stopImmediatePropagation();\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown, true);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown, true);\n    };\n  }, []);\n\n  // Push to talk (while Alt/Option + Shift + J key is pressed enable microphone, disable on key up)\n  useEffect(() => {\n    const handleKeyDown = (event) => {\n      if (!contentStateRef.current.pushToTalk) return;\n      if (event.code === \"KeyU\" && event.altKey && event.shiftKey) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          micActive: true,\n        }));\n\n        chrome.storage.local.set({\n          micActive: true,\n        });\n\n        chrome.runtime.sendMessage({\n          type: \"set-mic-active-tab\",\n          active: true,\n          defaultAudioInput: contentState.defaultAudioInput,\n        });\n      }\n    };\n\n    const handleKeyUp = (event) => {\n      if (!contentStateRef.current.pushToTalk) return;\n      if (event.code === \"KeyU\" && event.altKey && event.shiftKey) {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          micActive: false,\n        }));\n\n        chrome.storage.local.set({\n          micActive: false,\n        });\n\n        chrome.runtime.sendMessage({\n          type: \"set-mic-active-tab\",\n          active: false,\n          defaultAudioInput: contentState.defaultAudioInput,\n        });\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleKeyDown);\n    window.addEventListener(\"keyup\", handleKeyUp);\n\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown);\n      window.removeEventListener(\"keyup\", handleKeyUp);\n    };\n  }, []);\n\n  return <></>;\n};\n\nexport default Shortcuts;\n"
  },
  {
    "path": "src/pages/Content/styles/_variables.scss",
    "content": "@mixin font($font-family, $font-file) {\n  @font-face {\n    font-family: $font-family;\n    src: url($font-file + \".ttf\") format(\"truetype\");\n    font-weight: normal;\n    font-style: normal;\n  }\n}\n\n@include font(\n  \"Satoshi-Light\",\n  \"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light\"\n);\n@include font(\n  \"Satoshi-Medium\",\n  \"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium\"\n);\n@include font(\n  \"Satoshi-Bold\",\n  \"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold\"\n);\n@include font(\n  \"Gloria Hallelujah\",\n  \"chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular\"\n);\n\n/* Colors */\n$color-background: #fff;\n$color-light-grey: #f6f7fb;\n$color-primary: #3080f8;\n$color-border: #e8e8e8;\n$color-text-contrast: #fff;\n$color-text-primary: #29292f;\n$color-text-secondary: #6e7684;\n$color-red: #d2234d;\n$color-red-light: #faf0f4;\n$color-primary-transparent: rgba(48, 128, 248, 0.1);\n$color-icon: #9797a4;\n\n/* Font */\n//$font-size-normal: 0.875rem;\n//$font-size-small: 0.75rem;\n$font-size-normal: 14px;\n$font-size-detail: 0.8rem;\n$font-size-small: 12px;\n$font-weight-bold: 700;\n$font-weight-normal: 500;\n$font-weight-light: 400;\n$font-light: \"Satoshi-Light\", sans-serif;\n$font-medium: \"Satoshi-Medium\", sans-serif;\n$font-regular: \"Satoshi-Regular\", sans-serif;\n$font-bold: \"Satoshi-Bold\", sans-serif;\n\n/* Spacing */\n/*\n$spacing-01: 0.125rem;\n$spacing-02: 0.25rem;\n$spacing-03: 0.5rem;\n$spacing-04: 0.75rem;\n$spacing-05: 1rem;\n*/\n$spacing-01: 2px;\n$spacing-02: 4px;\n$spacing-03: 8px;\n$spacing-04: 12px;\n$spacing-05: 16px;\n\n/* Container */\n$container-border-radius: 30px;\n$container-border: 1px solid $color-border;\n$container-shadow: drop-shadow(0px 4px 100px rgba(0, 0, 0, 0.35));\n$container-shadow-focus: 2px 2px 10px rgba(0, 0, 0, 0.1);\n$container-soft-shadow: 0 4px 20px rgb(38 38 52 / 8%);\n\n/* Gradients */\n$gradient-primary: radial-gradient(\n  117.41% 117.78% at 35.44% 0%,\n  #2baef8 23.13%,\n  #3582f6 46.35%,\n  #486def 74.48%,\n  #7b9aea 100%\n);\n$gradient-shadow-primary: drop-shadow(0px 4px 20px rgba(86, 123, 218, 0.5));\n\n/* Events */\n$focus-border: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n$focus-inner-border: inset 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n\n/* Z-index */\n$z-index-max: 99999999999;\n"
  },
  {
    "path": "src/pages/Content/styles/app.css",
    "content": "@font-face {\n  font-family: \"Satoshi-Light\";\n  src: url(\"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: \"Satoshi-Medium\";\n  src: url(\"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: \"Satoshi-Bold\";\n  src: url(\"chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n}\n@font-face {\n  font-family: \"Gloria Hallelujah\";\n  src: url(\"chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular.ttf\") format(\"truetype\");\n  font-weight: normal;\n  font-style: normal;\n}\n/* Colors */\n/* Font */\n/* Spacing */\n/*\n$spacing-01: 0.125rem;\n$spacing-02: 0.25rem;\n$spacing-03: 0.5rem;\n$spacing-04: 0.75rem;\n$spacing-05: 1rem;\n*/\n/* Container */\n/* Gradients */\n/* Events */\n/* Z-index */\n/* reset */\na,\nbutton {\n  all: unset;\n}\n\niframe {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  overflow: scroll;\n  z-index: -9999;\n  top: 0px;\n  left: 0px;\n  border: 0px;\n  pointer-events: all !important;\n}\n\n.container {\n  pointer-events: none !important;\n}\n\n.visually-hidden-toolbar {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n  transition: opacity 0.3s ease, visibility 0s linear 0.3s;\n}\n\n.toolbar-controls {\n  opacity: 0;\n  cursor: pointer;\n  position: absolute;\n  top: -10px;\n  right: -10px;\n  box-sizing: border-box;\n  border-radius: 30px;\n  border: 1px solid #e8e8e8;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  z-index: 99999999999999;\n  background: rgb(240, 238, 238);\n  backdrop-filter: blur(10px);\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 6px;\n  padding-bottom: 6px;\n  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n  pointer-events: none;\n}\n.toolbar-controls.open {\n  opacity: 1 !important;\n  pointer-events: auto;\n}\n.toolbar-controls .popup-control svg {\n  color: #9797a4;\n  margin-bottom: -2px;\n}\n\n.ToolbarBounds {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  width: 100%;\n  height: 100%;\n  border: 10px solid #3080f8;\n  pointer-events: none;\n  transform: scale(1.2);\n  opacity: 0;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s ease-in-out;\n}\n\n.ToolbarBounds.ToolbarShake {\n  transform: scale(1);\n  opacity: 0.4;\n}\n\n.react-draggable {\n  pointer-events: all;\n}\n\n.ToolbarShake .react-draggable {\n  width: 100%;\n  height: 100%;\n}\n\n.ToolbarElastic {\n  transition: all 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n\n.ToolbarShake .ToolbarRoot {\n  animation: subtleshake 0.9s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n  animation-iteration-count: infinite !important;\n  background-color: white !important;\n}\n\n.ToolbarDragging .ToolbarRoot {\n  transform: scale(1.02);\n}\n.ToolbarDragging .ToolbarRoot::after {\n  filter: drop-shadow(0px 20px 50px rgba(0, 0, 0, 0.5));\n}\n\n@keyframes shake {\n  0% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  20% {\n    transform: translate(-3px, 0px) rotate(1deg);\n  }\n  30% {\n    transform: translate(3px, 2px) rotate(0deg);\n  }\n  40% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  50% {\n    transform: translate(-1px, 2px) rotate(-1deg);\n  }\n  60% {\n    transform: translate(-3px, 1px) rotate(0deg);\n  }\n  70% {\n    transform: translate(3px, 1px) rotate(-1deg);\n  }\n  80% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  90% {\n    transform: translate(1px, 2px) rotate(0deg);\n  }\n  100% {\n    transform: translate(1px, -2px) rotate(-1deg);\n  }\n}\n@keyframes subtleshake {\n  0% {\n    transform: translate(0px, 0px) rotate(0deg);\n  }\n  10% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n  20% {\n    transform: translate(-1px, -1px) rotate(-1deg);\n  }\n  30% {\n    transform: translate(1px, 0px) rotate(0deg);\n  }\n  40% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  50% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  60% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  70% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  80% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  90% {\n    transform: translate(0px, -1px) rotate(-1deg);\n  }\n  100% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n}\n.ToolbarTransparent {\n  opacity: 0;\n}\n.ToolbarTransparent:hover {\n  opacity: 1;\n}\n\n.ToolbarHoverZone {\n  display: inline-block;\n  position: relative;\n  padding: 4px;\n  width: -moz-fit-content;\n  width: fit-content;\n  height: -moz-fit-content;\n  height: fit-content;\n}\n\n.ToolbarRoot {\n  display: flex;\n  align-items: center;\n  padding-left: 10px;\n  transition: opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), transform 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  min-width: -moz-max-content;\n  min-width: max-content;\n  background-color: white;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  padding-right: 10px;\n  height: 48px;\n  position: absolute;\n  bottom: 20px;\n  left: 20px;\n  border-radius: 30px;\n}\n.ToolbarRoot::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: #fff;\n  z-index: -9999999;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  border-radius: 30px;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transition: filter 0.2s ease-in-out;\n}\n\n.ForceTransparent {\n  opacity: 0 !important;\n}\n\n.ToolbarRecordingControls {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #f6f7fb;\n  border-radius: 30px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  height: calc(100% - 12px);\n  padding-left: 2px;\n  padding-right: 2px;\n}\n\n.ToolbarRecordingTime {\n  margin-right: 4px;\n  width: 42px;\n  color: #29292f;\n  font-size: 13px;\n  line-height: 1;\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n}\n\n.TimerWarning {\n  color: #ff4c4c; /* or add a pulsing effect */\n  font-weight: bold;\n  animation: pulse 1s infinite alternate;\n}\n\n@keyframes pulse {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0.6;\n  }\n}\n.ToolbarToggleGroup {\n  display: flex;\n  align-items: center;\n}\n\n.ToolbarToggleWrap {\n  flex: 1 1 auto;\n  align-items: center;\n  justify-content: flex-start;\n  position: relative;\n  flex: 0 0 auto;\n  width: 32px;\n  height: 32px;\n  display: inline-flex;\n  line-height: 1;\n  align-items: center;\n  justify-content: center;\n}\n\n.ToolbarToggleItem,\n.ToolbarModeItem,\n.ToolbarModeItemSingle,\n.ToolbarLink,\n.ToolbarButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #000;\n  height: 32px;\n  width: 32px;\n  text-align: center;\n  font-size: 13px;\n  line-height: 1;\n  border-radius: 50%;\n  transition: background-color 0.25s ease-in-out;\n  background-color: rgba(124, 139, 165, 0);\n}\n.ToolbarToggleItem svg,\n.ToolbarModeItem svg,\n.ToolbarModeItemSingle svg,\n.ToolbarLink svg,\n.ToolbarButton svg {\n  color: #9797a4;\n}\n.ToolbarToggleItem:disabled,\n.ToolbarModeItem:disabled,\n.ToolbarModeItemSingle:disabled,\n.ToolbarLink:disabled,\n.ToolbarButton:disabled {\n  opacity: 0.5;\n  cursor: not-allowed !important;\n  background: none !important;\n}\n.ToolbarToggleItem.resume svg,\n.ToolbarModeItem.resume svg,\n.ToolbarModeItemSingle.resume svg,\n.ToolbarLink.resume svg,\n.ToolbarButton.resume svg {\n  color: #f7387d !important;\n}\n\n.ToolbarToggleItem:hover,\n.ToolbarModeItem:hover,\n.ToolbarModeItemSingle:hover,\n.ToolbarLink:hover,\n.ToolbarButton:hover {\n  cursor: pointer;\n  background-color: rgba(124, 139, 165, 0.1) !important;\n}\n\n.ToolbarToggleItem:focus-visible,\n.ToolbarModeItemSingle:focus-visible,\n.ToolbarModeItem:focus-visible,\n.ToolbarLink:focus-visible,\n.ToolbarButton:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.ToolbarModeItemSingle {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarModeItemSingle:first-child {\n  margin-left: 0;\n}\n.ToolbarModeItemSingle[data-state=on] {\n  background: rgba(120, 192, 114, 0.1);\n}\n.ToolbarModeItemSingle[data-state=on] svg {\n  color: #78c072;\n}\n.ToolbarModeItemSingle[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n  opacity: 1 !important;\n}\n\n.ToolbarModeItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarModeItem:first-child {\n  margin-left: 0;\n}\n.ToolbarModeItem::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 50%;\n  border-radius: 80px 80px 0% 0%;\n  box-sizing: border-box;\n  position: absolute;\n  top: -16px;\n  left: 0;\n  z-index: -999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s ease-in-out;\n  transform: translateY(5px) scale(0) !important;\n  border-right: 3px solid white;\n  border-top: 9px solid white;\n  border-left: 3px solid white;\n  background-color: transparent;\n  opacity: 0;\n}\n.ToolbarModeItem[data-state=on] {\n  background: rgba(56, 126, 247, 0.1);\n}\n.ToolbarModeItem[data-state=on] svg {\n  color: #3080f8;\n}\n.ToolbarModeItem[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n  opacity: 1 !important;\n}\n\n.ToolbarBottom .ToolbarModeItem::before {\n  transform: translateY(-5px) scale(0.5) !important;\n  bottom: -16px;\n  top: unset !important;\n  border-bottom: 9px solid white !important;\n  border-radius: 0% 0% 80px 80px !important;\n  border-top: none !important;\n}\n.ToolbarBottom .ToolbarModeItem[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n}\n\n.ToolbarToggleItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarToggleItem:first-child {\n  margin-left: 0;\n}\n.ToolbarToggleItem[data-state=on] {\n  background: radial-gradient(117.41% 117.78% at 35.44% 0%, #2baef8 23.13%, #3582f6 46.35%, #486def 74.48%, #7b9aea 100%);\n  color: #fff;\n}\n.ToolbarToggleItem[data-state=on] svg {\n  color: #fff;\n}\n\n.ToolbarSeparator {\n  width: 1px;\n  height: 19px;\n  background-color: #e8e8e8;\n  margin: 0 8px;\n}\n\n.ToolbarLink {\n  background-color: transparent;\n  color: var(--mauve11);\n  display: inline-flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.ToolbarLink:hover {\n  background-color: transparent;\n  cursor: pointer;\n}\n\n.ToolbarPaused {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  box-sizing: border-box;\n  border: 10px solid #f7387d;\n  pointer-events: none;\n  opacity: 0.5;\n}\n.ToolbarPaused.hidden {\n  display: none;\n}\n\n.OnboardingArrow {\n  position: absolute;\n  z-index: 99999999999;\n  width: -moz-max-content;\n  width: max-content;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 16px;\n  left: -69px;\n  bottom: 23px;\n  transform: rotate(12deg);\n  pointer-events: none !important;\n}\n\n.OnboardingText {\n  font-size: 32px;\n  color: #9797a4;\n  font-family: \"Gloria-Hallelujah\", sans-serif !important;\n}\n\n.ArrowShape {\n  margin-left: -15px;\n}\n\n.driver-popover.ScreenityOnboardingPopover,\n.ScreenityOnboardingPopover,\n.onboarding-popover {\n  border-radius: 30px !important;\n  background: #fff !important;\n  color: #29292f !important;\n  font-family: \"Satoshi-Regular\", sans-serif !important;\n  box-shadow: 0 4px 20px rgba(38, 38, 52, 0.08) !important;\n  padding: 20px !important;\n  max-width: 340px !important;\n  font-size: 14px !important;\n  z-index: 99999999999 !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-title,\n.ScreenityOnboardingPopover .driver-popover-title,\n.onboarding-popover .driver-popover-title {\n  font-size: 1rem !important;\n  font-family: \"Satoshi-Bold\", sans-serif !important;\n  margin-bottom: 12px !important;\n  color: #29292f !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-description,\n.ScreenityOnboardingPopover .driver-popover-description,\n.onboarding-popover .driver-popover-description {\n  font-size: 14px !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  color: #6e7684 !important;\n  line-height: 1.5 !important;\n  margin-bottom: 18px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-description a,\n.ScreenityOnboardingPopover .driver-popover-description a,\n.onboarding-popover .driver-popover-description a {\n  color: #3080f8 !important;\n  font-family: \"Satoshi-Bold\", sans-serif !important;\n  text-decoration: none !important;\n  cursor: pointer !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-close-btn,\n.onboarding-popover .driver-popover-close-btn {\n  display: none !important;\n  top: 14px;\n  right: 12px;\n  color: #6e7684 !important;\n  font-size: 1.5rem !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow,\n.ScreenityOnboardingPopover .driver-popover-arrow,\n.onboarding-popover .driver-popover-arrow {\n  width: 0px;\n  height: 0px;\n  background: none !important;\n  box-shadow: none !important;\n  box-sizing: border-box;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-top,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-top,\n.onboarding-popover .driver-popover-arrow-side-top {\n  border-left: 8px solid transparent !important;\n  border-right: 8px solid transparent !important;\n  border-top: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-bottom,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-bottom,\n.onboarding-popover .driver-popover-arrow-side-bottom {\n  border-left: 8px solid transparent !important;\n  border-right: 8px solid transparent !important;\n  border-bottom: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-left,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-left,\n.onboarding-popover .driver-popover-arrow-side-left {\n  border-top: 8px solid transparent !important;\n  border-bottom: 8px solid transparent !important;\n  border-left: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-right,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-right,\n.onboarding-popover .driver-popover-arrow-side-right {\n  border-top: 8px solid transparent !important;\n  border-bottom: 8px solid transparent !important;\n  border-right: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-start,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-start,\n.onboarding-popover .driver-popover-arrow-align-start {\n  top: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-end,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-end,\n.onboarding-popover .driver-popover-arrow-align-end {\n  bottom: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-left,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-left,\n.onboarding-popover .driver-popover-arrow-align-left {\n  left: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-right,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-right,\n.onboarding-popover .driver-popover-arrow-align-right {\n  right: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-progress-text,\n.ScreenityOnboardingPopover .driver-popover-progress-text,\n.onboarding-popover .driver-popover-progress-text {\n  font-size: 0.8rem !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  color: #6e7684 !important;\n  opacity: 0.7 !important;\n  margin-top: 2px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer,\n.ScreenityOnboardingPopover .driver-popover-footer,\n.onboarding-popover .driver-popover-footer {\n  display: flex !important;\n  justify-content: flex-end;\n  gap: 8px;\n  margin-top: 16px;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns {\n  gap: 6px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn {\n  background-color: #3080f8 !important;\n  color: white !important;\n  border: none !important;\n  padding: 10px 14px !important;\n  border-radius: 30px !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  font-weight: 500 !important;\n  cursor: pointer !important;\n  font-size: 0.875rem !important;\n  text-shadow: none !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover {\n  background: rgb(23.3341121495, 112.8668224299, 247.1658878505) !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn {\n  background-color: transparent !important;\n  color: #29292f !important;\n  border: 1px solid #e8e8e8 !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover {\n  background: #f6f7fb !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn {\n  background-color: transparent !important;\n  color: #6e7684 !important;\n}\n\nbody {\n  background-color: white !important;\n}\n\n.DrawingToolbar.show-toolbar {\n  opacity: 1 !important;\n  pointer-events: all !important;\n  transform: scale(1) translate(calc(-50% + 16px), 0px) !important;\n}\n\n.ToolbarBottom .DrawingToolbar {\n  transform-origin: 0 -100% !important;\n}\n\n.DrawingToolbar {\n  opacity: 0;\n  pointer-events: none;\n  align-items: center;\n  display: flex;\n  min-width: -moz-max-content;\n  min-width: max-content;\n  padding-left: 10px;\n  padding-right: 10px;\n  border-radius: 6px;\n  box-shadow: 0 2px 10px var(--blackA7);\n  position: absolute;\n  height: 44px;\n  left: 0px;\n  transform: translate(calc(-50% + 16px));\n  transform-origin: 0 100%;\n  border-radius: 15px;\n  z-index: 99999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  transform: scale(0.5) translate(calc(-50% + 16px), 10px);\n}\n.DrawingToolbar::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  filter: blur(10px);\n  opacity: 0.15;\n  background-color: #000;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  z-index: -999999999999999 !important;\n}\n.DrawingToolbar::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: rgba(242, 241, 242, 0.85);\n  backdrop-filter: blur(5px);\n  background-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n  mask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n  background-position: center bottom 50px;\n  border-radius: 15px;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: -2;\n}\n.DrawingToolbar .ToolbarSeparator {\n  background-color: #dddcdc;\n}\n\n.ToolbarTop .DrawingToolbar {\n  bottom: 49px !important;\n}\n\n.ToolbarBottom .DrawingToolbar {\n  top: 48px !important;\n}\n.ToolbarBottom .DrawingToolbar::before {\n  -webkit-mask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px) !important;\n  mask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px);\n  background-position: center bottom 50px !important;\n}\n\n.ColorPicker {\n  width: 14px;\n  height: 14px;\n  background: #ED6C3A;\n  border: 1.5px solid rgba(0, 0, 0, 0.2);\n  border-radius: 50%;\n}\n\n.shapeToolbar {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  box-shadow: 0 2px 10px var(--blackA7);\n  left: 165px;\n  padding: 4px;\n  opacity: 0;\n  bottom: 45px;\n  transform: translateY(calc(-50% + 16px));\n  transform-origin: 0 100%;\n  border-radius: 15px;\n  z-index: 99999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  transform: scale(0.5) translatY(calc(-50% + 16px), 10px);\n  pointer-events: none;\n}\n.shapeToolbar::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: #FFF;\n  z-index: -9999999;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  border-radius: 15px;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transition: filter 0.2s ease-in-out;\n}\n\n.shapeToolbar.show-toolbar {\n  opacity: 1 !important;\n  pointer-events: all !important;\n  transform: scale(1) translateY(calc(-50% + 16px), 0px) !important;\n}\n\n.TooltipContent {\n  border-radius: 30px;\n  background-color: #29292f;\n  padding: 10px 15px;\n  font-size: 12px;\n  margin-bottom: 10px;\n  bottom: 100px;\n  line-height: 1;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  z-index: 99999999 !important;\n  color: #fff;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  transition: opacity 0.3 ease-in-out !important;\n  will-change: transform, opacity;\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.hide-tooltip {\n  display: none !important;\n}\n\n.tooltip-tall {\n  margin-bottom: 20px;\n}\n\n.tooltip-small {\n  margin-bottom: 5px;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=top] {\n  animation-name: slideDownAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=right] {\n  animation-name: slideLeftAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=bottom] {\n  animation-name: slideUpAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=left] {\n  animation-name: slideRightAndFade;\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n#screenity-ui [data-radix-popper-content-wrapper] {\n  z-index: 99999999999 !important;\n}\n\n.override {\n  display: none !important;\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n\n.radial-menu {\n  position: absolute;\n  z-index: 9999999999999;\n  width: 100px;\n  height: 100px;\n  top: -66px;\n  left: -49px;\n  pointer-events: none;\n  opacity: 1;\n  transform: scale(1);\n  transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.25s ease-in-out;\n}\n.radial-menu::after {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: -1;\n  border: 28px solid #FFF;\n  box-sizing: border-box;\n  border-radius: 50%;\n  backdrop-filter: blur(40px);\n  opacity: 0;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transform: scale(0);\n  transition: transform 0.25s cubic-bezier(0.18, -0.55, 0.265, 1.45), opacity 0.2s ease-in-out;\n  transition-delay: 0.05s;\n}\n.radial-menu[data-state=open] {\n  transform: scale(1);\n  opacity: 1;\n  pointer-events: all !important;\n}\n.radial-menu[data-state=open]::after {\n  transform: scale(1);\n  border: 28px solid #FFF;\n  opacity: 1;\n}\n\n.color-wheel::after {\n  opacity: 0 !important;\n}\n\n.eyedropper {\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  width: 16px;\n  height: 16px;\n  padding: 8px;\n  z-index: 999999999;\n  background-color: #FFF;\n  border-radius: 50%;\n  opacity: 0;\n  text-align: center;\n  transition: transform 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.3s ease-in-out, background-color 0.25s ease-in-out !important;\n  transform: scale(0);\n  overflow: hidden;\n  transform-style: preserve-3d;\n}\n.eyedropper svg {\n  color: #9797a4;\n}\n.eyedropper:focus-visible {\n  outline: none;\n  background-color: #E6E7EA !important;\n}\n.eyedropper:hover {\n  cursor: pointer;\n  background-color: #E6E7EA;\n}\n\n.eye-active {\n  background-color: #3080f8 !important;\n}\n.eye-active svg {\n  color: #FFF !important;\n  fill: #FFF !important;\n}\n\n.color-wheel .eyedropper {\n  opacity: 0 !important;\n  pointer-events: none !important;\n  transform: scale(0) !important;\n}\n\n.radial-menu[data-state=open] .eyedropper {\n  transform: scale(1) !important;\n  opacity: 1;\n}\n\n.radial-menu-items {\n  transform: rotate(10deg);\n  z-index: 99999999;\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n}\n\n.radial-menu-item {\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n  width: 18px;\n  height: 18px;\n  z-index: 999;\n  border-radius: 50%;\n  text-align: center;\n  box-sizing: border-box;\n  line-height: 50px;\n  color: white;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  opacity: 0;\n}\n.radial-menu-item:hover {\n  cursor: pointer;\n}\n\n.color-wheel .radial-menu-item {\n  opacity: 0 !important;\n}\n\n.radial-menu-item-child {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  box-sizing: border-box;\n  border: 0px;\n  box-shadow: none;\n  top: 0px;\n  pointer-events: none;\n  z-index: 9999999;\n  left: 0px;\n  border-radius: 50%;\n  background-size: cover;\n}\n.radial-menu-item-child:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.radial-menu[data-state=open] .radial-menu-item-child {\n  pointer-events: all !important;\n}\n\n.radial-menu-item:nth-child(1), .wheel-trigger {\n  transform: rotate(0deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(1), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0s);\n  transform: rotate(0deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(2), .wheel-trigger {\n  transform: rotate(40deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(2), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.02s);\n  transform: rotate(40deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(3), .wheel-trigger {\n  transform: rotate(80deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(3), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.04s);\n  transform: rotate(80deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(4), .wheel-trigger {\n  transform: rotate(120deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(4), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.06s);\n  transform: rotate(120deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(5), .wheel-trigger {\n  transform: rotate(160deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(5), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.08s);\n  transform: rotate(160deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(6), .wheel-trigger {\n  transform: rotate(200deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(6), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.1s);\n  transform: rotate(200deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(7), .wheel-trigger {\n  transform: rotate(240deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(7), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.12s);\n  transform: rotate(240deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(8), .wheel-trigger {\n  transform: rotate(280deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(8), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.14s);\n  transform: rotate(280deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(9), .wheel-trigger {\n  transform: rotate(320deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(9), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(0.25s - 0.16s);\n  transform: rotate(320deg) translate(36px);\n  opacity: 1;\n}\n\n.color-active {\n  border: 1px solid #FFFFFF;\n  box-shadow: 0px 0px 0px 2px #0D99FF;\n}\n\n.color-wheel .color-active {\n  border: none !important;\n  box-shadow: none !important;\n}\n\n.wheel-trigger {\n  transition: transform 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), width 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  opacity: 0;\n  margin: auto;\n  width: 18px;\n  box-sizing: border-box;\n  height: 18px;\n  z-index: 9999;\n  box-sizing: border-box;\n  background-blend-mode: screen;\n  border-radius: 50%;\n}\n.wheel-trigger:hover {\n  cursor: pointer;\n}\n\n.wheel-trigger .radial-menu-item-child {\n  transform: rotate(30deg);\n}\n.wheel-trigger .radial-menu-item-child:after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  right: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 9999999;\n  border-radius: 50%;\n  box-sizing: border-box;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n}\n\n.color-wheel .wheel-trigger {\n  width: 100px !important;\n  height: 100px !important;\n  transform: rotate(320deg) translate(0px) !important;\n  z-index: 999999999999 !important;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n}\n\n.color-wheel-handle {\n  width: 12px !important;\n  height: 12px !important;\n  border-radius: 50%;\n  left: 20px;\n  top: 20px;\n  opacity: 0;\n  background-color: #F17FD7;\n  border: 2px solid white;\n  z-index: 999999999999;\n  display: none;\n  transition: width 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin-left 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin-top 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n}\n.color-wheel-handle:hover {\n  width: 18px !important;\n  height: 18px !important;\n  margin-left: -2px;\n  margin-top: -2px;\n}\n\n.color-wheel .color-wheel-handle {\n  opacity: 0;\n  display: block;\n  animation: fadeIn 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96) forwards;\n  animation-delay: 0.5s;\n}\n\n.w-color-wheel {\n  pointer-events: none;\n  position: absolute !important;\n  width: 100% !important;\n  height: 100% !important;\n  z-index: 99999999 !important;\n}\n.w-color-wheel::after {\n  content: \"\";\n  box-sizing: border-box;\n  display: block;\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  z-index: 9999999;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  box-sizing: border-box;\n  border-radius: 50%;\n}\n\n.color-wheel .w-color-wheel {\n  pointer-events: all !important;\n}\n\n.w-color-wheel-fill {\n  box-shadow: none !important;\n  border: 2px solid white !important;\n  width: 14px !important;\n  height: 14px !important;\n  transition: width 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96) !important;\n  box-sizing: border-box !important;\n  margin-left: -2px !important;\n  margin-top: -2px !important;\n  z-index: 9999999999 !important;\n}\n\n.w-color-wheel-pointer {\n  z-index: 99999999999 !important;\n  opacity: 0;\n  animation: none !important;\n}\n\n.color-wheel .w-color-wheel-pointer {\n  animation: fadeInScale 0.25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards !important;\n  animation-delay: 0.28s !important;\n}\n\n.w-color-wheel-fill:hover {\n  width: 18px !important;\n  height: 18px !important;\n  margin-left: -4px !important;\n  margin-top: -4px !important;\n}\n\n/* Fade in keyframes */\n@keyframes fadeInScale {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n.color-wheel, .color-wheel .wheel-trigger, .color-wheel .wheel-trigger .radial-menu-item-child, .color-wheel-handle {\n  cursor: pointer !important;\n}\n\n.color-wheel-input {\n  background: #000;\n  border-radius: 30px;\n  height: 29px;\n  color: #FFF;\n  text-align: center;\n  line-height: 29px;\n  padding-left: 8px;\n  padding-right: 8px;\n  position: absolute;\n  margin-top: -35px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  left: 50%;\n  transform: translate(-50%, 0);\n  opacity: 0;\n}\n\n.color-wheel .color-wheel-input {\n  animation: fadeIn 0.3s cubic-bezier(0.61, 0.11, 0.08, 0.96) forwards;\n  animation-delay: 0.2s;\n  pointer-events: all !important;\n}\n\n.color-wheel-input {\n  pointer-events: none;\n}\n\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n    margin-top: -35px;\n  }\n  100% {\n    opacity: 1;\n    margin-top: -40px;\n  }\n}\n.color-active .color-preview {\n  opacity: 1;\n}\n\n.radial-menu[data-state=closed] .color-preview {\n  opacity: 0 !important;\n}\n\n.color-preview {\n  width: 90%;\n  height: 90%;\n  box-sizing: border-box;\n  border-radius: 50%;\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  z-index: 9999999999;\n  transform: translate(-50%, -50%);\n  opacity: 0;\n  animation: none;\n  pointer-events: none;\n  border: 1px solid #FFF;\n  box-sizing: border-box;\n}\n\n.color-wheel .color-preview {\n  opacity: 0 !important;\n}\n\n.wheel-trigger .color-active {\n  box-shadow: none !important;\n}\n\n.color-active .w-color-wheel {\n  transform: scale(1.15) !important;\n}\n.color-active .w-color-wheel::after {\n  border: none !important;\n}\n\n.color-wheel .w-color-wheel {\n  transform: scale(1) !important;\n}\n.color-wheel .w-color-wheel::after {\n  border: 1px solid rgba(0, 0, 0, 0.2) !important;\n}\n\n.radial-menu[data-state=closed] .w-color-wheel {\n  transform: scale(1) !important;\n}\n\n.stroke-width-item span {\n  width: 18px;\n  height: 18px;\n  display: block;\n}\n.stroke-width-item div[data-state=on] {\n  background: #3080f8 !important;\n}\n.stroke-width-item div[data-state=on] svg {\n  color: #FFF !important;\n  fill: #FFF !important;\n}\n.stroke-width-item div[data-state=off] svg {\n  fill: #201F1D;\n}\n\n.stroke-icon svg {\n  text-align: center;\n  margin: auto;\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.ToastViewport {\n  --viewport-padding: 25px;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  margin: auto !important;\n  display: flex;\n  flex-direction: column;\n  padding: var(--viewport-padding);\n  gap: 14px;\n  max-width: 100vw;\n  width: -moz-fit-content;\n  width: fit-content;\n  list-style: none;\n  z-index: 2147483647;\n  outline: none;\n  pointer-events: all !important;\n}\n\n.ToastRoot {\n  background-color: #29292f;\n  color: #fff;\n  border-radius: 30px;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  padding: 10px 14px;\n  display: flex;\n  flex-direction: row;\n  gap: 8px;\n  font-size: 15px;\n  line-height: 1.5;\n  max-width: 100%;\n  overflow: hidden;\n  justify-content: center;\n  align-items: center;\n}\n\n.ToastRoot[data-state=open] {\n  animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.ToastRoot[data-state=closed] {\n  animation: hide 100ms ease-in;\n}\n\n.ToastRoot[data-swipe=move] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n\n.ToastRoot[data-swipe=cancel] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n\n.ToastRoot[data-swipe=end] {\n  animation: swipeOut 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@keyframes slideIn {\n  from {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n@keyframes swipeOut {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n}\n.ToastTitle {\n  color: #fff;\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.ToastDescription {\n  color: var(--slate-11);\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.ToastAction {\n  color: #fff;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  text-align: right;\n  background-color: #51515F;\n  padding: 0px 12px !important;\n  height: 24px !important;\n  cursor: pointer;\n}\n\n.toolbar-page {\n  width: 100%;\n  height: 100%;\n  pointer-events: none !important;\n}\n\n.popup-container:hover .popup-controls {\n  opacity: 1;\n}\n\n.open {\n  opacity: 1 !important;\n}\n\n.popup-drag-head {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100px;\n  z-index: 1;\n  border-radius: 30px 30px 0px 0px;\n  opacity: 0;\n}\n\n.popup-controls {\n  opacity: 0;\n  position: absolute;\n  top: -10px;\n  right: -10px;\n  box-sizing: border-box;\n  border-radius: 30px;\n  border: 1px solid #e8e8e8;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  z-index: 999999999;\n  background: rgb(240, 238, 238);\n  backdrop-filter: blur(10px);\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 6px;\n  padding-bottom: 6px;\n  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n}\n.popup-controls .popup-control svg {\n  color: #9797a4;\n  margin-bottom: -2px;\n}\n.popup-controls .popup-grab {\n  cursor: grab;\n}\n.popup-controls .popup-close {\n  cursor: pointer;\n}\n\n.tempimg {\n  height: 100%;\n  opacity: 1;\n  position: fixed;\n  right: 0px;\n  top: 20px;\n}\n\n.container {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  z-index: 999999999;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n}\n\n.ToolbarDragging .popup-container {\n  transform: scale(1.02);\n  filter: drop-shadow(0px 20px 50px rgba(0, 0, 0, 0.4)) !important;\n}\n\n/* Recording popup parent */\n.popup-container {\n  width: 356px;\n  position: fixed;\n  top: 32px;\n  right: 28px;\n  z-index: 99999999999;\n  filter: drop-shadow(0px 4px 100px rgba(0, 0, 0, 0.35));\n  pointer-events: all;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), filter 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n}\n\n.popup-container::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  transition: 2s;\n  background: #fff;\n  background-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  background-position: center bottom 50px;\n  border-radius: 30px;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n}\n\n/*\n.popup-shape {\n\twidth: 100%;\n\theight: 100%;\n\tbackground: $color-background;\n\tbackground-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  background-position: center bottom 50px;\n\tborder-radius: $container-border-radius;\n\tposition: relative;\n}\n*/\n.popup-cutout {\n  width: 44px;\n  height: 44px;\n  border-radius: 50%;\n  text-align: center;\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  top: -22px;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n}\n\n.popup-cutout img {\n  text-align: center;\n  margin: auto;\n  display: inline-block;\n  width: 100%;\n  border-radius: 50%;\n}\n\n/* Recording nav area */\n.popup-nav {\n  width: 100%;\n  position: relative;\n}\n\n/* Recording content area */\n.popup-content {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  border-radius: 30px;\n  overflow: hidden;\n}\n\n.waveform {\n  width: 100%;\n  margin-top: 12px;\n  margin-bottom: 12px;\n}\n\n.popup-content-divider {\n  width: 100%;\n  height: 1px;\n  background: #e8e8e8;\n  margin-top: 12px;\n  margin-bottom: 12px;\n}\n\n.popup-warning {\n  display: flex;\n  width: calc(100% + 32px);\n  height: 80px;\n  justify-content: space-between;\n  align-items: center;\n  position: relative;\n  overflow: hidden;\n  background-color: rgba(56, 126, 247, 0.1);\n  margin-left: -16px;\n  margin-top: -16px;\n  margin-bottom: 8px;\n}\n.popup-warning .popup-warning-right {\n  color: #3080f8;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  width: 90px;\n}\n\n.popup-warning-left,\n.popup-warning-right {\n  width: 50px;\n  display: flex;\n  align-items: center;\n  text-align: center;\n  height: 100%;\n  justify-content: center;\n}\n.popup-warning-left svg,\n.popup-warning-right svg {\n  color: #3080f8;\n}\n\n.popup-warning-right {\n  cursor: pointer;\n}\n\n.popup-warning-middle {\n  flex: 1;\n}\n.popup-warning-middle .popup-warning-title {\n  font-family: \"Satoshi-Bold\", sans-serif;\n  color: #29292f;\n}\n.popup-warning-middle .popup-warning-description {\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #6e7684;\n  margin-top: 4px;\n}\n\n.permission-button {\n  background: rgba(48, 128, 248, 0.1);\n  border-radius: 30px;\n  color: #3080f8;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 44px;\n  gap: 8px;\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n.permission-button:first-child {\n  margin-top: 4px !important;\n}\n.permission-button:last-child {\n  margin-bottom: 4px !important;\n}\n.permission-button:hover {\n  background: rgba(48, 128, 248, 0.15);\n  cursor: pointer;\n}\n.permission-button svg {\n  color: #3080f8;\n}\n\n.HelpSection {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  bottom: -40px;\n  background: #edeef2;\n  border-radius: 30px;\n  padding: 4px 12px;\n  text-align: center;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #6e7684;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  width: -moz-fit-content;\n  width: fit-content;\n}\n.HelpSection .HelpIcon {\n  margin-top: 3px;\n}\n.HelpSection:hover {\n  cursor: pointer;\n  background: #fefeff;\n}\n\n.CollapsibleTrigger {\n  margin-top: 12px;\n  font-weight: \"Satoshi-Bold\", sans-serif;\n  padding-top: 4px;\n  padding-bottom: 4px;\n  padding-left: 12px;\n  padding-right: 12px;\n  margin-left: auto;\n  margin-right: auto;\n  text-align: center;\n  display: block;\n  border-radius: 30px;\n}\n\n.CollapsibleTrigger:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.CollapsibleLabel {\n  color: #6e7684;\n  font-weight: \"Satoshi-Bold\", sans-serif;\n  text-align: center;\n  display: inline-block;\n}\n\n.CollapsibleLabel img {\n  margin-left: 4px;\n}\n\n.CollapsibleTrigger:hover {\n  cursor: pointer;\n  background: #FFF;\n}\n\n.CollapsibleRoot[data-state=open] > .CollapsibleTrigger > .CollapsibleLabel img {\n  transform: scaleY(-1);\n  margin-bottom: 2px;\n}\n\n.video-ui {\n  position: relative;\n  /* Blur the background */\n}\n\n.blurred {\n  /* Blur the background */\n}\n.blurred:before {\n  content: \"\";\n  position: absolute;\n  backdrop-filter: blur(5px);\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  background: rgba(255, 255, 255, 0.5);\n  z-index: 999;\n}\n\n.videos-list {\n  padding-bottom: 10px !important;\n  max-height: calc(96vh - 260px);\n  overflow-y: overlay;\n  padding: 16px;\n}\n\n.bottom-section {\n  width: 100%;\n  bottom: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  padding-top: 8px !important;\n  padding-bottom: 12px !important;\n  z-index: 999999;\n  padding: 16px;\n}\n\n.ModalSoon {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: -moz-fit-content;\n  height: fit-content;\n  width: 70%;\n  padding: 20px 16px;\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: 30px;\n  background: white;\n  box-shadow: 0px 4px 100px 0px rgba(0, 0, 0, 0.15);\n  z-index: 9999999;\n}\n\n.strong {\n  box-shadow: 0px 4px 100px -20px rgba(0, 0, 0, 0.75) !important;\n}\n\n.ModalSoonEmoji {\n  font-size: 20px;\n  margin-bottom: 8px;\n}\n\n.ModalSoonTitle {\n  font-weight: 700;\n  color: #29292f;\n  margin-bottom: 8px;\n  text-align: center;\n}\n\n.ModalSoonDescription {\n  text-align: center;\n  color: #6e7684;\n  text-align: center;\n  margin-bottom: 8px;\n}\n\n.ModalSoonButton {\n  margin-top: 8px;\n  color: #fff;\n  text-align: center;\n  border-radius: 30px;\n  background: radial-gradient(117.41% 117.78% at 35.44% 0%, #2baef8 23.13%, #3582f6 46.35%, #486def 74.48%, #7b9aea 100%);\n  padding: 8px 16px;\n}\n.ModalSoonButton:hover {\n  cursor: pointer;\n}\n\n.spinner-container,\n.empty-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 60px 0;\n  color: #6e7684;\n  font-size: 14px;\n}\n\n.spinner {\n  width: 32px;\n  height: 32px;\n  border: 3px solid rgba(0, 0, 0, 0.1);\n  border-top-color: #6e7684;\n  border-radius: 50%;\n  animation: spin 0.8s linear infinite;\n  margin-bottom: 10px;\n}\n\n@keyframes spin {\n  to {\n    transform: rotate(360deg);\n  }\n}\n.announcement {\n  width: 100%;\n  top: 0px;\n  left: 0px;\n  padding-bottom: 24px;\n}\n\n.announcement-wrap {\n  width: 85%;\n  margin: auto;\n}\n\n.announcement-hero {\n  width: 100%;\n  margin-bottom: 16px;\n  margin-top: 44px;\n}\n.announcement-hero img {\n  width: 100%;\n  border-radius: 15px;\n}\n\n.announcement-details {\n  text-align: center;\n}\n\n.announcement-title {\n  font-family: \"Satoshi-Bold\", sans-serif;\n  font-size: 16px;\n  color: #29292f;\n  letter-spacing: -0.8px;\n  margin-bottom: 12px;\n  text-align: center;\n}\n\n.announcement-description {\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n  color: #6e7684;\n  text-align: center;\n  margin-bottom: 24px;\n  line-height: 1.6;\n  letter-spacing: -0.6px;\n}\n.announcement-description a {\n  text-decoration: none !important;\n  color: #3080f8 !important;\n  cursor: pointer !important;\n}\n\n.announcement-cta {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  width: 100%;\n  height: 44px;\n  border-radius: 30px;\n  background: radial-gradient(117.41% 117.78% at 35.44% 0%, #2baef8 23.13%, #3582f6 46.35%, #486def 74.48%, #7b9aea 100%);\n  color: #fff;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n  cursor: pointer;\n  transition: all 0.2s ease-in-out;\n  animation: background-size 6s ease-in-out infinite;\n  animation-play-state: paused;\n  filter: drop-shadow(0px 4px 20px rgba(86, 123, 218, 0.5));\n}\n.announcement-cta:hover {\n  animation-play-state: running !important;\n}\n\n@keyframes background-size {\n  /* Animate scale and position in and out looping */\n  0% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n  50% {\n    background-size: 150% 150%;\n    background-position: 100% 0%;\n  }\n  100% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n}\n.welcome-title {\n  font-family: \"Satoshi-Bold\", sans-serif;\n  font-size: 18px;\n  color: #29292f;\n  letter-spacing: -0.8px;\n  margin-bottom: 12px;\n  text-align: center;\n}\n\n.welcome-description {\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 16px;\n  color: #6e7684;\n  text-align: center;\n  margin-bottom: 20px;\n  line-height: 1.6;\n  letter-spacing: -0.6px;\n}\n.welcome-description a {\n  text-decoration: none !important;\n  color: #3080f8 !important;\n  cursor: pointer !important;\n}\n\n.welcome-cta {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  width: 100%;\n  height: 44px;\n  border-radius: 30px;\n  background: white;\n  border: 1px solid #e8e8e8;\n  color: #29292f;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n  cursor: pointer;\n  transition: all 0.2s ease-in-out;\n  filter: drop-shadow(0px 1px 2px rgba(53, 87, 98, 0.1));\n}\n\n.welcome-content {\n  background: #f6f7fb;\n  border-bottom-left-radius: 30px;\n  border-bottom-right-radius: 30px;\n  max-height: calc(95vh - 300px);\n  overflow-y: overlay;\n  padding-top: 20px;\n  padding-bottom: 20px;\n  border-top: 1px solid #e8e8e8;\n}\n\n.welcome-content-wrap {\n  width: 85%;\n  margin: auto;\n}\n\n.welcome-content-title {\n  font-family: \"Satoshi-Bold\", sans-serif;\n  font-size: 14px;\n  color: #29292f;\n  margin-bottom: 8px;\n  margin-top: 8px;\n  text-align: center;\n  line-height: 1.4;\n}\n\n.welcome-feature-list {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 12px;\n}\n\n.welcome-feature-item {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n\n.welcome-feature-icon {\n  width: 20px;\n  height: 20px;\n  background: rgba(48, 128, 248, 0.1);\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.welcome-feature-icon-text {\n  font-size: 14px;\n  color: #6e7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.welcome-video {\n  margin: 25px 0 !important;\n  width: 100%;\n}\n\n.video-wrapper {\n  border-radius: 10px;\n}\n\n.welcome-support {\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 12px;\n  color: #6e7684 !important;\n  text-align: center;\n  margin-top: 20px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 6px;\n  text-decoration: none !important;\n}\n.welcome-support img {\n  width: 20px;\n}\n\n.DropdownMenuContent,\n.DropdownMenuSubContent {\n  min-width: 200px;\n  background-color: white;\n  margin-top: 4px;\n  margin-right: 8px;\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-radius: 15px;\n  z-index: 99999;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #29292f;\n  box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.DropdownMenuContent[data-side=top],\n.DropdownMenuSubContent[data-side=top] {\n  animation-name: slideDownAndFade;\n}\n\n.DropdownMenuContent[data-side=right],\n.DropdownMenuSubContent[data-side=right] {\n  animation-name: slideLeftAndFade;\n}\n\n.DropdownMenuContent[data-side=bottom],\n.DropdownMenuSubContent[data-side=bottom] {\n  animation-name: slideUpAndFade;\n}\n\n.DropdownMenuContent[data-side=left],\n.DropdownMenuSubContent[data-side=left] {\n  animation-name: slideRightAndFade;\n}\n\n.ItemIndicator,\n.ItemIndicatorArrow {\n  position: absolute;\n  right: 12px;\n  width: 18px;\n  height: 18px;\n  background: #3080f8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.ItemIndicatorArrow {\n  background: transparent !important;\n}\n\n.DropdownMenuItem,\n.DropdownMenuCheckboxItem,\n.DropdownMenuRadioItem,\n.DropdownMenuSubTrigger {\n  font-size: 14px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  height: 40px;\n  padding: 0 5px;\n  position: relative;\n  padding-left: 22px;\n  padding-right: 22px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  outline: none;\n}\n.DropdownMenuItem:hover,\n.DropdownMenuCheckboxItem:hover,\n.DropdownMenuRadioItem:hover,\n.DropdownMenuSubTrigger:hover {\n  background-color: #f6f7fb !important;\n  cursor: pointer;\n}\n\n.DropdownMenuSubTrigger[data-state=open] {\n  background-color: var(--violet-4);\n  color: var(--violet-11);\n}\n\n.DropdownMenuItem[data-disabled],\n.DropdownMenuCheckboxItem[data-disabled],\n.DropdownMenuRadioItem[data-disabled],\n.DropdownMenuSubTrigger[data-disabled] {\n  color: #6e7684 !important;\n  cursor: not-allowed;\n  background-color: #f6f7fb !important;\n}\n\n.DropdownMenuItem[data-highlighted],\n.DropdownMenuCheckboxItem[data-highlighted],\n.DropdownMenuRadioItem[data-highlighted],\n.DropdownMenuSubTrigger[data-highlighted] {\n  background-color: var(--violet-9);\n  color: var(--violet-1);\n}\n\n.DropdownMenuLabel {\n  padding-left: 25px;\n  font-size: 12px;\n  line-height: 25px;\n  color: var(--mauve-11);\n}\n\n.DropdownMenuSeparator {\n  height: 1px;\n  background-color: var(--violet-6);\n  margin: 5px;\n}\n\n.DropdownMenuItemIndicator {\n  position: absolute;\n  left: 0;\n  width: 25px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.DropdownMenuArrow {\n  fill: white;\n}\n\n.IconButton {\n  font-family: inherit;\n  border-radius: 100%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  margin-top: 4px;\n  cursor: pointer;\n}\n.IconButton svg {\n  color: #9797a4;\n}\n\n.IconButton:hover {\n  background-color: var(--violet-3);\n}\n\n.RightSlot {\n  margin-left: auto;\n  padding-left: 20px;\n  color: var(--mauve-11);\n}\n\n[data-highlighted] > .RightSlot {\n  color: white;\n}\n\n[data-disabled] .RightSlot {\n  color: var(--mauve-8);\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SelectTrigger {\n  display: inline-flex;\n  align-items: center;\n  justify-content: space-between;\n  border-radius: 30px;\n  line-height: 1;\n  height: 44px;\n  gap: 5px;\n  background-color: #fff;\n  color: #29292f;\n  width: 100%;\n  box-sizing: border-box;\n  margin-top: 4px;\n  margin-bottom: 4px;\n}\n\n.SelectTrigger:hover {\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n  cursor: pointer;\n}\n\n.SelectTrigger:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n.SelectTrigger[data-placeholder] {\n  color: var(--violet9);\n}\n\n.SelectTrigger[data-state=open] {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.SelectValue {\n  text-align: left;\n  flex: 1;\n  display: block;\n  width: 100%;\n  height: 100%;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  box-sizing: border-box;\n}\n.SelectValue span {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  height: 100%;\n  line-height: 44px;\n}\n\n.SelectIconDrop,\n.SelectIconType {\n  text-align: center;\n}\n\n.SelectIconType {\n  padding-left: 6px;\n  padding-right: 0px;\n}\n\n.SelectIconDrop {\n  padding-right: 16px;\n}\n\n.SelectTrigger[data-state=open] .SelectIconDrop img {\n  transform: rotate(180deg);\n}\n\n.SelectContent {\n  overflow: hidden;\n  z-index: 99999999999;\n  width: var(--radix-select-trigger-width);\n  max-height: var(--radix-select-content-available-height);\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: white;\n  border-radius: 15px;\n  margin-top: 4px;\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.SelectItem {\n  font-size: 14px;\n  line-height: 1;\n  color: var(--violet11);\n  display: flex;\n  align-items: center;\n  height: 44px;\n  padding-left: 16px;\n  padding-right: 16px;\n  color: #29292f;\n  position: relative;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.SelectItem[data-disabled] {\n  color: #6e7684;\n  pointer-events: none;\n}\n\n.SelectItem[data-highlighted] {\n  background: #f6f7fb;\n  outline: none !important;\n}\n\n.SelectItem:hover {\n  background: #f6f7fb;\n  cursor: pointer;\n}\n\n.SelectSeparator {\n  height: 1px;\n  background-color: #e8e8e8;\n  width: calc(100% - 24px);\n  margin: auto;\n  border-radius: 30px;\n  margin-top: 4px;\n  margin-bottom: 4px;\n}\n\n.SelectItemIndicator {\n  position: absolute;\n  right: 12px;\n  width: 24px;\n  height: 24px;\n  background: #3080f8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.SelectScrollButton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 25px;\n  background-color: white;\n  color: var(--violet11);\n  cursor: default;\n}\n\n.SelectOff {\n  background: #faf0f4;\n  color: #d2234d;\n  padding-left: 12px;\n  padding-right: 12px;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  margin-right: 4px;\n  border-radius: 30px;\n  font-size: 12px;\n  font-weight: 700;\n}\n\n.SelectIconButton {\n  border-radius: 30px;\n  position: relative;\n  padding: 8px;\n}\n.SelectIconButton:hover {\n  background-color: #f6f7fb;\n}\n\n/* Radix tabs navigation */\n/* reset */\nbutton,\nfieldset,\ninput {\n  all: unset;\n}\n\n.TabsRoot {\n  width: 100%;\n  margin: auto;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n}\n\n.TabsList {\n  margin: auto;\n  flex-shrink: 0;\n  display: flex;\n  width: -moz-fit-content;\n  width: fit-content;\n  align-items: center;\n}\n\n.hiddenTabs {\n  display: none !important;\n  pointer-events: none !important;\n  -webkit-user-select: none !important;\n     -moz-user-select: none !important;\n          user-select: none !important;\n  opacity: 0 !important;\n  height: 0 !important;\n  overflow: hidden !important;\n  transition: opacity 0.3s ease !important;\n}\n\n.TabsTrigger {\n  padding-left: 14px;\n  padding-right: 14px;\n  color: #6e7684;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  cursor: pointer;\n}\n\n.TabsTrigger[data-state=active] {\n  color: #29292f;\n}\n\n.TabsTrigger:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n.TabsTriggerSpacer {\n  height: 50px;\n  width: 1px;\n  background: #e8e8e8;\n  flex-shrink: 0;\n  margin-left: 8px;\n  margin-right: 8px;\n}\n\n/* Content of the Radix tabs */\n.TabsContent {\n  width: 100%;\n  display: block;\n  height: 100%;\n  box-sizing: border-box;\n  flex: 1 1 auto;\n}\n.TabsContent::after {\n  content: \"\";\n  display: block;\n  clear: both;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  width: 100%;\n  height: 30px;\n  background: linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.5) 100%);\n  pointer-events: none; /* Allow content behind the gradient to be clickable */\n}\n\n.TabsContent:focus {\n  outline: none;\n}\n\n.TabsContent:focus-visible {\n  box-shadow: inset 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.TabsContent[data-state=inactive] {\n  display: none;\n}\n\n/* Pill animation */\n.pill-anim {\n  position: absolute;\n  height: 32px;\n  top: 0px;\n  bottom: 0px;\n  margin-top: auto;\n  margin-bottom: auto;\n  border-radius: 30px;\n  background: #fff;\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n  transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n}\n\n/*\n.TabsList[data-value=\"record\"] {\n\t.pill-anim {\n\t\tleft: $spacing-03;\n\t\twidth: 102px;\n\t}\n}\n.TabsList[data-value=\"dashboard\"] {\n\t.pill-anim {\n\t\tleft: 109px;\n\t\twidth: 132px;\n\t}\n}\n*/\n/* Specific to the top level tabs */\n.TabsRoot.tl {\n  height: calc(100% - 40px);\n  margin-top: 40px;\n}\n\n.TabsList.tl {\n  border-radius: 30px;\n  background: #f6f7fb;\n  padding: 6px;\n  font-family: \"Satoshi-Bold\", sans-serif;\n  position: relative;\n}\n\n.TabsTrigger.tl {\n  border-radius: 30px;\n  background: transparent;\n  height: 32px;\n  display: flex;\n  align-items: center;\n  padding-left: 16px;\n  padding-right: 17px;\n  z-index: 2;\n  position: relative;\n}\n\n.TabsTrigger.tl[data-state=inactive]:hover :before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  box-sizing: border-box;\n  height: 100%;\n  width: calc(100% - 10px);\n  margin-left: 5px;\n  background: #edeef3;\n  z-index: -2;\n  left: 0px;\n  border-radius: 30px;\n}\n\n.TabsTriggerIcon {\n  width: 20px;\n  height: 20px;\n  text-align: center;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  margin-right: 4px;\n}\n\n/* Specific to recording tab context */\n.recording-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n.recording-ui .TabsRoot {\n  margin-top: 8px;\n}\n.recording-ui .TabsList {\n  width: 100%;\n  border-bottom: 1px solid #e8e8e8;\n  margin: auto;\n  justify-content: center;\n}\n.recording-ui .TabsTrigger {\n  padding-top: 8px;\n  padding-bottom: 12px;\n  box-sizing: border-box;\n  position: relative;\n  display: block;\n  padding-left: 16px;\n  padding-right: 16px;\n}\n.recording-ui .TabsTrigger:hover {\n  background: #f6f7fb;\n  border-top-right-radius: 15px;\n  border-top-left-radius: 15px;\n}\n.recording-ui .TabsTrigger:focus-visible {\n  border-radius: 10px 10px 0px 0px !important;\n}\n.recording-ui .TabsTrigger[data-state=active]::after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  width: 80%;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  height: 2px;\n  border-radius: 30px;\n  background: #3080f8;\n}\n.recording-ui .TabsTrigger[data-state=active] > .TabsTriggerLabel {\n  color: #29292f !important;\n}\n.recording-ui .TabsTriggerLabel {\n  text-align: center;\n}\n.recording-ui .TabsTriggerIcon {\n  width: 20px;\n  height: 20px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  margin: auto;\n  margin-bottom: 8px;\n  border-radius: 30px;\n}\n.recording-ui .TabsContent {\n  background: #f6f7fb;\n  padding: 16px;\n  border-bottom-left-radius: 30px;\n  border-bottom-right-radius: 30px;\n  max-height: calc(95vh - 200px);\n  overflow-y: overlay;\n}\n.recording-ui span {\n  display: block;\n}\n\n.video-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n.video-ui .TabsRoot {\n  margin-top: 8px;\n}\n.video-ui .TabsList {\n  width: 100%;\n  border-bottom: 1px solid #e8e8e8;\n  margin: auto;\n  justify-content: space-between;\n  padding-left: 12px;\n  padding-right: 12px;\n  box-sizing: border-box;\n}\n.video-ui .TabsTriggerWrap {\n  display: flex !important;\n  align-items: center;\n  flex-direction: row;\n  justify-content: left;\n  position: relative;\n  display: block;\n  box-sizing: border-box;\n}\n.video-ui .TabsTrigger {\n  padding-top: 8px;\n  padding-bottom: 12px;\n  box-sizing: border-box;\n  position: relative;\n  display: block;\n  padding-left: 20px;\n  padding-right: 20px;\n}\n.video-ui .TabsTrigger:hover {\n  background: #f6f7fb;\n  border-top-right-radius: 15px;\n  border-top-left-radius: 15px;\n}\n.video-ui .TabsTrigger:focus-visible {\n  border-radius: 10px 10px 0px 0px !important;\n}\n.video-ui .TabsTrigger[data-state=active]::after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  width: 80%;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  height: 2px;\n  border-radius: 30px;\n  background: #3080f8;\n}\n.video-ui .TabsTrigger[data-state=active] > .TabsTriggerLabel {\n  color: #29292f !important;\n}\n.video-ui .TabsTriggerLabel {\n  text-align: center;\n}\n.video-ui .TabsContent {\n  background: #f6f7fb;\n  border-bottom-left-radius: 30px;\n  border-bottom-right-radius: 30px;\n}\n.video-ui span {\n  display: block;\n}\n.video-ui .TabsSort {\n  margin-right: 12px;\n  border-radius: 30px;\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  margin-bottom: 5px;\n}\n.video-ui .TabsSort:hover {\n  cursor: pointer;\n  background: #f6f7fb;\n}\n.video-ui .TabsSortLabel {\n  display: flex;\n  flex-direction: row;\n  justify-content: right;\n  align-items: center;\n  color: #6e7684;\n  white-space: nowrap;\n}\n.video-ui .TabsSortLabel img {\n  margin-left: 8px;\n}\n.video-ui .TabsSort:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n.projectActiveBanner {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  background-color: #29292f; /* dark background */\n  color: white;\n  border-radius: 100px; /* pill shape */\n  padding: 10px 16px;\n  font-weight: 600;\n  font-size: 15px;\n  line-height: 1.3;\n  max-width: 80%;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  cursor: default;\n  gap: 16px;\n  top: 41px;\n  z-index: 999999;\n  margin: auto;\n  left: 0;\n  right: 0;\n  position: absolute;\n}\n\n.projectActiveBannerLeft {\n  /* text container */\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.projectActiveBannerRight {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\n.projectActiveBannerDivider {\n  width: 1px;\n  height: 24px;\n  background-color: #44444d; /* subtle divider color */\n}\n\n.projectActiveBannerClose {\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.projectActiveBannerClose img {\n  width: 14px;\n  height: 14px;\n  opacity: 0.6;\n}\n.projectActiveBannerClose img:hover {\n  opacity: 1;\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SwitchRow {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 40px;\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  -o-user-select: none;\n}\n\n.SwitchRow * {\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  -o-user-select: none;\n}\n\n.SwitchRoot {\n  width: 34px;\n  height: 22px;\n  background-color: #e8e8e8;\n  border-radius: 9999px;\n  position: relative;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\n.SwitchRoot[disabled] {\n  opacity: 0.5;\n  cursor: not-allowed;\n}\n\n.SwitchRoot:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.SwitchRoot[data-state=checked] {\n  background-color: #3080f8;\n}\n\n.SwitchRoot:hover {\n  cursor: pointer;\n}\n\n.SwitchThumb {\n  display: block;\n  width: 14px;\n  height: 14px;\n  background-color: white;\n  border-radius: 9999px;\n  box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1);\n  transition: transform 100ms;\n  transform: translateX(4px);\n  will-change: transform;\n}\n\n.SwitchThumb[data-state=checked] {\n  transform: translateX(16px);\n}\n\n.Label {\n  color: #6e7684;\n  display: inline-block !important;\n  /* Ellipsis */\n  text-overflow: clip;\n  white-space: nowrap;\n}\n.Label:hover {\n  text-overflow: clip;\n}\n\n.ExperimentalLabel {\n  color: #fff;\n  font-size: 12px;\n  background-color: #3080f8;\n  border-radius: 15px;\n  padding: 2px 8px;\n  display: inline-block !important;\n  margin-left: 8px;\n}\n\n.labelDropdownWrap {\n  display: inline-block;\n  vertical-align: middle;\n  position: relative;\n  border-radius: 30px;\n  box-sizing: border-box;\n}\n.labelDropdownWrap img {\n  display: inline-block;\n  margin-left: 6px;\n}\n.labelDropdownWrap .labelDropdown {\n  display: inline-block;\n}\n.labelDropdownWrap:hover {\n  cursor: pointer;\n}\n.labelDropdownWrap::after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  border-radius: 30px;\n  border-left: 8px solid transparent;\n  border-right: 8px solid transparent;\n  border-top: 4px solid transparent;\n  border-bottom: 4px solid transparent;\n  margin-top: -4px;\n  margin-left: -8px;\n}\n.labelDropdownWrap:hover::after {\n  border-color: #fff;\n}\n.labelDropdownWrap:hover {\n  background-color: #fff;\n}\n\n.labelDropdownActive .labelDropdownContent {\n  display: block !important;\n}\n.labelDropdownActive img {\n  transform: rotate(180deg);\n}\n\n.labelDropdownContent {\n  position: absolute;\n  background-color: #fff;\n  min-width: 160px;\n  box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);\n  z-index: 9999999999;\n  border-radius: 15px;\n  padding: 8px 0px;\n  margin-top: 4px;\n  border: 1px solid #e8e8e8;\n  display: none;\n}\n.labelDropdownContent .labelDropdownContentItem {\n  color: #29292f;\n  padding: 12px 16px;\n  text-decoration: none;\n  display: block;\n}\n.labelDropdownContent .labelDropdownContentItem:hover {\n  background-color: #f6f7fb;\n  cursor: pointer;\n}\n\n.video-item-root {\n  width: calc(100% - 2 * 8px);\n  border-radius: 15px;\n  padding: 8px;\n  display: block;\n}\n.video-item-root .video-item {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n}\n.video-item-root .video-item-left {\n  display: flex;\n  flex-grow: 1;\n  flex-direction: row;\n  align-items: center;\n  min-width: 0;\n  justify-content: left;\n}\n.video-item-root .video-item-thumbnail {\n  min-width: 48px;\n  width: 48px;\n  height: 38px;\n  background: grey;\n  border-radius: 5px;\n  margin-right: 12px;\n}\n.video-item-root .video-item-info {\n  display: block;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  padding-right: 4px;\n}\n.video-item-root .video-item-info-title {\n  color: #29292f;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n.video-item-root .video-item-info-date {\n  margin-top: 4px;\n  color: #6e7684;\n  font-size: 12px;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n\n.video-item-root:hover {\n  cursor: pointer;\n  background: #e9eaee;\n}\n\n.video-item-root:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n/* Actions */\n.video-item-right {\n  display: flex;\n  align-items: center;\n  justify-content: right;\n  gap: 8px;\n  min-width: -moz-max-content;\n  min-width: max-content;\n  opacity: 0;\n}\n.video-item-right .copy-link {\n  background: #fff;\n  height: 32px;\n  width: 32px;\n  border-radius: 10px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  align-items: center;\n  z-index: 999999;\n}\n.video-item-right .copy-link:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n.video-item-right .more-actions {\n  height: 32px;\n  width: 32px;\n  background: #fff;\n  text-align: center;\n  border-radius: 10px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.video-item-right .more-actions:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n.video-item-root:hover .video-item-right,\n.video-item-root:focus-visible .video-item-right {\n  opacity: 1;\n}\n\n.main-button {\n  width: 100%;\n  height: 45px;\n  border-radius: 30px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  align-items: center;\n  position: relative;\n  box-sizing: border-box;\n}\n.main-button .main-button-label {\n  color: #fff;\n  text-align: center;\n  vertical-align: middle;\n  align-items: center;\n}\n.main-button .main-button-shortcut {\n  position: absolute;\n  font-size: 12px;\n  right: 16px;\n  color: #fff;\n  opacity: 0.7;\n}\n.main-button:hover {\n  cursor: pointer;\n}\n.main-button:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.main-button:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n@property --x {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 35.44%;\n}\n@property --y {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 0%;\n}\n.recording-button {\n  margin-top: 8px;\n  filter: drop-shadow(0px 4px 20px rgba(86, 123, 218, 0.5));\n  background: radial-gradient(127.41% 127.78% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n  animation: 0;\n  animation: background-size 6s ease-in-out infinite;\n  animation-play-state: paused;\n  position: relative;\n  z-index: 2;\n}\n\n@keyframes background-size {\n  /* Animate scale and position in and out looping */\n  0% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n  50% {\n    background-size: 150% 150%;\n    background-position: 100% 0%;\n  }\n  100% {\n    background-size: 100% 100%;\n    background-position: 0% 0%;\n  }\n}\n.recording-button:hover {\n  animation-play-state: running !important;\n}\n\n.recording-button:before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  box-sizing: border-box;\n  border-radius: 30px;\n  transition: all 0.25s ease-in-out;\n}\n\n.recording-button:hover:before {\n  box-shadow: 0px 0px 0px 4px rgba(52, 138, 247, 0.25);\n}\n\n@keyframes pulse-animation {\n  0% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n  25% {\n    box-shadow: 0px 0px 0px 6px rgba(52, 138, 247, 0.25);\n  }\n  50% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n  100% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n}\n@keyframes gradient-animation {\n  0% {\n    --x: 35.44%;\n    --y: 0%;\n  }\n  25% {\n    --x: 100%;\n    --y: 30%;\n  }\n  50% {\n    --x: 70%;\n    --y: 100%;\n  }\n  75% {\n    --x: 30%;\n    --y: 90%;\n  }\n  100% {\n    --x: 35.44%;\n    --y: 0%;\n  }\n}\n.dashboard-button {\n  background: #29292f;\n  box-shadow: 0px 0px 0px 0px rgba(41, 41, 47, 0.25);\n  transition: all 0.25s ease-in-out;\n}\n\n.dashboard-button:hover {\n  box-shadow: 0px 0px 0px 4px rgba(41, 41, 47, 0.25);\n}\n\n.alarm-time-button {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  border-radius: 15px;\n  padding: 4px 8px;\n  position: absolute;\n  color: #fff;\n  opacity: 0.7;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 12px;\n  left: 6px;\n}\n.alarm-time-button svg {\n  margin-top: 4px;\n  margin-right: 4px;\n  width: 14px;\n}\n\n.background-effects-toggle-group {\n  display: flex;\n  height: 40px;\n  width: 100%;\n  gap: 8px;\n  margin-bottom: 8px;\n  margin-top: 8px;\n}\n\n.background-effect {\n  display: flex;\n  width: 40px;\n  height: 40px;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  position: relative;\n  color: #FFF;\n}\n.background-effect span {\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 12px;\n  font-weight: 600;\n  z-index: 99999;\n  font-weight: 500;\n}\n.background-effect[data-state=on]::after {\n  content: \"\";\n  border-radius: 50%;\n  display: block;\n  width: 46px;\n  height: 46px;\n  position: absolute;\n  border: 2px solid #3080f8;\n  box-sizing: border-box;\n}\n.background-effect img {\n  width: 100%;\n  height: 100%;\n  border-radius: 50%;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n}\n.background-effect:hover:not([data-state=on]) {\n  cursor: pointer;\n}\n.background-effect:hover:not([data-state=on])::after {\n  content: \"\";\n  border-radius: 50%;\n  display: block;\n  width: 46px;\n  height: 46px;\n  position: absolute;\n  border: 2px solid #3080f8;\n  opacity: 0.5;\n  box-sizing: border-box;\n}\n.background-effect:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.region-dimensions {\n  width: 100%;\n  display: flex;\n  gap: 10px;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n\n.region-input {\n  flex: 1;\n  position: relative;\n}\n.region-input input {\n  color: #29292f !important;\n  border-radius: 30px;\n  height: 40px;\n  box-sizing: border-box;\n  position: relative;\n  width: 100%;\n  padding-left: 18px;\n  padding-right: 40px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: #fff;\n}\n.region-input input:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n.region-input span {\n  color: #6e7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  position: absolute;\n  right: 18px;\n  bottom: 12px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.time-set-parent {\n  width: 100%;\n  display: flex;\n  gap: 10px;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n\n.time-set-input {\n  flex: 1;\n  position: relative;\n}\n.time-set-input input {\n  border-radius: 30px;\n  height: 40px;\n  box-sizing: border-box;\n  position: relative;\n  width: 100%;\n  padding-left: 18px;\n  padding-right: 40px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: #fff;\n  -webkit-appearance: textfield;\n  -moz-appearance: textfield;\n  appearance: textfield;\n}\n.time-set-input input:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n.time-set-input span {\n  color: #6e7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  position: absolute;\n  right: 18px;\n  bottom: 12px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.TooltipContent {\n  border-radius: 30px;\n  background-color: #29292f;\n  padding: 10px 15px;\n  font-size: 12px;\n  line-height: 1;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  z-index: 99999999 !important;\n  color: #fff;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  transition: opacity 0.3 ease-in-out !important;\n  will-change: transform, opacity;\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.hide-tooltip {\n  display: none !important;\n}\n\n.tooltip-tall {\n  margin-bottom: 20px;\n}\n\n.tooltip-small {\n  margin-bottom: 5px;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=top] {\n  animation-name: slideDownAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=right] {\n  animation-name: slideLeftAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=bottom] {\n  animation-name: slideUpAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=left] {\n  animation-name: slideRightAndFade;\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n#screenity-ui [data-radix-popper-content-wrapper] {\n  z-index: 99999999999 !important;\n}\n\n.override {\n  display: none !important;\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n\n.CanvasContainer {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  pointer-events: all !important;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999 !important;\n}\n\n.canvas {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999 !important;\n}\n\n.canvas-container {\n  width: 100vw !important;\n  height: 100vh !important;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999;\n  position: absolute !important;\n}\n\n.camera-draggable {\n  width: 100%;\n  height: 100%;\n  transform-origin: left top;\n  border-radius: 50%;\n}\n\n.camera-grab {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  border-radius: 50%;\n  z-index: 99999999 !important;\n  cursor: grab;\n}\n\n.camera-flipped {\n  transform: scaleX(-1);\n}\n\n.camera-toolbar {\n  display: flex;\n  align-items: center;\n  padding-left: 4px;\n  padding-right: 4px;\n  transition: opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  min-width: -moz-max-content;\n  min-width: max-content;\n  background-color: rgba(30, 30, 30, 0.8);\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  height: 28px;\n  position: absolute;\n  left: 10px;\n  top: 10px;\n  border-radius: 30px;\n  backdrop-filter: blur(10px);\n  z-index: 99999999999;\n  opacity: 0;\n  border: 3px solid rgba(255, 255, 255, 0.2);\n}\n\n.camera-draggable:hover .camera-toolbar, .camera-draggable:hover .camera-resize {\n  opacity: 1 !important;\n}\n\n.camera-toolbar:hover, .camera-resize:hover {\n  opacity: 1 !important;\n}\n\n.CameraToolbarSeparator {\n  width: 1px;\n  height: 18px;\n  background-color: rgba(255, 255, 255, 0.3);\n  margin: 0 4px;\n}\n\n.CameraToggleItem, .CameraToolbarButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #000;\n  height: 22px;\n  width: 22px;\n  text-align: center;\n  font-size: 13px;\n  line-height: 1;\n  border-radius: 50%;\n  transition: background-color 0.25s ease-in-out;\n  background-color: rgba(124, 139, 165, 0);\n}\n.CameraToggleItem svg, .CameraToolbarButton svg {\n  color: #9797a4;\n}\n.CameraToggleItem:hover, .CameraToolbarButton:hover {\n  background-color: rgba(124, 139, 165, 0.2) !important;\n  cursor: pointer;\n}\n.CameraToggleItem:disabled, .CameraToolbarButton:disabled {\n  opacity: 0.5;\n  pointer-events: none;\n}\n.CameraToggleItem[data-state=on], .CameraToolbarButton[data-state=on] {\n  color: #FFF;\n}\n.CameraToggleItem[data-state=on] svg, .CameraToolbarButton[data-state=on] svg {\n  color: #FFF;\n}\n\n.CameraToggleItem:hover, .CameraToggleButton:hover {\n  cursor: pointer;\n}\n\n.CameraToggleItem:focus-visible, .CameraToggleButton:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.CameraToggleGroup {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.CameraToggleGroup, .CameraToolbarSeparator {\n  display: none;\n}\n\n.camera-resize {\n  position: absolute;\n  bottom: 20px;\n  right: 20px;\n  z-index: 99999999999;\n  height: 28px;\n  width: 28px;\n  border-radius: 50%;\n  background-color: rgba(30, 30, 30, 0.8);\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  border: 3px solid rgba(255, 255, 255, 0.2);\n  backdrop-filter: blur(10px);\n  align-items: center;\n  box-sizing: border-box;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0;\n}\n.camera-resize svg {\n  color: #9797A4;\n  text-align: center;\n  margin: auto;\n  display: block;\n}\n\n.countdown {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 99999999999;\n}\n\n.countdown-circle {\n  width: 200px;\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 999;\n  text-align: center;\n}\n\n.countdown-overlay {\n  width: 100%;\n  height: 100%;\n  background: rgba(0, 0, 0, 0.5);\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 99;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.countdown-number {\n  position: absolute;\n  width: 20px;\n  height: 60px;\n  z-index: 9999999;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  font-weight: 300 !important;\n  font-family: \"Satoshi-Light\", sans-serif !important;\n  font-size: 48px !important;\n  line-height: 60px !important;\n  letter-spacing: normal !important;\n  text-transform: none !important;\n  word-spacing: normal !important;\n  color: #fff;\n  text-align: center;\n  display: block;\n  transition: all 0.6s ease-in-out;\n}\n\n.background {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  filter: url(\"#goo\");\n  transform: rotate(0deg);\n  transition: all 3s ease-in-out;\n}\n\n.circle {\n  z-index: 9;\n  position: absolute;\n  transform: scale(0.8);\n  top: 0px;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: 50%;\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #2baef8 23.13%, #3582f6 46.35%, #486def 74.48%, #7b9aea 100%);\n  width: 200px;\n  height: 200px;\n  transition: all 1.5s ease-in-out;\n}\n\n.c {\n  width: 50px;\n  height: 50px;\n  z-index: 999;\n  border-radius: 50%;\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  opacity: 1;\n}\n\n.c2 {\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #2b96f8 23.13%, #356bf6 64.58%);\n  transform: translate(20px, 20px);\n}\n\n.c3 {\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #4884ca 15.3%, #2b89f8 78.83%);\n  transform: translate(-30px, -40px);\n}\n\n.c3:after {\n  content: \"\";\n  position: absolute;\n  width: 150px;\n  height: 150px;\n  filter: blur(50px);\n  border-radius: 50%;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  background: #cbe8f7;\n  z-index: -1;\n}\n\n.recording-countdown {\n  pointer-events: all;\n}\n\n.recording-countdown .c2 {\n  transform: translate(-15px, 15px);\n}\n\n.recording-countdown .c3 {\n  transform: translate(-10px, -5px);\n}\n\n.countdown-info {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  bottom: 20px;\n  border-radius: 30px;\n  border: 2px solid rgba(255, 255, 255, 0.3);\n  text-align: center;\n  display: block;\n  padding: 10px 20px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n  line-height: 1.4;\n  letter-spacing: normal;\n  text-transform: none;\n  color: #fff;\n  z-index: 99999999999;\n  width: -moz-fit-content;\n  width: fit-content;\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.AlertDialogOverlay {\n  background-color: rgba(0, 0, 0, 0.5);\n  position: fixed;\n  inset: 0;\n  animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent {\n  overflow: auto !important;\n  background-color: white;\n  border-radius: 30px;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 90vw;\n  max-width: 500px;\n  max-height: 85vh;\n  padding: 35px 25px;\n  animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent:focus {\n  outline: none;\n}\n\n.AlertDialogTitle {\n  margin: 0;\n  color: #29292f;\n  font-size: 14px;\n  line-height: 1.4;\n  font-family: \"Satoshi-Bold\", sans-serif;\n  font-weight: 700;\n}\n\n.AlertDialogDescription {\n  margin-bottom: 20px;\n  color: #6e7684;\n  font-size: 14px;\n  line-height: 1.5;\n}\n.AlertDialogDescription a {\n  color: #3080f8 !important;\n  font-weight: 600 !important;\n  text-decoration: none !important;\n  display: inline-block;\n  cursor: pointer;\n}\n\n.Button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n}\n\n.Button.blue {\n  background-color: rgba(48, 128, 248, 0.1);\n  color: #3080f8;\n}\n.Button.blue:hover {\n  background-color: rgba(48, 128, 248, 0.15);\n  cursor: pointer;\n}\n.Button.blue:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.Button.red {\n  background-color: rgba(247, 56, 90, 0.1);\n  color: rgb(247, 56, 90);\n}\n\n.Button.red:hover {\n  background-color: rgba(247, 56, 90, 0.15);\n  cursor: pointer;\n}\n\n.Button.red:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.Button.grey {\n  background: rgba(110, 118, 132, 0.1);\n  color: #6e7684;\n}\n\n.Button.grey:hover {\n  background: rgba(110, 118, 132, 0.15);\n  cursor: pointer;\n}\n\n.Button.grey:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n@keyframes overlayShow {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes contentShow {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -48%) scale(0.96);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%) scale(1);\n  }\n}\n.SideButtonModal {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n  color: #6e7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n.SideButtonModal:hover {\n  cursor: pointer;\n  background: rgba(110, 118, 132, 0.05);\n}\n\n.box-hole {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 9999999999;\n}\n\n.resize-handle {\n  position: absolute;\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background-color: white;\n  border: 2px solid rgba(0, 0, 0, 0.5);\n  box-sizing: border-box;\n}\n\n.resize-handle.top-left {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nwse-resize;\n}\n\n.resize-handle.top {\n  top: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n\n.resize-handle.top-right {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nesw-resize;\n}\n\n.resize-handle.right {\n  top: 50%;\n  right: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n\n.resize-handle.bottom-right {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nwse-resize;\n}\n\n.resize-handle.bottom {\n  bottom: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n\n.resize-handle.bottom-left {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nesw-resize;\n}\n\n.resize-handle.left {\n  top: 50%;\n  left: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n\n.region-recording * {\n  pointer-events: none !important;\n}\n\n.WarningViewport {\n  --viewport-padding: 25px;\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  margin: auto !important;\n  display: flex;\n  flex-direction: column;\n  padding: var(--viewport-padding);\n  gap: 14px;\n  max-width: 100vw;\n  width: -moz-fit-content;\n  width: fit-content;\n  list-style: none;\n  z-index: 2147483647;\n  outline: none;\n  pointer-events: all !important;\n}\n\n.warning-root {\n  background-color: #29292f;\n  color: #fff;\n  border-radius: 30px;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  padding: 14px 20px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  gap: 8px;\n  font-size: 15px;\n  line-height: 1.5;\n  max-width: 350px;\n  overflow: hidden;\n  align-items: center;\n  text-align: left;\n  align-items: flex-start;\n}\n\n.warning-content {\n  display: flex;\n  flex-direction: column;\n  justify-content: left;\n  align-items: flex-start;\n  gap: 8px;\n  width: 100%;\n}\n\n.warning-root[data-state=open] {\n  animation: slideIn2 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.warning-root[data-state=closed] {\n  animation: hide 100ms ease-in;\n}\n\n.warning-root[data-swipe=move] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n\n.warning-root[data-swipe=cancel] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n\n.warning-root[data-swipe=end] {\n  animation: swipeOut2 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0 !important;\n  }\n}\n@keyframes slideIn2 {\n  from {\n    transform: translateY(calc(-100% - var(--viewport-padding)));\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n@keyframes swipeOut2 {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(-100% - var(--viewport-padding)));\n  }\n}\n.warning-title {\n  color: #fff;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  line-height: 1.4;\n}\n\n.warning-description {\n  color: #fff;\n  opacity: 0.8;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  line-height: 1.5;\n}\n\n.ToastAction {\n  color: #fff;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  text-align: right;\n  background-color: #51515f;\n  padding: 0px 12px !important;\n  height: 24px !important;\n  cursor: pointer;\n}\n\n.warning-close {\n  z-index: 999999;\n}\n\n.warning-close:hover {\n  cursor: pointer;\n}\n\n:host {\n  font-size: 16px;\n  line-height: normal;\n  letter-spacing: normal;\n  word-spacing: normal;\n  text-transform: none;\n  font-style: normal;\n  font-variant: normal;\n  text-indent: 0;\n  direction: ltr;\n  white-space: normal;\n}\n\n.screenity-scrollbar::-webkit-scrollbar {\n  background-color: rgba(0, 0, 0, 0);\n  width: 16px;\n  height: 16px;\n  z-index: 999999;\n}\n\n.screenity-scrollbar::-webkit-scrollbar-track {\n  background-color: rgba(0, 0, 0, 0);\n}\n\n.screenity-scrollbar::-webkit-scrollbar-thumb {\n  background-color: rgba(0, 0, 0, 0);\n  border-radius: 16px;\n  border: 0px solid #fff;\n}\n\n.screenity-scrollbar::-webkit-scrollbar-button {\n  display: none;\n}\n\n.screenity-scrollbar:hover::-webkit-scrollbar-thumb {\n  background-color: #a0a0a5;\n  border: 4px solid #fff;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background-color: #a0a0a5;\n  border: 4px solid #f4f4f4;\n}\n\n.ScreenityDropdownMenuContent {\n  min-width: 200px;\n  background-color: white;\n  margin-top: 4px;\n  margin-right: 8px;\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-radius: 15px;\n  z-index: 99999;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #29292f;\n  box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2);\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.ScreenityDropdownMenuContent[data-side=top] {\n  animation-name: slideDownAndFade;\n}\n\n.ScreenityDropdownMenuContent[data-side=right] {\n  animation-name: slideLeftAndFade;\n}\n\n.ScreenityDropdownMenuContent[data-side=bottom] {\n  animation-name: slideUpAndFade;\n}\n\n.ScreenityDropdownMenuContent[data-side=left] {\n  animation-name: slideRightAndFade;\n}\n\n.ScreenityItemIndicator {\n  position: absolute;\n  right: 12px;\n  width: 18px;\n  height: 18px;\n  background: #3080f8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.ScreenityDropdownMenuItem,\n.ScreenityDropdownMenuRadioItem {\n  font-size: 14px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  height: 40px;\n  padding: 0 5px;\n  position: relative;\n  padding-left: 22px;\n  padding-right: 22px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  outline: none;\n}\n\n.ScreenityDropdownMenuItem:hover {\n  background-color: #f6f7fb !important;\n  cursor: pointer;\n}\n\n.ScreenityDropdownMenuItem[data-disabled] {\n  color: #6e7684 !important;\n  cursor: not-allowed;\n  background-color: #f6f7fb !important;\n}\n\n.driver-popover.ScreenityOnboardingPopover,\n.ScreenityOnboardingPopover,\n.onboarding-popover {\n  border-radius: 30px !important;\n  background: #fff !important;\n  color: #29292f !important;\n  font-family: \"Satoshi-Regular\", sans-serif !important;\n  box-shadow: 0 4px 20px rgba(38, 38, 52, 0.08) !important;\n  padding: 20px !important;\n  max-width: 340px !important;\n  font-size: 14px !important;\n  z-index: 99999999999 !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-title,\n.ScreenityOnboardingPopover .driver-popover-title,\n.onboarding-popover .driver-popover-title {\n  font-size: 1rem !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  margin-bottom: 12px !important;\n  color: #29292f !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-description,\n.ScreenityOnboardingPopover .driver-popover-description,\n.onboarding-popover .driver-popover-description {\n  font-size: 14px !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  color: #6e7684 !important;\n  line-height: 1.5 !important;\n  margin-bottom: 18px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-description a,\n.ScreenityOnboardingPopover .driver-popover-description a,\n.onboarding-popover .driver-popover-description a {\n  color: #3b82f6 !important;\n  font-family: \"Satoshi-Bold\", sans-serif !important;\n  text-decoration: none !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-close-btn,\n.onboarding-popover .driver-popover-close-btn {\n  top: 14px;\n  right: 12px;\n  color: #6e7684 !important;\n  font-size: 1.5rem !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow,\n.ScreenityOnboardingPopover .driver-popover-arrow,\n.onboarding-popover .driver-popover-arrow {\n  width: 0px;\n  height: 0px;\n  background: none !important;\n  box-shadow: none !important;\n  box-sizing: border-box;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-top,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-top,\n.onboarding-popover .driver-popover-arrow-side-top {\n  border-left: 8px solid transparent !important;\n  border-right: 8px solid transparent !important;\n  border-top: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-bottom,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-bottom,\n.onboarding-popover .driver-popover-arrow-side-bottom {\n  border-left: 8px solid transparent !important;\n  border-right: 8px solid transparent !important;\n  border-bottom: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-left,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-left,\n.onboarding-popover .driver-popover-arrow-side-left {\n  border-top: 8px solid transparent !important;\n  border-bottom: 8px solid transparent !important;\n  border-left: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-side-right,\n.ScreenityOnboardingPopover .driver-popover-arrow-side-right,\n.onboarding-popover .driver-popover-arrow-side-right {\n  border-top: 8px solid transparent !important;\n  border-bottom: 8px solid transparent !important;\n  border-right: 8px solid #fff !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-start,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-start,\n.onboarding-popover .driver-popover-arrow-align-start {\n  top: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-end,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-end,\n.onboarding-popover .driver-popover-arrow-align-end {\n  bottom: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-left,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-left,\n.onboarding-popover .driver-popover-arrow-align-left {\n  left: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-arrow-align-right,\n.ScreenityOnboardingPopover .driver-popover-arrow-align-right,\n.onboarding-popover .driver-popover-arrow-align-right {\n  right: 35px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-progress-text,\n.ScreenityOnboardingPopover .driver-popover-progress-text,\n.onboarding-popover .driver-popover-progress-text {\n  font-size: 0.8rem !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  color: #6e7684 !important;\n  opacity: 0.7 !important;\n  margin-top: 2px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer,\n.ScreenityOnboardingPopover .driver-popover-footer,\n.onboarding-popover .driver-popover-footer {\n  display: flex !important;\n  justify-content: flex-end;\n  gap: 8px;\n  margin-top: 16px;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns {\n  gap: 6px !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn {\n  background-color: #3080f8 !important;\n  color: white !important;\n  border: none !important;\n  padding: 10px 14px !important;\n  border-radius: 30px !important;\n  font-family: \"Satoshi-Medium\", sans-serif !important;\n  font-weight: 500 !important;\n  cursor: pointer !important;\n  font-size: 0.875rem !important;\n  text-shadow: none !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-next-btn:hover {\n  background: rgb(23.3341121495, 112.8668224299, 247.1658878505) !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn {\n  background-color: transparent !important;\n  color: #29292f !important;\n  border: 1px solid #e8e8e8 !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-prev-btn:hover {\n  background: #f6f7fb !important;\n}\n.driver-popover.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.ScreenityOnboardingPopover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn,\n.onboarding-popover .driver-popover-footer .driver-popover-navigation-btns .driver-popover-close-btn {\n  background-color: transparent !important;\n  color: #6e7684 !important;\n}\n\n.driver-overlay {\n  z-index: 2147483645 !important;\n}\n\n.driver-stage {\n  z-index: 2147483646 !important;\n}\n\n.driver-popover,\n.driver-popover.ScreenityOnboardingPopover {\n  z-index: 2147483647 !important;\n}/*# sourceMappingURL=app.css.map */"
  },
  {
    "path": "src/pages/Content/styles/app.scss",
    "content": "@use \"../toolbar/styles/_Page.scss\";\n@use \"../popup/styles/_Popup.scss\";\n@use \"../canvas/styles/_Canvas.scss\";\n@use \"../camera/styles/_Camera.scss\";\n@use \"../countdown/styles/_Countdown.scss\";\n@use \"../modal/styles/_Modal.scss\";\n@use \"../region/styles/_Region.scss\";\n@use \"../camera-only/styles/CameraOnly.scss\";\n@use \"../warning/styles/Warning.scss\";\n@use \"./_variables\" as *;\n@use \"sass:color\";\n\n// Reset inherited properties inside the shadow tree.\n:host {\n  font-size: 16px;\n  line-height: normal;\n  letter-spacing: normal;\n  word-spacing: normal;\n  text-transform: none;\n  font-style: normal;\n  font-variant: normal;\n  text-indent: 0;\n  direction: ltr;\n  white-space: normal;\n}\n\n.screenity-scrollbar::-webkit-scrollbar {\n  background-color: rgba(0, 0, 0, 0);\n  width: 16px;\n  height: 16px;\n  z-index: 999999;\n}\n.screenity-scrollbar::-webkit-scrollbar-track {\n  background-color: rgba(0, 0, 0, 0);\n}\n.screenity-scrollbar::-webkit-scrollbar-thumb {\n  background-color: rgba(0, 0, 0, 0);\n  border-radius: 16px;\n  border: 0px solid #fff;\n}\n.screenity-scrollbar::-webkit-scrollbar-button {\n  display: none;\n}\n.screenity-scrollbar:hover::-webkit-scrollbar-thumb {\n  background-color: #a0a0a5;\n  border: 4px solid #fff;\n}\n::-webkit-scrollbar-thumb:hover {\n  background-color: #a0a0a5;\n  border: 4px solid #f4f4f4;\n}\n\n.ScreenityDropdownMenuContent {\n  min-width: 200px;\n  background-color: white;\n  margin-top: 4px;\n  margin-right: 8px;\n  padding-top: 12px;\n  padding-bottom: 12px;\n  border-radius: 15px;\n  z-index: 99999;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #29292f;\n  box-shadow: 0px 10px 38px -10px rgba(22, 23, 24, 0.35),\n    0px 10px 20px -15px rgba(22, 23, 24, 0.2);\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n.ScreenityDropdownMenuContent[data-side=\"top\"] {\n  animation-name: slideDownAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"right\"] {\n  animation-name: slideLeftAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"bottom\"] {\n  animation-name: slideUpAndFade;\n}\n.ScreenityDropdownMenuContent[data-side=\"left\"] {\n  animation-name: slideRightAndFade;\n}\n.ScreenityItemIndicator {\n  position: absolute;\n  right: 12px;\n  width: 18px;\n  height: 18px;\n  background: #3080f8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n.ScreenityDropdownMenuItem,\n.ScreenityDropdownMenuRadioItem {\n  font-size: 14px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  height: 40px;\n  padding: 0 5px;\n  position: relative;\n  padding-left: 22px;\n  padding-right: 22px;\n  user-select: none;\n  outline: none;\n}\n.ScreenityDropdownMenuItem:hover {\n  background-color: #f6f7fb !important;\n  cursor: pointer;\n}\n.ScreenityDropdownMenuItem[data-disabled] {\n  color: #6e7684 !important;\n  cursor: not-allowed;\n  background-color: #f6f7fb !important;\n}\n\n// Onboarding (driver.js)\n.driver-popover.ScreenityOnboardingPopover,\n.ScreenityOnboardingPopover,\n.onboarding-popover {\n  border-radius: $container-border-radius !important;\n  background: $color-background !important;\n  color: $color-text-primary !important;\n  font-family: $font-regular !important;\n  box-shadow: $container-soft-shadow !important;\n  padding: 20px !important;\n  max-width: 340px !important;\n  font-size: $font-size-normal !important;\n  z-index: $z-index-max !important;\n\n  .driver-popover-title {\n    font-size: 1rem !important;\n    font-family: $font-medium !important;\n    margin-bottom: 12px !important;\n    color: $color-text-primary !important;\n  }\n\n  .driver-popover-description {\n    font-size: $font-size-normal !important;\n    font-family: $font-medium !important;\n    color: $color-text-secondary !important;\n    line-height: 1.5 !important;\n    margin-bottom: 18px !important;\n\n    a {\n      color: #3b82f6 !important;\n      font-family: $font-bold !important;\n      text-decoration: none !important;\n    }\n  }\n\n  .driver-popover-close-btn {\n    top: 14px;\n    right: 12px;\n    color: $color-text-secondary !important;\n    font-size: 1.5rem !important;\n  }\n\n  .driver-popover-arrow {\n    width: 0px;\n    height: 0px;\n    // border: 4px solid #fff;\n    background: none !important;\n    box-shadow: none !important;\n    box-sizing: border-box;\n  }\n\n  .driver-popover-arrow-side-top {\n    border-left: 8px solid transparent !important;\n    border-right: 8px solid transparent !important;\n    border-top: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-bottom {\n    border-left: 8px solid transparent !important;\n    border-right: 8px solid transparent !important;\n    border-bottom: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-left {\n    border-top: 8px solid transparent !important;\n    border-bottom: 8px solid transparent !important;\n    border-left: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-right {\n    border-top: 8px solid transparent !important;\n    border-bottom: 8px solid transparent !important;\n    border-right: 8px solid $color-background !important;\n  }\n  .driver-popover-arrow-align-start {\n    top: 35px !important;\n  }\n  .driver-popover-arrow-align-end {\n    bottom: 35px !important;\n  }\n  .driver-popover-arrow-align-left {\n    left: 35px !important;\n  }\n  .driver-popover-arrow-align-right {\n    right: 35px !important;\n  }\n  .driver-popover-progress-text {\n    font-size: $font-size-detail !important;\n    font-family: $font-medium !important;\n    color: $color-text-secondary !important;\n    opacity: 0.7 !important;\n    margin-top: 2px !important;\n  }\n  .driver-popover-footer {\n    display: flex !important;\n    justify-content: flex-end;\n    gap: 8px;\n    margin-top: 16px;\n\n    .driver-popover-navigation-btns {\n      gap: 6px !important;\n\n      .driver-popover-next-btn,\n      .driver-popover-prev-btn,\n      .driver-popover-close-btn {\n        background-color: $color-primary !important;\n        color: white !important;\n        border: none !important;\n        padding: 10px 14px !important;\n        border-radius: 30px !important;\n        font-family: $font-medium !important;\n        font-weight: 500 !important;\n        cursor: pointer !important;\n        font-size: 0.875rem !important;\n        text-shadow: none !important;\n      }\n      .driver-popover-next-btn {\n        &:hover {\n          background: color.adjust($color-primary, $lightness: -5%) !important;\n        }\n      }\n\n      .driver-popover-prev-btn {\n        background-color: transparent !important;\n        color: $color-text-primary !important;\n        border: 1px solid $color-border !important;\n\n        &:hover {\n          background: $color-light-grey !important;\n        }\n      }\n\n      .driver-popover-close-btn {\n        background-color: transparent !important;\n        color: $color-text-secondary !important;\n      }\n    }\n  }\n}\n\n// Keep driver overlay/stage above extension UI layers during onboarding.\n.driver-overlay {\n  z-index: 2147483645 !important;\n}\n\n.driver-stage {\n  z-index: 2147483646 !important;\n}\n\n.driver-popover,\n.driver-popover.ScreenityOnboardingPopover {\n  z-index: 2147483647 !important;\n}\n"
  },
  {
    "path": "src/pages/Content/styles/dist/app.css",
    "content": "/* Colors */\n/* Font */\n/* Spacing */\n/*\n$spacing-01: 0.125rem;\n$spacing-02: 0.25rem;\n$spacing-03: 0.5rem;\n$spacing-04: 0.75rem;\n$spacing-05: 1rem;\n*/\n/* Container */\n/* Gradients */\n/* Events */\n/* Z-index */\n/* reset */\na,\nbutton {\n  all: unset;\n}\n\niframe {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  overflow: scroll;\n  z-index: -9999;\n  top: 0px;\n  left: 0px;\n  border: 0px;\n  pointer-events: all !important;\n}\n\n.container {\n  pointer-events: none !important;\n}\n\n.ToolbarBounds {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  width: 100%;\n  height: 100%;\n  border: 10px solid #3080F8;\n  pointer-events: none;\n  transform: scale(1.2);\n  opacity: 0;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s ease-in-out;\n}\n\n.ToolbarBounds.ToolbarShake {\n  transform: scale(1);\n  opacity: 0.4;\n}\n\n.react-draggable {\n  pointer-events: all;\n}\n\n.ToolbarShake .react-draggable {\n  width: 100%;\n  height: 100%;\n}\n\n.ToolbarElastic {\n  transition: all 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n\n.ToolbarShake .ToolbarRoot {\n  animation: subtleshake 0.9s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n  animation-iteration-count: infinite !important;\n  background-color: white !important;\n}\n\n.ToolbarDragging .ToolbarRoot {\n  transform: scale(1.02);\n}\n.ToolbarDragging .ToolbarRoot::after {\n  filter: drop-shadow(0px 20px 50px rgba(0, 0, 0, 0.5));\n}\n\n@keyframes shake {\n  0% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  20% {\n    transform: translate(-3px, 0px) rotate(1deg);\n  }\n  30% {\n    transform: translate(3px, 2px) rotate(0deg);\n  }\n  40% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  50% {\n    transform: translate(-1px, 2px) rotate(-1deg);\n  }\n  60% {\n    transform: translate(-3px, 1px) rotate(0deg);\n  }\n  70% {\n    transform: translate(3px, 1px) rotate(-1deg);\n  }\n  80% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  90% {\n    transform: translate(1px, 2px) rotate(0deg);\n  }\n  100% {\n    transform: translate(1px, -2px) rotate(-1deg);\n  }\n}\n@keyframes subtleshake {\n  0% {\n    transform: translate(0px, 0px) rotate(0deg);\n  }\n  10% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n  20% {\n    transform: translate(-1px, -1px) rotate(-1deg);\n  }\n  30% {\n    transform: translate(1px, 0px) rotate(0deg);\n  }\n  40% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  50% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  60% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  70% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  80% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  90% {\n    transform: translate(0px, -1px) rotate(-1deg);\n  }\n  100% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n}\n.ToolbarRoot {\n  display: flex;\n  align-items: center;\n  padding-left: 10px;\n  transition: opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), transform 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  min-width: -moz-max-content;\n  min-width: max-content;\n  background-color: white;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  padding-right: 10px;\n  height: 48px;\n  position: absolute;\n  bottom: 20px;\n  left: 20px;\n  border-radius: 30px;\n}\n.ToolbarRoot::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: #FFF;\n  z-index: -9999999;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  border-radius: 30px;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transition: filter 0.2s ease-in-out;\n}\n\n.ToolbarRecordingControls {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #F6F7FB;\n  border-radius: 30px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  height: calc(100% - 12px);\n  padding-left: 2px;\n  padding-right: 2px;\n}\n\n.ToolbarRecordingTime {\n  margin-right: 4px;\n  width: 42px;\n  color: #29292F;\n  font-size: 13px;\n}\n\n.ToolbarToggleGroup {\n  display: flex;\n  align-items: center;\n}\n\n.ToolbarToggleWrap {\n  flex: 1 1 auto;\n  align-items: center;\n  justify-content: flex-start;\n  position: relative;\n  flex: 0 0 auto;\n  width: 32px;\n  height: 32px;\n  display: inline-flex;\n  line-height: 1;\n  align-items: center;\n  justify-content: center;\n}\n\n.ToolbarToggleItem,\n.ToolbarModeItem,\n.ToolbarModeItemSingle,\n.ToolbarLink,\n.ToolbarButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #000;\n  height: 32px;\n  width: 32px;\n  text-align: center;\n  font-size: 13px;\n  line-height: 1;\n  border-radius: 50%;\n  transition: background-color 0.25s ease-in-out;\n  background-color: rgba(124, 139, 165, 0);\n}\n.ToolbarToggleItem svg,\n.ToolbarModeItem svg,\n.ToolbarModeItemSingle svg,\n.ToolbarLink svg,\n.ToolbarButton svg {\n  color: #9797A4;\n}\n.ToolbarToggleItem:disabled,\n.ToolbarModeItem:disabled,\n.ToolbarModeItemSingle:disabled,\n.ToolbarLink:disabled,\n.ToolbarButton:disabled {\n  opacity: 0.5;\n  cursor: not-allowed !important;\n  background: none !important;\n}\n\n.ToolbarToggleItem:hover,\n.ToolbarModeItem:hover,\n.ToolbarModeItemSingle:hover,\n.ToolbarLink:hover,\n.ToolbarButton:hover {\n  cursor: pointer;\n  background-color: rgba(124, 139, 165, 0.1) !important;\n}\n\n.ToolbarToggleItem:focus-visible,\n.ToolbarModeItemSingle:focus-visible,\n.ToolbarModeItem:focus-visible,\n.ToolbarLink:focus-visible,\n.ToolbarButton:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.ToolbarModeItemSingle {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarModeItemSingle:first-child {\n  margin-left: 0;\n}\n.ToolbarModeItemSingle[data-state=on] {\n  background: rgba(120, 192, 114, 0.1);\n}\n.ToolbarModeItemSingle[data-state=on] svg {\n  color: #78C072;\n}\n.ToolbarModeItemSingle[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n  opacity: 1 !important;\n}\n\n.ToolbarModeItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarModeItem:first-child {\n  margin-left: 0;\n}\n.ToolbarModeItem::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 50%;\n  border-radius: 5rem 5rem 0% 0%;\n  box-sizing: border-box;\n  position: absolute;\n  top: -16px;\n  left: 0;\n  z-index: -999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s ease-in-out;\n  transform: translateY(5px) scale(0) !important;\n  border-right: 3px solid white;\n  border-top: 9px solid white;\n  border-left: 3px solid white;\n  background-color: transparent;\n  opacity: 0;\n}\n.ToolbarModeItem[data-state=on] {\n  background: rgba(56, 126, 247, 0.1);\n}\n.ToolbarModeItem[data-state=on] svg {\n  color: #3080F8;\n}\n.ToolbarModeItem[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n  opacity: 1 !important;\n}\n\n.ToolbarBottom .ToolbarModeItem::before {\n  transform: translateY(-5px) scale(0.5) !important;\n  bottom: -16px;\n  top: unset !important;\n  border-bottom: 9px solid white !important;\n  border-radius: 0% 0% 5rem 5rem !important;\n  border-top: none !important;\n}\n.ToolbarBottom .ToolbarModeItem[data-state=on]::before {\n  transform: translateY(0px) scale(1) !important;\n}\n\n.ToolbarToggleItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n}\n.ToolbarToggleItem:first-child {\n  margin-left: 0;\n}\n.ToolbarToggleItem[data-state=on] {\n  background: radial-gradient(117.41% 117.78% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n  color: #FFF;\n}\n.ToolbarToggleItem[data-state=on] svg {\n  color: #FFF;\n}\n\n.ToolbarSeparator {\n  width: 1px;\n  height: 19px;\n  background-color: #E8E8E8;\n  margin: 0 8px;\n}\n\n.ToolbarLink {\n  background-color: transparent;\n  color: var(--mauve11);\n  display: inline-flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.ToolbarLink:hover {\n  background-color: transparent;\n  cursor: pointer;\n}\n\nbody {\n  background-color: white !important;\n}\n\n.DrawingToolbar.show-toolbar {\n  opacity: 1 !important;\n  pointer-events: all !important;\n  transform: scale(1) translate(calc(-50% + 16px), 0px) !important;\n}\n\n.ToolbarBottom .DrawingToolbar {\n  transform-origin: 0 -100% !important;\n}\n\n.DrawingToolbar {\n  opacity: 0;\n  pointer-events: none;\n  align-items: center;\n  display: flex;\n  min-width: -moz-max-content;\n  min-width: max-content;\n  padding-left: 10px;\n  padding-right: 10px;\n  border-radius: 6px;\n  box-shadow: 0 2px 10px var(--blackA7);\n  position: absolute;\n  height: 44px;\n  left: 0px;\n  transform: translate(calc(-50% + 16px));\n  transform-origin: 0 100%;\n  border-radius: 15px;\n  z-index: 99999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  transform: scale(0.5) translate(calc(-50% + 16px), 10px);\n}\n.DrawingToolbar::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  filter: blur(10px);\n  opacity: 0.15;\n  background-color: #000;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  z-index: -999999999999999 !important;\n}\n.DrawingToolbar::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: rgba(242, 241, 242, 0.85);\n  -webkit-backdrop-filter: blur(5px);\n          backdrop-filter: blur(5px);\n  background-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n  mask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n  background-position: center bottom 50px;\n  border-radius: 15px;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: -2;\n}\n.DrawingToolbar .ToolbarSeparator {\n  background-color: #dddcdc;\n}\n\n.ToolbarTop .DrawingToolbar {\n  bottom: 49px !important;\n}\n\n.ToolbarBottom .DrawingToolbar {\n  top: 48px !important;\n}\n.ToolbarBottom .DrawingToolbar::before {\n  -webkit-mask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px) !important;\n  mask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px);\n  background-position: center bottom 50px !important;\n}\n\n.ColorPicker {\n  width: 14px;\n  height: 14px;\n  background: #ED6C3A;\n  border: 1.5px solid rgba(0, 0, 0, 0.2);\n  border-radius: 50%;\n}\n\n.shapeToolbar {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  box-shadow: 0 2px 10px var(--blackA7);\n  left: 165px;\n  padding: 4px;\n  opacity: 0;\n  bottom: 45px;\n  transform: translateY(calc(-50% + 16px));\n  transform-origin: 0 100%;\n  border-radius: 15px;\n  z-index: 99999999;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  transform: scale(0.5) translatY(calc(-50% + 16px), 10px);\n}\n.shapeToolbar::after {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  background: #FFF;\n  z-index: -9999999;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  border-radius: 15px;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transition: filter 0.2s ease-in-out;\n}\n\n.shapeToolbar.show-toolbar {\n  opacity: 1 !important;\n  pointer-events: all !important;\n  transform: scale(1) translateY(calc(-50% + 16px), 0px) !important;\n}\n\n.TooltipContent {\n  border-radius: 30px;\n  background-color: #29292F;\n  padding: 10px 15px;\n  font-size: 12px;\n  margin-bottom: 10px;\n  bottom: 100px;\n  line-height: 1;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  z-index: 99999999 !important;\n  color: #FFF;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  transition: opacity 0.3 ease-in-out;\n  will-change: transform, opacity;\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.hide-tooltip {\n  display: none !important;\n}\n\n.tooltip-tall {\n  margin-bottom: 20px;\n}\n\n.tooltip-small {\n  margin-bottom: 5px;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=top] {\n  animation-name: slideDownAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=right] {\n  animation-name: slideLeftAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=bottom] {\n  animation-name: slideUpAndFade;\n}\n\n.TooltipContent[data-state=delayed-open][data-side=left] {\n  animation-name: slideRightAndFade;\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n#screenity-ui [data-radix-popper-content-wrapper] {\n  z-index: 99999999999 !important;\n}\n\n.radial-menu {\n  position: absolute;\n  z-index: 9999999999999;\n  width: 100px;\n  height: 100px;\n  top: -66px;\n  left: -49px;\n  pointer-events: none;\n  opacity: 1;\n  transform: scale(1);\n  transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.25s ease-in-out;\n}\n.radial-menu::after {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: -1;\n  border: 28px solid #FFF;\n  box-sizing: border-box;\n  border-radius: 50%;\n  -webkit-backdrop-filter: blur(40px);\n          backdrop-filter: blur(40px);\n  opacity: 0;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n  transform: scale(0);\n  transition: transform 0.25s cubic-bezier(0.18, -0.55, 0.265, 1.45), opacity 0.2s ease-in-out;\n  transition-delay: 0.05s;\n}\n.radial-menu[data-state=open] {\n  transform: scale(1);\n  opacity: 1;\n  pointer-events: all !important;\n}\n.radial-menu[data-state=open]::after {\n  transform: scale(1);\n  border: 28px solid #FFF;\n  opacity: 1;\n}\n\n.color-wheel::after {\n  opacity: 0 !important;\n}\n\n.eyedropper {\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  width: 16px;\n  height: 16px;\n  padding: 8px;\n  z-index: 999999999;\n  background-color: #FFF;\n  border-radius: 50%;\n  opacity: 0;\n  text-align: center;\n  transition: transform 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.3s ease-in-out, background-color 0.25s ease-in-out !important;\n  transform: scale(0);\n  overflow: hidden;\n  transform-style: preserve-3d;\n}\n.eyedropper svg {\n  color: #9797A4;\n}\n.eyedropper:focus-visible {\n  outline: none;\n  background-color: #E6E7EA !important;\n}\n.eyedropper:hover {\n  cursor: pointer;\n  background-color: #E6E7EA;\n}\n\n.eye-active {\n  background-color: #3080F8 !important;\n}\n.eye-active svg {\n  color: #FFF !important;\n  fill: #FFF !important;\n}\n\n.color-wheel .eyedropper {\n  opacity: 0 !important;\n  pointer-events: none !important;\n  transform: scale(0) !important;\n}\n\n.radial-menu[data-state=open] .eyedropper {\n  transform: scale(1) !important;\n  opacity: 1;\n}\n\n.radial-menu-items {\n  transform: rotate(10deg);\n  z-index: 99999999;\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n}\n\n.radial-menu-item {\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  margin: auto;\n  width: 18px;\n  height: 18px;\n  z-index: 999;\n  border-radius: 50%;\n  text-align: center;\n  box-sizing: border-box;\n  line-height: 50px;\n  color: white;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  opacity: 0;\n}\n.radial-menu-item:hover {\n  cursor: pointer;\n}\n\n.color-wheel .radial-menu-item {\n  opacity: 0 !important;\n}\n\n.radial-menu-item-child {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  box-sizing: border-box;\n  border: 0px;\n  box-shadow: none;\n  top: 0px;\n  pointer-events: none;\n  z-index: 9999999;\n  left: 0px;\n  border-radius: 50%;\n  background-size: cover;\n}\n.radial-menu-item-child:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.radial-menu[data-state=open] .radial-menu-item-child {\n  pointer-events: all !important;\n}\n\n.radial-menu-item:nth-child(1), .wheel-trigger {\n  transform: rotate(0deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(1), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0s);\n  transform: rotate(0deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(2), .wheel-trigger {\n  transform: rotate(40deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(2), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.02s);\n  transform: rotate(40deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(3), .wheel-trigger {\n  transform: rotate(80deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(3), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.04s);\n  transform: rotate(80deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(4), .wheel-trigger {\n  transform: rotate(120deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(4), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.06s);\n  transform: rotate(120deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(5), .wheel-trigger {\n  transform: rotate(160deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(5), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.08s);\n  transform: rotate(160deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(6), .wheel-trigger {\n  transform: rotate(200deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(6), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.1s);\n  transform: rotate(200deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(7), .wheel-trigger {\n  transform: rotate(240deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(7), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.12s);\n  transform: rotate(240deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(8), .wheel-trigger {\n  transform: rotate(280deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(8), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.14s);\n  transform: rotate(280deg) translate(36px);\n  opacity: 1;\n}\n\n.radial-menu-item:nth-child(9), .wheel-trigger {\n  transform: rotate(320deg) translate(0px);\n}\n\n.radial-menu[data-state=open] .radial-menu-item:nth-child(9), .radial-menu[data-state=open] .wheel-trigger {\n  transition-delay: calc(.25s - 0.16s);\n  transform: rotate(320deg) translate(36px);\n  opacity: 1;\n}\n\n.color-active {\n  border: 1px solid #FFFFFF;\n  box-shadow: 0px 0px 0px 2px #0D99FF;\n}\n\n.color-wheel .color-active {\n  border: none !important;\n  box-shadow: none !important;\n}\n\n.wheel-trigger {\n  transition: transform 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), width 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  position: absolute;\n  left: 0;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  opacity: 0;\n  margin: auto;\n  width: 18px;\n  box-sizing: border-box;\n  height: 18px;\n  z-index: 9999;\n  box-sizing: border-box;\n  background-blend-mode: screen;\n  border-radius: 50%;\n}\n.wheel-trigger:hover {\n  cursor: pointer;\n}\n\n.wheel-trigger .radial-menu-item-child {\n  transform: rotate(30deg);\n}\n.wheel-trigger .radial-menu-item-child:after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  right: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 9999999;\n  border-radius: 50%;\n  box-sizing: border-box;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n}\n\n.color-wheel .wheel-trigger {\n  width: 100px !important;\n  height: 100px !important;\n  transform: rotate(320deg) translate(0px) !important;\n  z-index: 999999999999 !important;\n  filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n}\n\n.color-wheel-handle {\n  width: 12px !important;\n  height: 12px !important;\n  border-radius: 50%;\n  left: 20px;\n  top: 20px;\n  opacity: 0;\n  background-color: #F17FD7;\n  border: 2px solid white;\n  z-index: 999999999999;\n  display: none;\n  transition: width 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin-left 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin-top 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n}\n.color-wheel-handle:hover {\n  width: 18px !important;\n  height: 18px !important;\n  margin-left: -2px;\n  margin-top: -2px;\n}\n\n.color-wheel .color-wheel-handle {\n  opacity: 0;\n  display: block;\n  animation: fadeIn 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96) forwards;\n  animation-delay: 0.5s;\n}\n\n.w-color-wheel {\n  pointer-events: none;\n  position: absolute !important;\n  width: 100% !important;\n  height: 100% !important;\n  z-index: 99999999 !important;\n}\n.w-color-wheel::after {\n  content: \"\";\n  box-sizing: border-box;\n  display: block;\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  z-index: 9999999;\n  border: 1px solid rgba(0, 0, 0, 0.2);\n  box-sizing: border-box;\n  border-radius: 50%;\n}\n\n.color-wheel .w-color-wheel {\n  pointer-events: all !important;\n}\n\n.w-color-wheel-fill {\n  box-shadow: none !important;\n  border: 2px solid white !important;\n  width: 14px !important;\n  height: 14px !important;\n  transition: width 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), height 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96), margin 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96) !important;\n  box-sizing: border-box !important;\n  margin-left: -2px !important;\n  margin-top: -2px !important;\n  z-index: 9999999999 !important;\n}\n\n.w-color-wheel-pointer {\n  z-index: 99999999999 !important;\n  opacity: 0;\n  animation: none !important;\n}\n\n.color-wheel .w-color-wheel-pointer {\n  animation: fadeInScale 0.25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards !important;\n  animation-delay: 0.28s !important;\n}\n\n.w-color-wheel-fill:hover {\n  width: 18px !important;\n  height: 18px !important;\n  margin-left: -4px !important;\n  margin-top: -4px !important;\n}\n\n/* Fade in keyframes */\n@keyframes fadeInScale {\n  0% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n.color-wheel, .color-wheel .wheel-trigger, .color-wheel .wheel-trigger .radial-menu-item-child, .color-wheel-handle {\n  cursor: pointer !important;\n}\n\n.color-wheel-input {\n  background: #000;\n  border-radius: 30px;\n  height: 29px;\n  color: #FFF;\n  text-align: center;\n  line-height: 29px;\n  padding-left: 8px;\n  padding-right: 8px;\n  position: absolute;\n  margin-top: -35px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  left: 50%;\n  transform: translate(-50%, 0);\n  opacity: 0;\n}\n\n.color-wheel .color-wheel-input {\n  animation: fadeIn 0.3s cubic-bezier(0.61, 0.11, 0.08, 0.96) forwards;\n  animation-delay: 0.2s;\n  pointer-events: all !important;\n}\n\n.color-wheel-input {\n  pointer-events: none;\n}\n\n@keyframes fadeIn {\n  0% {\n    opacity: 0;\n    margin-top: -35px;\n  }\n  100% {\n    opacity: 1;\n    margin-top: -40px;\n  }\n}\n.color-active .color-preview {\n  opacity: 1;\n}\n\n.radial-menu[data-state=closed] .color-preview {\n  opacity: 0 !important;\n}\n\n.color-preview {\n  width: 90%;\n  height: 90%;\n  box-sizing: border-box;\n  border-radius: 50%;\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  z-index: 9999999999;\n  transform: translate(-50%, -50%);\n  opacity: 0;\n  animation: none;\n  pointer-events: none;\n  border: 1px solid #FFF;\n  box-sizing: border-box;\n}\n\n.color-wheel .color-preview {\n  opacity: 0 !important;\n}\n\n.wheel-trigger .color-active {\n  box-shadow: none !important;\n}\n\n.color-active .w-color-wheel {\n  transform: scale(1.15) !important;\n}\n.color-active .w-color-wheel::after {\n  border: none !important;\n}\n\n.color-wheel .w-color-wheel {\n  transform: scale(1) !important;\n}\n.color-wheel .w-color-wheel::after {\n  border: 1px solid rgba(0, 0, 0, 0.2) !important;\n}\n\n.radial-menu[data-state=closed] .w-color-wheel {\n  transform: scale(1) !important;\n}\n\n.stroke-width-item span {\n  width: 18px;\n  height: 18px;\n  display: block;\n}\n.stroke-width-item div[data-state=on] {\n  background: #3080F8 !important;\n}\n.stroke-width-item div[data-state=on] svg {\n  color: #FFF !important;\n  fill: #FFF !important;\n}\n.stroke-width-item div[data-state=off] svg {\n  fill: #201F1D;\n}\n\n.stroke-icon svg {\n  text-align: center;\n  margin: auto;\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n.ToastViewport {\n  --viewport-padding: 25px;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  margin: auto !important;\n  display: flex;\n  flex-direction: column;\n  padding: var(--viewport-padding);\n  gap: 14px;\n  max-width: 100vw;\n  width: -moz-fit-content;\n  width: fit-content;\n  list-style: none;\n  z-index: 2147483647;\n  outline: none;\n  pointer-events: all !important;\n}\n\n.ToastRoot {\n  background-color: #29292F;\n  color: #FFF;\n  border-radius: 30px;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  padding: 10px 14px;\n  display: flex;\n  flex-direction: row;\n  gap: 8px;\n  font-size: 15px;\n  line-height: 1.5;\n  max-width: 100%;\n  overflow: hidden;\n  justify-content: center;\n  align-items: center;\n}\n\n.ToastRoot[data-state=open] {\n  animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.ToastRoot[data-state=closed] {\n  animation: hide 100ms ease-in;\n}\n\n.ToastRoot[data-swipe=move] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n\n.ToastRoot[data-swipe=cancel] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n\n.ToastRoot[data-swipe=end] {\n  animation: swipeOut 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@keyframes slideIn {\n  from {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n@keyframes swipeOut {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n}\n.ToastTitle {\n  color: #FFF;\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.ToastDescription {\n  color: var(--slate-11);\n  font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.ToastAction {\n  color: #FFF;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  text-align: right;\n  background-color: #51515F;\n  padding: 0px 12px !important;\n  height: 24px !important;\n  cursor: pointer;\n}\n\n.toolbar-page {\n  width: 100%;\n  height: 100%;\n  pointer-events: none !important;\n}\n\n.tempimg {\n  height: 100%;\n  opacity: 1;\n  position: fixed;\n  right: 0px;\n  top: 20px;\n}\n\n.container {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  z-index: 999999999;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 14px;\n}\n\n/* Recording popup parent */\n.popup-container {\n  width: 356px;\n  position: fixed;\n  top: 32px;\n  right: 28px;\n  z-index: 99999999999;\n  filter: drop-shadow(0px 4px 100px rgba(0, 0, 0, 0.35));\n  pointer-events: all;\n}\n\n.popup-container::before {\n  content: \"\";\n  display: block;\n  width: 100%;\n  height: 100%;\n  transition: 2s;\n  background: #FFF;\n  background-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  background-position: center bottom 50px;\n  border-radius: 30px;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n}\n\n/*\n.popup-shape {\n\twidth: 100%;\n\theight: 100%;\n\tbackground: $color-background;\n\tbackground-clip: content-box;\n  -webkit-mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  mask-image: radial-gradient(circle at center top, transparent 31px, #000 31px);\n  background-position: center bottom 50px;\n\tborder-radius: $container-border-radius;\n\tposition: relative;\n}\n*/\n.popup-cutout {\n  width: 44px;\n  height: 44px;\n  border-radius: 50%;\n  text-align: center;\n  position: absolute;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  top: -22px;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n}\n\n.popup-cutout img {\n  text-align: center;\n  margin: auto;\n  display: inline-block;\n  width: 100%;\n  border-radius: 50%;\n}\n\n/* Recording nav area */\n.popup-nav {\n  width: 100%;\n  position: relative;\n}\n\n/* Recording content area */\n.popup-content {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  border-radius: 30px;\n  overflow: hidden;\n}\n\n.waveform {\n  width: 100%;\n  margin-top: 12px;\n  margin-bottom: 12px;\n}\n\n.popup-content-divider {\n  width: 100%;\n  height: 1px;\n  background: #E8E8E8;\n  margin-top: 12px;\n  margin-bottom: 12px;\n}\n\n.popup-warning {\n  display: flex;\n  width: calc(100% + 2rem);\n  height: 80px;\n  justify-content: space-between;\n  align-items: center;\n  position: relative;\n  overflow: hidden;\n  background-color: rgba(56, 126, 247, 0.1);\n  margin-left: -1rem;\n  margin-top: -1rem;\n  margin-bottom: 0.5rem;\n}\n.popup-warning .popup-warning-right {\n  color: #3080F8;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  width: 90px;\n}\n\n.popup-warning-left, .popup-warning-right {\n  width: 50px;\n  display: flex;\n  align-items: center;\n  text-align: center;\n  height: 100%;\n  justify-content: center;\n}\n.popup-warning-left svg, .popup-warning-right svg {\n  color: #3080F8;\n}\n\n.popup-warning-right {\n  cursor: pointer;\n}\n\n.popup-warning-middle {\n  flex: 1;\n}\n.popup-warning-middle .popup-warning-title {\n  font-family: \"Satoshi-Bold\", sans-serif;\n  color: #29292F;\n}\n.popup-warning-middle .popup-warning-description {\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #6E7684;\n  margin-top: 4px;\n}\n\n.permission-button {\n  background: rgba(48, 128, 248, 0.1);\n  border-radius: 30px;\n  color: #3080F8;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 44px;\n  gap: 8px;\n  margin-top: 8px;\n  margin-bottom: 8px;\n}\n.permission-button:first-child {\n  margin-top: 4px !important;\n}\n.permission-button:last-child {\n  margin-bottom: 4px !important;\n}\n.permission-button:hover {\n  background: rgba(48, 128, 248, 0.15);\n  cursor: pointer;\n}\n.permission-button svg {\n  color: #3080F8;\n}\n\n.CollapsibleTrigger {\n  margin-top: 12px;\n  font-weight: \"Satoshi-Bold\", sans-serif;\n  padding-top: 4px;\n  padding-bottom: 4px;\n  padding-left: 12px;\n  padding-right: 12px;\n  margin-left: auto;\n  margin-right: auto;\n  text-align: center;\n  display: block;\n  border-radius: 30px;\n}\n\n.CollapsibleTrigger:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.CollapsibleLabel {\n  color: #6E7684;\n  font-weight: \"Satoshi-Bold\", sans-serif;\n  text-align: center;\n  display: inline-block;\n}\n\n.CollapsibleLabel img {\n  margin-left: 4px;\n}\n\n.CollapsibleTrigger:hover {\n  cursor: pointer;\n  background: #FFF;\n}\n\n.CollapsibleRoot[data-state=open] > .CollapsibleTrigger > .CollapsibleLabel img {\n  transform: scaleY(-1);\n  margin-bottom: 2px;\n}\n\n.video-ui {\n  position: relative;\n  /* Blur the background */\n}\n/* .video-ui:before {\n  content: \"\";\n  position: absolute;\n  -webkit-backdrop-filter: blur(5px);\n          backdrop-filter: blur(5px);\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  background: rgba(255, 255, 255, 0.5);\n  z-index: 999;\n} */\n\n.videos-list {\n  padding-bottom: 10px !important;\n  max-height: calc(96vh - 260px);\n  overflow-y: overlay;\n  padding: 16px;\n}\n\n.bottom-section {\n  width: 100%;\n  bottom: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  padding-top: 8px !important;\n  padding-bottom: 12px !important;\n  padding: 16px;\n}\n\n.ModalSoon {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: -moz-fit-content;\n  height: fit-content;\n  width: 70%;\n  padding: 30px 16px;\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: 30px;\n  background: white;\n  box-shadow: 0px 4px 100px 0px rgba(0, 0, 0, 0.15);\n  z-index: 9999999;\n}\n\n.ModalSoonEmoji {\n  font-size: 20px;\n  margin-bottom: 8px;\n}\n\n.ModalSoonTitle {\n  font-weight: 700;\n  color: #29292F;\n  margin-bottom: 8px;\n  text-align: center;\n}\n\n.ModalSoonDescription {\n  text-align: center;\n  color: #6E7684;\n  text-align: center;\n  margin-bottom: 8px;\n}\n\n.ModalSoonButton {\n  margin-top: 8px;\n  color: #FFF;\n  text-align: center;\n  border-radius: 30px;\n  background: radial-gradient(117.41% 117.78% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n  padding: 8px 16px;\n}\n.ModalSoonButton:hover {\n  cursor: pointer;\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SelectTrigger {\n  display: inline-flex;\n  align-items: center;\n  justify-content: space-between;\n  border-radius: 30px;\n  line-height: 1;\n  height: 44px;\n  gap: 5px;\n  background-color: #FFF;\n  color: #29292F;\n  width: 100%;\n  box-sizing: border-box;\n  margin-top: 4px;\n  margin-bottom: 4px;\n}\n\n.SelectTrigger:hover {\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n  cursor: pointer;\n}\n\n.SelectTrigger:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n.SelectTrigger[data-placeholder] {\n  color: var(--violet9);\n}\n\n.SelectTrigger[data-state=open] {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.SelectValue {\n  flex: 1;\n  display: block;\n  width: 100%;\n  height: 100%;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  box-sizing: border-box;\n}\n.SelectValue span {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  height: 100%;\n  line-height: 44px;\n}\n\n.SelectIconDrop, .SelectIconType {\n  text-align: center;\n}\n\n.SelectIconType {\n  padding-left: 0.4rem;\n  padding-right: 0px;\n}\n\n.SelectIconDrop {\n  padding-right: 16px;\n}\n\n.SelectTrigger[data-state=open] .SelectIconDrop img {\n  transform: rotate(180deg);\n}\n\n.SelectContent {\n  overflow: hidden;\n  z-index: 99999999999;\n  width: var(--radix-select-trigger-width);\n  max-height: var(--radix-select-content-available-height);\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: white;\n  border-radius: 15px;\n  margin-top: 4px;\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.SelectItem {\n  font-size: 14px;\n  line-height: 1;\n  color: var(--violet11);\n  display: flex;\n  align-items: center;\n  height: 44px;\n  padding-left: 16px;\n  padding-right: 16px;\n  color: #29292F;\n  position: relative;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.SelectItem[data-disabled] {\n  color: #6E7684;\n  pointer-events: none;\n}\n\n.SelectItem[data-highlighted] {\n  background: #F6F7FB;\n  outline: none !important;\n}\n\n.SelectItem:hover {\n  background: #F6F7FB;\n  cursor: pointer;\n}\n\n.SelectSeparator {\n  height: 1px;\n  background-color: #E8E8E8;\n  width: calc(100% - 24px);\n  margin: auto;\n  border-radius: 30px;\n  margin-top: 4px;\n  margin-bottom: 4px;\n}\n\n.SelectItemIndicator {\n  position: absolute;\n  right: 12px;\n  width: 24px;\n  height: 24px;\n  background: #3080F8;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.SelectScrollButton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 25px;\n  background-color: white;\n  color: var(--violet11);\n  cursor: default;\n}\n\n.SelectOff {\n  background: #FAF0F4;\n  color: #D2234D;\n  padding-left: 12px;\n  padding-right: 12px;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  margin-right: 4px;\n  border-radius: 30px;\n  font-size: 12px;\n  font-weight: 700;\n}\n\n.SelectIconButton {\n  border-radius: 30px;\n  position: relative;\n  padding: 8px;\n}\n.SelectIconButton:hover {\n  background-color: #F6F7FB;\n}\n\n/* Radix tabs navigation */\n/* reset */\nbutton,\nfieldset,\ninput {\n  all: unset;\n}\n\n.TabsRoot {\n  width: 100%;\n  margin: auto;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n}\n\n.TabsList {\n  margin: auto;\n  flex-shrink: 0;\n  display: flex;\n  width: -moz-fit-content;\n  width: fit-content;\n}\n\n.TabsTrigger {\n  padding-left: 12px;\n  padding-right: 12px;\n  color: #6E7684;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n  cursor: pointer;\n}\n\n.TabsTrigger[data-state=active] {\n  color: #29292F;\n}\n\n.TabsTrigger:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n/* Content of the Radix tabs */\n.TabsContent {\n  width: 100%;\n  display: block;\n  height: 100%;\n  box-sizing: border-box;\n  flex: 1 1 auto;\n}\n\n.TabsContent:focus {\n  outline: none;\n}\n\n.TabsContent:focus-visible {\n  box-shadow: inset 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.TabsContent[data-state=inactive] {\n  display: none;\n}\n\n/* Pill animation */\n.pill-anim {\n  position: absolute;\n  height: 32px;\n  top: 0px;\n  bottom: 0px;\n  margin-top: auto;\n  margin-bottom: auto;\n  border-radius: 30px;\n  background: #FFF;\n  box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);\n  transition: all 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n}\n\n.TabsList[data-value=record] .pill-anim {\n  left: 8px;\n  width: 102px;\n}\n\n.TabsList[data-value=dashboard] .pill-anim {\n  left: 109px;\n  width: 132px;\n}\n\n/* Specific to the top level tabs */\n.TabsRoot.tl {\n  height: calc(100% - 40px);\n  margin-top: 40px;\n}\n\n.TabsList.tl {\n  border-radius: 30px;\n  background: #F6F7FB;\n  padding: 6px;\n  font-family: \"Satoshi-Bold\", sans-serif;\n  position: relative;\n}\n\n.TabsTrigger.tl {\n  border-radius: 30px;\n  background: transparent;\n  height: 32px;\n  display: flex;\n  align-items: center;\n  padding-left: 16px;\n  padding-right: 17px;\n  z-index: 2;\n  position: relative;\n}\n\n.TabsTrigger.tl[data-state=inactive]:hover :before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  box-sizing: border-box;\n  height: 100%;\n  width: calc(100% - 10px);\n  margin-left: 5px;\n  background: #EDEEF3;\n  z-index: -2;\n  left: 0px;\n  border-radius: 30px;\n}\n\n.TabsTriggerIcon {\n  width: 20px;\n  height: 20px;\n  text-align: center;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  margin-right: 4px;\n}\n\n/* Specific to recording tab context */\n.recording-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n.recording-ui .TabsRoot {\n  margin-top: 8px;\n}\n.recording-ui .TabsList {\n  width: 100%;\n  border-bottom: 1px solid #E8E8E8;\n  margin: auto;\n  justify-content: center;\n}\n.recording-ui .TabsTrigger {\n  padding-top: 8px;\n  padding-bottom: 12px;\n  box-sizing: border-box;\n  position: relative;\n  display: block;\n  padding-left: 16px;\n  padding-right: 16px;\n}\n.recording-ui .TabsTrigger:hover {\n  background: #F6F7FB;\n  border-top-right-radius: 15px;\n  border-top-left-radius: 15px;\n}\n.recording-ui .TabsTrigger:focus-visible {\n  border-radius: 10px 10px 0px 0px !important;\n}\n.recording-ui .TabsTrigger[data-state=active]::after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  width: 80%;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  height: 2px;\n  border-radius: 30px;\n  background: #3080F8;\n}\n.recording-ui .TabsTrigger[data-state=active] > .TabsTriggerLabel {\n  color: #29292F !important;\n}\n.recording-ui .TabsTriggerLabel {\n  text-align: center;\n}\n.recording-ui .TabsTriggerIcon {\n  width: 20px;\n  height: 20px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  margin: auto;\n  margin-bottom: 8px;\n  border-radius: 30px;\n}\n.recording-ui .TabsContent {\n  background: #F6F7FB;\n  padding: 16px;\n  border-bottom-left-radius: 30px;\n  border-bottom-right-radius: 30px;\n  max-height: calc(95vh - 200px);\n  overflow-y: overlay;\n}\n.recording-ui span {\n  display: block;\n}\n\n.video-ui {\n  width: 100%;\n  flex: 1 1 auto;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n.video-ui .TabsRoot {\n  margin-top: 8px;\n}\n.video-ui .TabsList {\n  width: 100%;\n  border-bottom: 1px solid #E8E8E8;\n  margin: auto;\n  justify-content: space-between;\n  padding-left: 12px;\n  padding-right: 12px;\n  box-sizing: border-box;\n}\n.video-ui .TabsTriggerWrap {\n  display: flex !important;\n  align-items: center;\n  flex-direction: row;\n  justify-content: left;\n  position: relative;\n  display: block;\n  box-sizing: border-box;\n}\n.video-ui .TabsTrigger {\n  padding-top: 8px;\n  padding-bottom: 12px;\n  box-sizing: border-box;\n  position: relative;\n  display: block;\n  padding-left: 20px;\n  padding-right: 20px;\n}\n.video-ui .TabsTrigger:hover {\n  background: #F6F7FB;\n  border-top-right-radius: 15px;\n  border-top-left-radius: 15px;\n}\n.video-ui .TabsTrigger:focus-visible {\n  border-radius: 10px 10px 0px 0px !important;\n}\n.video-ui .TabsTrigger[data-state=active]::after {\n  content: \"\";\n  display: block;\n  position: absolute;\n  width: 80%;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  height: 2px;\n  border-radius: 30px;\n  background: #3080F8;\n}\n.video-ui .TabsTrigger[data-state=active] > .TabsTriggerLabel {\n  color: #29292F !important;\n}\n.video-ui .TabsTriggerLabel {\n  text-align: center;\n}\n.video-ui .TabsContent {\n  background: #F6F7FB;\n  border-bottom-left-radius: 30px;\n  border-bottom-right-radius: 30px;\n}\n.video-ui span {\n  display: block;\n}\n.video-ui .TabsSort {\n  margin-right: 12px;\n  border-radius: 30px;\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  margin-bottom: 5px;\n}\n.video-ui .TabsSort:hover {\n  cursor: pointer;\n  background: #F6F7FB;\n}\n.video-ui .TabsSortLabel {\n  display: flex;\n  flex-direction: row;\n  justify-content: right;\n  align-items: center;\n  color: #6E7684;\n}\n.video-ui .TabsSortLabel img {\n  margin-left: 8px;\n}\n.video-ui .TabsSort:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.SwitchRow {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 40px;\n}\n\n.SwitchRoot {\n  width: 34px;\n  height: 22px;\n  background-color: #E8E8E8;\n  border-radius: 9999px;\n  position: relative;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\n.SwitchRoot:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.SwitchRoot[data-state=checked] {\n  background-color: #3080F8;\n}\n\n.SwitchRoot:hover {\n  cursor: pointer;\n}\n\n.SwitchThumb {\n  display: block;\n  width: 14px;\n  height: 14px;\n  background-color: white;\n  border-radius: 9999px;\n  box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1);\n  transition: transform 100ms;\n  transform: translateX(2px);\n  will-change: transform;\n}\n\n.SwitchThumb[data-state=checked] {\n  transform: translateX(18px);\n}\n\n.Label {\n  color: #6E7684;\n  display: flex;\n}\n\n.ExperimentalLabel {\n  color: #FFF;\n  font-size: 12px;\n  background-color: #3080F8;\n  border-radius: 15px;\n  padding: 2px 8px;\n  display: inline-block;\n  margin-left: 8px;\n}\n\n.video-item-root {\n  width: calc(100% - 2 * 8px);\n  border-radius: 15px;\n  padding: 8px;\n  display: block;\n}\n.video-item-root .video-item {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n}\n.video-item-root .video-item-left {\n  display: flex;\n  flex-grow: 1;\n  flex-direction: row;\n  align-items: center;\n  min-width: 0;\n  justify-content: left;\n}\n.video-item-root .video-item-thumbnail {\n  min-width: 48px;\n  width: 48px;\n  height: 38px;\n  background: grey;\n  border-radius: 5px;\n  margin-right: 12px;\n}\n.video-item-root .video-item-info {\n  display: block;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  padding-right: 4px;\n}\n.video-item-root .video-item-info-title {\n  color: #29292F;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n.video-item-root .video-item-info-date {\n  margin-top: 4px;\n  color: #6E7684;\n  font-size: 12px;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  overflow: hidden;\n}\n\n.video-item-root:hover {\n  cursor: pointer;\n  background: #E9EAEE;\n}\n\n.video-item-root:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n/* Actions */\n.video-item-right {\n  display: flex;\n  align-items: center;\n  justify-content: right;\n  gap: 8px;\n  min-width: -moz-max-content;\n  min-width: max-content;\n  opacity: 0;\n}\n.video-item-right .copy-link {\n  background: #FFF;\n  height: 32px;\n  padding-left: 8px;\n  padding-right: 8px;\n  border-radius: 10px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  align-items: center;\n}\n.video-item-right .copy-link img {\n  margin-right: 4px;\n}\n.video-item-right .copy-link:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n.video-item-right .more-actions {\n  height: 32px;\n  width: 32px;\n  background: #FFF;\n  text-align: center;\n  border-radius: 10px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.video-item-right .more-actions:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n  outline: none !important;\n}\n\n.video-item-root:hover .video-item-right, .video-item-root:focus-visible .video-item-right {\n  opacity: 1;\n}\n\n.video-item-right:focus-within {\n  opacity: 1;\n}\n\n.main-button {\n  width: 100%;\n  height: 45px;\n  border-radius: 30px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  align-items: center;\n  position: relative;\n  box-sizing: border-box;\n}\n.main-button .main-button-label {\n  color: #FFF;\n  text-align: center;\n  vertical-align: middle;\n  align-items: center;\n}\n.main-button .main-button-shortcut {\n  position: absolute;\n  font-size: 12px;\n  right: 16px;\n  color: #FFF;\n  opacity: 0.7;\n}\n.main-button:hover {\n  cursor: pointer;\n}\n.main-button:disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.main-button:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5) !important;\n}\n\n@property --x {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 35.44%;\n}\n@property --y {\n  syntax: \"<percentage>\";\n  inherits: false;\n  initial-value: 0%;\n}\n.recording-button {\n  --x: 35.44%;\n  --y: 0%;\n  margin-top: 8px;\n  filter: drop-shadow(0px 4px 20px rgba(86, 123, 218, 0.5));\n  background: radial-gradient(127.41% 127.78% at var(--x) var(--y), #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n  transition: --x 0.2s ease-in-out, --y 0.2s ease-in-out;\n  animation: 0;\n  animation: gradient-animation 15s linear infinite;\n  animation-play-state: paused;\n  position: relative;\n  z-index: 2;\n}\n\n.recording-button:hover {\n  animation-play-state: running;\n}\n\n.recording-button:before {\n  content: \"\";\n  position: absolute;\n  display: block;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  box-sizing: border-box;\n  border-radius: 30px;\n  transition: all 0.25s ease-in-out;\n}\n\n.recording-button:hover:before {\n  box-shadow: 0px 0px 0px 4px rgba(52, 138, 247, 0.25);\n}\n\n@keyframes pulse-animation {\n  0% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n  25% {\n    box-shadow: 0px 0px 0px 6px rgba(52, 138, 247, 0.25);\n  }\n  50% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n  100% {\n    box-shadow: 0px 0px 0px 2px rgba(52, 138, 247, 0.25);\n  }\n}\n@keyframes gradient-animation {\n  0% {\n    --x: 35.44%;\n    --y: 0%;\n  }\n  25% {\n    --x: 100%;\n    --y: 30%;\n  }\n  50% {\n    --x: 70%;\n    --y: 100%;\n  }\n  75% {\n    --x: 30%;\n    --y: 90%;\n  }\n  100% {\n    --x: 35.44%;\n    --y: 0%;\n  }\n}\n.dashboard-button {\n  background: #29292F;\n  box-shadow: 0px 0px 0px 0px rgba(41, 41, 47, 0.25);\n  transition: all 0.25s ease-in-out;\n}\n\n.dashboard-button:hover {\n  box-shadow: 0px 0px 0px 4px rgba(41, 41, 47, 0.25);\n}\n\n.alarm-time-button {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  border-radius: 15px;\n  padding: 4px 8px;\n  position: absolute;\n  color: #FFF;\n  opacity: 0.7;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-size: 0.75rem;\n  left: 6px;\n}\n.alarm-time-button svg {\n  margin-top: 4px;\n  margin-right: 4px;\n  width: 14px;\n}\n\n.background-effects-toggle-group {\n  display: flex;\n  height: 40px;\n  width: 100%;\n  gap: 8px;\n  margin-bottom: 8px;\n  margin-top: 8px;\n}\n\n.background-effect {\n  display: flex;\n  width: 40px;\n  height: 40px;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  position: relative;\n  color: #FFF;\n}\n.background-effect span {\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 12px;\n  font-weight: 600;\n  z-index: 99999;\n  font-weight: 500;\n}\n.background-effect[data-state=on]::after {\n  content: \"\";\n  border-radius: 50%;\n  display: block;\n  width: 46px;\n  height: 46px;\n  position: absolute;\n  border: 2px solid #3080F8;\n  box-sizing: border-box;\n}\n.background-effect img {\n  width: 100%;\n  height: 100%;\n  border-radius: 50%;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n}\n.background-effect:hover:not([data-state=on]) {\n  cursor: pointer;\n}\n.background-effect:hover:not([data-state=on])::after {\n  content: \"\";\n  border-radius: 50%;\n  display: block;\n  width: 46px;\n  height: 46px;\n  position: absolute;\n  border: 2px solid #3080F8;\n  opacity: 0.5;\n  box-sizing: border-box;\n}\n.background-effect:focus-visible {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.region-dimensions {\n  width: 100%;\n  display: flex;\n  gap: 10px;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n\n.region-input {\n  flex: 1;\n  position: relative;\n}\n.region-input input {\n\tcolor: #29292F !important;\n  border-radius: 30px;\n  height: 40px;\n  box-sizing: border-box;\n  position: relative;\n  width: 100%;\n  padding-left: 18px;\n  padding-right: 40px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: #FFF;\n}\n.region-input input:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n.region-input span {\n  color: #6E7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  position: absolute;\n  right: 18px;\n  bottom: 12px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.time-set-parent {\n  width: 100%;\n  display: flex;\n  gap: 10px;\n  margin-top: 10px;\n  margin-bottom: 10px;\n}\n\n.time-set-input {\n  flex: 1;\n  position: relative;\n}\n.time-set-input input {\n  border-radius: 30px;\n  height: 40px;\n  box-sizing: border-box;\n  position: relative;\n  width: 100%;\n  padding-left: 18px;\n  padding-right: 40px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  background-color: #FFF;\n  -webkit-appearance: textfield;\n  -moz-appearance: textfield;\n  appearance: textfield;\n}\n.time-set-input input:focus-visible {\n  outline: none;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n.time-set-input span {\n  color: #6E7684;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  position: absolute;\n  right: 18px;\n  bottom: 12px;\n  -webkit-user-select: none;\n     -moz-user-select: none;\n          user-select: none;\n}\n\n.CanvasContainer {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  pointer-events: all !important;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999 !important;\n}\n\n.canvas {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999 !important;\n}\n\n.canvas-container {\n  width: 100vw !important;\n  height: 100vh !important;\n  top: 0px !important;\n  left: 0px !important;\n  z-index: 99999999999;\n  position: absolute !important;\n}\n\n.camera-draggable {\n  width: 100%;\n  height: 100%;\n  transform-origin: left top;\n  border-radius: 50%;\n}\n\n.camera-grab {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  border-radius: 50%;\n  z-index: 99999999 !important;\n  cursor: grab;\n}\n\n.camera-flipped {\n  transform: scaleX(-1);\n}\n\n.camera-toolbar {\n  display: flex;\n  align-items: center;\n  padding-left: 4px;\n  padding-right: 4px;\n  transition: opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  min-width: -moz-max-content;\n  min-width: max-content;\n  background-color: rgba(30, 30, 30, 0.8);\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  height: 28px;\n  position: absolute;\n  left: 10px;\n  top: 10px;\n  border-radius: 30px;\n  -webkit-backdrop-filter: blur(10px);\n          backdrop-filter: blur(10px);\n  z-index: 99999999999;\n  opacity: 0;\n  border: 3px solid rgba(255, 255, 255, 0.2);\n}\n\n.camera-draggable:hover .camera-toolbar, .camera-draggable:hover .camera-resize {\n  opacity: 1 !important;\n}\n\n.camera-toolbar:hover, .camera-resize:hover {\n  opacity: 1 !important;\n}\n\n.CameraToolbarSeparator {\n  width: 1px;\n  height: 18px;\n  background-color: rgba(255, 255, 255, 0.3);\n  margin: 0 4px;\n}\n\n.CameraToggleItem, .CameraToolbarButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #000;\n  height: 22px;\n  width: 22px;\n  text-align: center;\n  font-size: 13px;\n  line-height: 1;\n  border-radius: 50%;\n  transition: background-color 0.25s ease-in-out;\n  background-color: rgba(124, 139, 165, 0);\n}\n.CameraToggleItem svg, .CameraToolbarButton svg {\n  color: #9797A4;\n}\n.CameraToggleItem:hover, .CameraToolbarButton:hover {\n  background-color: rgba(124, 139, 165, 0.2) !important;\n  cursor: pointer;\n}\n.CameraToggleItem:disabled, .CameraToolbarButton:disabled {\n  opacity: 0.5;\n  pointer-events: none;\n}\n.CameraToggleItem[data-state=on], .CameraToolbarButton[data-state=on] {\n  color: #FFF;\n}\n.CameraToggleItem[data-state=on] svg, .CameraToolbarButton[data-state=on] svg {\n  color: #FFF;\n}\n\n.CameraToggleItem:hover, .CameraToggleButton:hover {\n  cursor: pointer;\n}\n\n.CameraToggleItem:focus-visible, .CameraToggleButton:focus-visible {\n  position: relative;\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.CameraToggleGroup {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.CameraToggleGroup, .CameraToolbarSeparator {\n  display: none;\n}\n\n.camera-resize {\n  position: absolute;\n  bottom: 20px;\n  right: 20px;\n  z-index: 99999999999;\n  height: 28px;\n  width: 28px;\n  border-radius: 50%;\n  background-color: rgba(30, 30, 30, 0.8);\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  border: 3px solid rgba(255, 255, 255, 0.2);\n  -webkit-backdrop-filter: blur(10px);\n          backdrop-filter: blur(10px);\n  align-items: center;\n  box-sizing: border-box;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0;\n}\n.camera-resize svg {\n  color: #9797A4;\n  text-align: center;\n  margin: auto;\n  display: block;\n}\n\n.countdown {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 99999999999;\n}\n\n.countdown-circle {\n  width: 200px;\n  height: 200px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 999;\n  text-align: center;\n}\n\n.countdown-overlay {\n  width: 100%;\n  height: 100%;\n  background: rgba(0, 0, 0, 0.5);\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 99;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.countdown-number {\n  position: absolute;\n  width: 20px;\n  height: 60px;\n  z-index: 9999999;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  font-weight: 300;\n  font-family: \"Satoshi-Light\", sans-serif;\n  font-size: 3rem;\n  color: #FFF;\n  text-align: center;\n  display: block;\n  transition: all 0.6s ease-in-out;\n}\n\n.background {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  filter: url(\"#goo\");\n  transform: rotate(0deg);\n  transition: all 3s ease-in-out;\n}\n\n.circle {\n  z-index: 9;\n  position: absolute;\n  transform: scale(0.8);\n  top: 0px;\n  left: 0px;\n  right: 0px;\n  bottom: 0px;\n  margin: auto;\n  border-radius: 50%;\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n  width: 200px;\n  height: 200px;\n  transition: all 1.5s ease-in-out;\n}\n\n.c {\n  width: 50px;\n  height: 50px;\n  z-index: 999;\n  border-radius: 50%;\n  position: absolute;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  opacity: 1;\n}\n\n.c2 {\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #2B96F8 23.13%, #356BF6 64.58%);\n  transform: translate(20px, 20px);\n}\n\n.c3 {\n  background: radial-gradient(118.3% 119.01% at 35.44% 0%, #4884CA 15.3%, #2B89F8 78.83%);\n  transform: translate(-30px, -40px);\n}\n\n.c3:after {\n  content: \"\";\n  position: absolute;\n  width: 150px;\n  height: 150px;\n  filter: blur(50px);\n  border-radius: 50%;\n  top: 0px;\n  right: 0px;\n  left: 0px;\n  bottom: 0px;\n  margin: auto;\n  transition: cubic-bezier(0.82, 0.1, 0.24, 0.99) 1.5s;\n  background: #CBE8F7;\n  z-index: -1;\n}\n\n.recording-countdown .c2 {\n  transform: translate(-15px, 15px);\n}\n\n.recording-countdown .c3 {\n  transform: translate(-10px, -5px);\n}\n\n.countdown-info {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  bottom: 20px;\n  border-radius: 30px;\n  border: 2px solid rgba(255, 255, 255, 0.3);\n  text-align: center;\n  display: block;\n  padding: 10px 20px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  color: #FFF;\n  z-index: 99999999999;\n  width: -moz-fit-content;\n  width: fit-content;\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.AlertDialogOverlay {\n  background-color: rgba(0, 0, 0, 0.5);\n  position: fixed;\n  inset: 0;\n  animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent {\n  background-color: white;\n  border-radius: 30px;\n  box-shadow: hsla(206, 22%, 7%, 0.35) 0px 10px 38px -10px, hsla(206, 22%, 7%, 0.2) 0px 10px 20px -15px;\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 90vw;\n  max-width: 500px;\n  max-height: 85vh;\n  padding: 35px 25px;\n  animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent:focus {\n  outline: none;\n}\n\n.AlertDialogTitle {\n  margin: 0;\n  color: #29292F;\n  font-size: 14px;\n  font-family: \"Satoshi-Medium\", sans-serif;\n  font-weight: 700;\n}\n\n.AlertDialogDescription {\n  margin-bottom: 20px;\n  color: #6E7684;\n  font-size: 14px;\n  line-height: 1.5;\n}\n\n.Button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 15px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n}\n\n.Button.red {\n  background-color: rgba(247, 56, 90, 0.1);\n  color: rgb(247, 56, 90);\n}\n\n.Button.red:hover {\n  background-color: rgba(247, 56, 90, 0.15);\n  cursor: pointer;\n}\n\n.Button.red:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n.Button.grey {\n  background: rgba(110, 118, 132, 0.1);\n  color: #6E7684;\n}\n\n.Button.grey:hover {\n  background: rgba(110, 118, 132, 0.15);\n  cursor: pointer;\n}\n\n.Button.grey:focus {\n  box-shadow: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n}\n\n@keyframes overlayShow {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes contentShow {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -48%) scale(0.96);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%) scale(1);\n  }\n}\n.box-hole {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 9999999999;\n}\n\n.resize-handle {\n  position: absolute;\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background-color: white;\n  border: 2px solid rgba(0, 0, 0, 0.5);\n  box-sizing: border-box;\n}\n\n.resize-handle.top-left {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nwse-resize;\n}\n\n.resize-handle.top {\n  top: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n\n.resize-handle.top-right {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nesw-resize;\n}\n\n.resize-handle.right {\n  top: 50%;\n  right: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n\n.resize-handle.bottom-right {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nwse-resize;\n}\n\n.resize-handle.bottom {\n  bottom: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  cursor: ns-resize;\n}\n\n.resize-handle.bottom-left {\n  bottom: 0;\n  left: 0;\n  top: 0;\n  right: 0;\n  margin: auto;\n  cursor: nesw-resize;\n}\n\n.resize-handle.left {\n  top: 50%;\n  left: 0;\n  transform: translateY(-50%);\n  cursor: ew-resize;\n}\n\n.region-recording * {\n  pointer-events: none !important;\n}\n\nhtml {\n  font-size: 16px;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/Toolbar.jsx",
    "content": "import React from \"react\";\n\nimport ToolbarWrap from \"./layout/ToolbarWrap\";\n\nconst Toolbar = () => {\n  return (\n    <div className=\"toolbar-page\">\n      <ToolbarWrap />\n    </div>\n  );\n};\n\nexport default Toolbar;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/ColorWheel.jsx",
    "content": "import React, { useRef, useState, useContext } from \"react\";\n\nimport Wheel from \"@uiw/react-color-wheel\";\nimport { hsvaToHex } from \"@uiw/color-convert\";\n\n// Components\nimport TooltipWrap from \"./TooltipWrap\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst ColorWheel = (props) => {\n  const [hsva, setHsva] = React.useState({ h: 200, s: 50, v: 100, a: 1 });\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const wheelRef = useRef(null);\n  const stateRef = useRef();\n\n  stateRef.current = props.fullwheel;\n\n  const handleClick = (e) => {\n    if (!props.fullwheel) {\n      props.setFullWheel(true);\n    }\n  };\n\n  return (\n    <TooltipWrap\n      content={chrome.i18n.getMessage(\"moreColorsTooltip\")}\n      name=\"wheel-trigger\"\n      override=\"tooltip-small\"\n      hide={props.fullwheel ? \"hide-tooltip\" : \"\"}\n    >\n      <div\n        className={\n          contentState.swatch === 5\n            ? \"radial-menu-item-child color-active\"\n            : \"radial-menu-item-child\"\n        }\n        onClick={handleClick}\n        style={\n          contentState.swatch === 5\n            ? { backgroundColor: contentState.color }\n            : {}\n        }\n        ref={wheelRef}\n        tabIndex={props.open ? \"0\" : \"-1\"}\n      >\n        <div className=\"color-wheel-input\">\n          {contentState.color.toUpperCase()}\n        </div>\n        <div\n          className=\"color-preview\"\n          style={{ backgroundColor: contentState.color }}\n        ></div>\n        <Wheel\n          color={hsva}\n          width={100}\n          height={100}\n          onChange={(color) => {\n            setHsva({ ...hsva, ...color.hsva });\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              color: hsvaToHex({ h: hsva.h, s: hsva.s, v: hsva.v, a: hsva.a }),\n              swatch: 5,\n            }));\n            chrome.storage.local.set({\n              color: hsvaToHex({ h: hsva.h, s: hsva.s, v: hsva.v, a: hsva.a }),\n              swatch: 5,\n            });\n          }}\n        />\n      </div>\n    </TooltipWrap>\n  );\n};\n\nexport default ColorWheel;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/MicToggle.jsx",
    "content": "import React, { useContext } from \"react\";\nimport * as Toggle from \"@radix-ui/react-toggle\";\n\n// Components\nimport TooltipWrap from \"./TooltipWrap\";\n\nimport { MicIcon } from \"./SVG\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst MicToggle = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <TooltipWrap\n      content={\n        contentState.microphonePermission && contentState.micActive\n          ? chrome.i18n.getMessage(\"disableMicrophoneTooltip\")\n          : contentState.microphonePermission && !contentState.micactive\n          ? chrome.i18n.getMessage(\"enableMicrophoneTooltip\")\n          : chrome.i18n.getMessage(\"noMicrophonePermissionsTooltip\")\n      }\n    >\n      <div className=\"ToolbarToggleWrap\">\n        <Toggle.Root\n          className=\"ToolbarModeItemSingle\"\n          aria-label=\"Toggle microphone\"\n          pressed={contentState.micActive}\n          disabled={\n            !contentState.microphonePermission ||\n            contentState.defaultAudioInput === \"none\"\n          }\n          onPressedChange={(pressed) => {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              micActive: pressed,\n            }));\n\n            chrome.storage.local.set({\n              micActive: pressed,\n            });\n\n            chrome.runtime.sendMessage({\n              type: \"set-mic-active-tab\",\n              active: pressed,\n              defaultAudioInput: contentState.defaultAudioInput,\n            });\n\n            // Show toast\n            contentState.openToast(\n              pressed\n                ? chrome.i18n.getMessage(\"micOnToast\")\n                : chrome.i18n.getMessage(\"micOffToast\"),\n              () => {}\n            );\n          }}\n        >\n          <MicIcon />\n        </Toggle.Root>\n      </div>\n    </TooltipWrap>\n  );\n};\n\nexport default MicToggle;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/RadialMenu.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\nimport * as Popover from \"@radix-ui/react-popover\";\n\n// Icons\nimport { EyeDropperIcon } from \"./SVG\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nimport * as ToggleGroup from \"@radix-ui/react-toggle-group\";\n\n// Components\nimport TooltipWrap from \"./TooltipWrap\";\nimport ColorWheel from \"./ColorWheel\";\nimport StrokeWeight from \"./StrokeWeight\";\n\nconst RadialMenu = (props) => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n  const ref = React.useRef(null);\n  const buttonRef = React.useRef(null);\n  const radialMenuRef = React.useRef(null);\n  const [fullwheel, setFullWheel] = useState(false);\n  const [open, setOpen] = useState(false);\n  const [eyeDropperActive, setEyeDropperActive] = useState(false);\n  const [renderColorWheel, setRenderColorWheel] = useState(false);\n\n  // Colors in menu\n  const [colors, setColors] = useState([\n    { color: \"#FED252\", label: \"Yellow\" },\n    { color: \"#4597F7\", label: \"Blue\" },\n    { color: \"#F24822\", label: \"Red\" },\n    { color: \"#FFFFFF\", label: \"White\" },\n    { color: \"#201F1D\", label: \"Black\" },\n  ]);\n\n  useEffect(() => {\n    if (!open) {\n      setFullWheel(false);\n      setRenderColorWheel(false);\n    } else {\n      const timeout = setTimeout(() => {\n        setRenderColorWheel(true);\n      }, 50);\n      return () => clearTimeout(timeout);\n    }\n  }, [open]);\n\n  const selectColor = () => {\n    const eyeDropper = new window.EyeDropper();\n    setEyeDropperActive(true);\n\n    eyeDropper\n      .open()\n      .then((color) => {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          color: color.sRGBHex,\n          swatch: 5,\n        }));\n        setEyeDropperActive(false);\n      })\n      .catch((err) => {\n        setEyeDropperActive(false);\n      });\n  };\n\n  useEffect(() => {\n    if (!buttonRef.current) return;\n    if (!radialMenuRef.current) return;\n\n    const left =\n      buttonRef.current.getBoundingClientRect().left +\n      buttonRef.current.getBoundingClientRect().width / 2;\n    const top =\n      buttonRef.current.getBoundingClientRect().top +\n      buttonRef.current.getBoundingClientRect().height / 2;\n\n    radialMenuRef.current.style.left = `${left}px`;\n    radialMenuRef.current.style.top = `${top}px`;\n  }, [buttonRef, radialMenuRef]);\n\n  return (\n    <Popover.Root open={open} onOpenChange={() => setOpen(!open)}>\n      <TooltipWrap content=\"Color and stroke\" shortcut={props.shortcut}>\n        <Popover.Trigger as=\"div\" ref={ref} data-color-trigger=\"true\">\n          <div className=\"ToolbarButton\" component=\"div\" ref={buttonRef}>\n            <div\n              className=\"ColorPicker\"\n              style={{ backgroundColor: contentState.color }}\n            ></div>\n          </div>\n        </Popover.Trigger>\n      </TooltipWrap>\n      <Popover.Portal forceMount container={ref.current}>\n        <Popover.Content avoidCollisions={false} asChild onOpenAutoFocus>\n          <div\n            className={fullwheel ? \"radial-menu color-wheel\" : \"radial-menu\"}\n            ref={radialMenuRef}\n            style={{\n              position: \"fixed\",\n            }}\n          >\n            <div className=\"eyedropper\">\n              <TooltipWrap content=\"Eyedropper\">\n                <div\n                  tabIndex={open ? \"0\" : \"-1\"}\n                  className={\n                    eyeDropperActive ? \"eyedropper eye-active\" : \"eyedropper\"\n                  }\n                  onClick={() => {\n                    selectColor();\n                  }}\n                >\n                  <EyeDropperIcon />\n                </div>\n              </TooltipWrap>\n            </div>\n            <div className=\"radial-menu-items\">\n              <ToggleGroup.Root\n                className=\"stroke-weight\"\n                type=\"single\"\n                value={contentState.strokeWidth}\n                onValueChange={(value) => {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    strokeWidth: value,\n                  }));\n                }}\n              >\n                <StrokeWeight open={open} />\n                {colors.map((color, index) => {\n                  return (\n                    <TooltipWrap\n                      content={color.label}\n                      style={{ backgroundColor: color.color }}\n                      name=\"radial-menu-item\"\n                      override=\"tooltip-small\"\n                      key={index}\n                    >\n                      <div\n                        tabIndex={open ? \"0\" : \"-1\"}\n                        onClick={() => {\n                          setContentState((prevContentState) => ({\n                            ...prevContentState,\n                            color: color.color,\n                            swatch: index,\n                          }));\n                          chrome.storage.local.set({\n                            color: color.color,\n                            swatch: index,\n                          });\n                        }}\n                        className={\n                          contentState.swatch === index\n                            ? \"radial-menu-item-child color-active\"\n                            : \"radial-menu-item-child\"\n                        }\n                      >\n                        {index}\n                      </div>\n                    </TooltipWrap>\n                  );\n                })}\n                {renderColorWheel && (\n                  <ColorWheel\n                    fullwheel={fullwheel}\n                    open={open}\n                    setFullWheel={setFullWheel}\n                  />\n                )}\n              </ToggleGroup.Root>\n            </div>\n          </div>\n        </Popover.Content>\n      </Popover.Portal>\n    </Popover.Root>\n  );\n};\n\nexport default RadialMenu;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/SVG.jsx",
    "content": "import React from \"react\";\nimport { ReactSVG } from \"react-svg\";\n\nconst URL =\n  \"chrome-extension://\" + chrome.i18n.getMessage(\"@@extension_id\") + \"/assets/\";\n\nconst GrabIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/grab-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n/*\n\n*/\n\n// Convert all to ReactSVG\n\nconst StopIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/stop-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst DrawIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/draw-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst PauseIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/pause-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst ResumeIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/resume-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CursorIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/cursor-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CommentIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/comment-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst MicIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/mic-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst MoreIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/more-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst RestartIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/restart-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst DiscardIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/discard-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst EyeDropperIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/eyedropper-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst Stroke1Icon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/stroke-1-icon.svg\"}\n      width={props.width}\n      height={props.height}\n      className={props.className}\n      style={{\n        textAlign: \"center\",\n        margin: \"auto\",\n        display: \"block\",\n        width: \"100%\",\n        height: \"100%\",\n      }}\n    />\n  );\n};\n\nconst Stroke2Icon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/stroke-2-icon.svg\"}\n      width={props.width}\n      height={props.height}\n      className={props.className}\n      style={{\n        textAlign: \"center\",\n        margin: \"auto\",\n        display: \"block\",\n        width: \"100%\",\n        height: \"100%\",\n      }}\n    />\n  );\n};\n\nconst Stroke3Icon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/stroke-3-icon.svg\"}\n      width={props.width}\n      height={props.height}\n      className={props.className}\n      style={{\n        textAlign: \"center\",\n        margin: \"auto\",\n        display: \"block\",\n        width: \"100%\",\n        height: \"100%\",\n      }}\n    />\n  );\n};\n\nconst TargetCursorIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/target-cursor-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst HighlightCursorIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/highlight-cursor-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst HideCursorIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/hide-cursor-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TextIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/text-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst ArrowIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/arrow-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst EraserIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/eraser-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst PenIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/pen-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst ShapeIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/shape-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst SelectIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/select-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst UndoIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/undo-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst RedoIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/redo-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst ImageIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/image-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TransformIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/transform-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst HighlighterIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/highlighter-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst RectangleIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/rectangle-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CircleIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/circle-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TriangleIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/triangle-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst RectangleFilledIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/rectangle-filled-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CircleFilledIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/circle-filled-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TriangleFilledIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/triangle-filled-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TrashIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/trash-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst VideoOffIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"camera-icons/video-off.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CameraCloseIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"camera-icons/close.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CameraMoreIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"camera-icons/more.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CameraResizeIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"camera-icons/camera-resize.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CameraIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/camera-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst BlurIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/blur-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst AlertIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/alert-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst TimeIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/time-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\nconst SpotlightCursorIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"tool-icons/spotlight-cursor-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst Pip = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"camera-icons/pip.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CloseIconPopup = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"close-icon-popup.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst GrabIconPopup = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"grab-icon-popup.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst MoreIconPopup = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"more-icon-popup.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst OnboardingArrow = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/helper/onboarding-arrow.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst NoInternet = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/editor/icons/no-internet.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst CloseButtonToolbar = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/tool-icons/close-button.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst HelpIconPopup = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/tool-icons/help-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst AudioIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/tool-icons/audio-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nconst NotSupportedIcon = (props) => {\n  return (\n    <ReactSVG\n      src={URL + \"/tool-icons/not-supported-icon.svg\"}\n      width={props.width}\n      height={props.height}\n    />\n  );\n};\n\nexport {\n  GrabIcon,\n  StopIcon,\n  DrawIcon,\n  PauseIcon,\n  ResumeIcon,\n  CursorIcon,\n  CommentIcon,\n  MicIcon,\n  MoreIcon,\n  RestartIcon,\n  DiscardIcon,\n  EyeDropperIcon,\n  Stroke1Icon,\n  Stroke2Icon,\n  Stroke3Icon,\n  TargetCursorIcon,\n  HighlightCursorIcon,\n  HideCursorIcon,\n  TextIcon,\n  ArrowIcon,\n  EraserIcon,\n  PenIcon,\n  ShapeIcon,\n  SelectIcon,\n  UndoIcon,\n  RedoIcon,\n  ImageIcon,\n  TransformIcon,\n  HighlighterIcon,\n  RectangleIcon,\n  CircleIcon,\n  TriangleIcon,\n  RectangleFilledIcon,\n  CircleFilledIcon,\n  TriangleFilledIcon,\n  TrashIcon,\n  VideoOffIcon,\n  CameraCloseIcon,\n  CameraMoreIcon,\n  CameraResizeIcon,\n  CameraIcon,\n  BlurIcon,\n  AlertIcon,\n  TimeIcon,\n  SpotlightCursorIcon,\n  Pip,\n  CloseIconPopup,\n  GrabIconPopup,\n  OnboardingArrow,\n  NoInternet,\n  CloseButtonToolbar,\n  HelpIconPopup,\n  MoreIconPopup,\n  AudioIcon,\n  NotSupportedIcon,\n};\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/StrokeWeight.jsx",
    "content": "import React from \"react\";\n\n// Icons\nimport { Stroke1Icon, Stroke2Icon, Stroke3Icon } from \"./SVG\";\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\n// Components\nimport TooltipWrap from \"./TooltipWrap\";\n\nimport * as ToggleGroup from \"@radix-ui/react-toggle-group\";\n\nconst StrokeWeight = (props) => {\n  const [contentState, setContentState] = React.useContext(contentStateContext);\n\n  return (\n    <React.Fragment>\n      <TooltipWrap\n        name=\"radial-menu-item stroke-width-item\"\n        override=\"tooltip-small\"\n        content={props.open ? chrome.i18n.getMessage(\"thickStrokeTooltip\") : \"\"}\n      >\n        <span>\n          <ToggleGroup.Item value={3} asChild>\n            <div\n              tabIndex={props.open ? \"0\" : \"-1\"}\n              className=\"radial-menu-item-child\"\n            >\n              <Stroke3Icon className=\"stroke-icon\" />\n            </div>\n          </ToggleGroup.Item>\n        </span>\n      </TooltipWrap>\n      <TooltipWrap\n        name=\"radial-menu-item stroke-width-item\"\n        override=\"tooltip-small\"\n        content={\n          props.open ? chrome.i18n.getMessage(\"mediumStrokeTooltip\") : \"\"\n        }\n      >\n        <span>\n          <ToggleGroup.Item value={2} asChild>\n            <div\n              tabIndex={props.open ? \"0\" : \"-1\"}\n              className=\"radial-menu-item-child\"\n            >\n              <Stroke2Icon className=\"stroke-icon\" />\n            </div>\n          </ToggleGroup.Item>\n        </span>\n      </TooltipWrap>\n      <TooltipWrap\n        name=\"radial-menu-item stroke-width-item\"\n        override=\"tooltip-small\"\n        content={props.open ? chrome.i18n.getMessage(\"thinStrokeTooltip\") : \"\"}\n      >\n        <span>\n          <ToggleGroup.Item value={1} asChild>\n            <div\n              tabIndex={props.open ? \"0\" : \"-1\"}\n              className=\"radial-menu-item-child\"\n            >\n              <Stroke1Icon className=\"stroke-icon\" />\n            </div>\n          </ToggleGroup.Item>\n        </span>\n      </TooltipWrap>\n    </React.Fragment>\n  );\n};\n\nexport default StrokeWeight;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/Toast.jsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  useContext,\n  useCallback,\n  useRef,\n} from \"react\";\n\nimport * as ToastEl from \"@radix-ui/react-toast\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst Toast = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [open, setOpen] = useState(false);\n  const [title, setTitle] = useState(\"\");\n  const [trigger, setTrigger] = useState(() => {});\n  const triggerRef = useRef(trigger);\n  const openRef = useRef(open);\n  const contentStateRef = useRef(contentState);\n  const [toastDuration, setToastDuration] = useState(2000);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  const openToast = useCallback((title, action, durationMs = 2000) => {\n    if (contentStateRef.current.hideUI) return;\n    setTitle(title);\n    setOpen(true);\n    setTrigger(() => action);\n    setToastDuration(durationMs);\n  });\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      openToast: openToast,\n    }));\n\n    return () => {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        openToast: null,\n      }));\n    };\n  }, []);\n\n  useEffect(() => {\n    openRef.current = open;\n  }, [open]);\n\n  useEffect(() => {\n    triggerRef.current = trigger;\n\n    return () => {\n      triggerRef.current = () => {};\n    };\n  }, [trigger]);\n\n  return (\n    <ToastEl.Provider swipeDirection=\"down\" duration={toastDuration}>\n      <ToastEl.Root\n        className=\"ToastRoot\"\n        open={open}\n        onOpenChange={setOpen}\n        onEscapeKeyDown={(e) => {\n          e.stopPropagation();\n          e.preventDefault();\n          triggerRef.current();\n          setOpen(false);\n        }}\n      >\n        <ToastEl.Title className=\"ToastTitle\">{title}</ToastEl.Title>\n        <ToastEl.Action\n          className=\"ToastAction\"\n          asChild\n          altText=\"Escape\"\n          onClick={() => {\n            trigger();\n          }}\n        >\n          <button\n            className=\"Button\"\n            onClick={(e) => {\n              e.stopPropagation();\n              trigger();\n            }}\n          >\n            Esc\n          </button>\n        </ToastEl.Action>\n      </ToastEl.Root>\n      <ToastEl.Viewport className=\"ToastViewport\" />\n    </ToastEl.Provider>\n  );\n};\n\nexport default Toast;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/ToolTrigger.jsx",
    "content": "import React from \"react\";\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\n// Components\nimport TooltipWrap from \"./TooltipWrap\";\n\nconst ToolTrigger = (props) => {\n  const grab = props.grab ? \" grab\" : \"\";\n  const resume = props.resume ? \" resume\" : \"\";\n\n  return (\n    <TooltipWrap content={props.content} shortcut={props.shortcut}>\n      {props.type === \"button\" ? (\n        <Toolbar.Button\n          className={\"ToolbarButton\" + grab + resume}\n          onClick={props.onClick}\n          disabled={props.disabled}\n        >\n          {props.children}\n        </Toolbar.Button>\n      ) : props.type === \"mode\" ? (\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem\n            className=\"ToolbarModeItem\"\n            value={props.value}\n            disabled={props.disabled}\n          >\n            {props.children}\n          </Toolbar.ToggleItem>\n        </div>\n      ) : (\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem\n            className=\"ToolbarToggleItem\"\n            value={props.value}\n            disabled={props.disabled}\n          >\n            {props.children}\n          </Toolbar.ToggleItem>\n        </div>\n      )}\n    </TooltipWrap>\n  );\n};\n\nexport default ToolTrigger;\n"
  },
  {
    "path": "src/pages/Content/toolbar/components/TooltipWrap.jsx",
    "content": "import React, { useEffect, useContext, useState } from \"react\";\n\nimport * as Tooltip from \"@radix-ui/react-tooltip\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst TooltipWrap = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const classname = props.name ? props.name : \"\";\n  const [override, setOverride] = useState(\"\");\n  const content =\n    props.shortcut && props.content\n      ? `${props.content} (${props.shortcut})`\n      : props.content;\n\n  useEffect(() => {\n    // Check if hideUI is set\n    if (contentState.hideUI) {\n      setOverride(\"override\");\n    } else {\n      setOverride(\"\");\n    }\n  }, [contentState.hideUI]);\n\n  return (\n    <div className={classname} style={props.style}>\n      {props.content == \"\" ? (\n        <div>{props.children}</div>\n      ) : (\n        <Tooltip.Provider>\n          <Tooltip.Root delayDuration={700} defaultOpen={false}>\n            <Tooltip.Trigger asChild>{props.children}</Tooltip.Trigger>\n            <Tooltip.Portal\n              container={\n                document.getElementsByClassName(\"screenity-shadow-dom\")[0]\n              }\n            >\n              <Tooltip.Content\n                className={\n                  \"TooltipContent\" +\n                  \" \" +\n                  props.override +\n                  \" \" +\n                  props.hide +\n                  \" \" +\n                  override\n                }\n                style={{\n                  display: override === \"override\" ? \"none\" : \"block\",\n                }}\n              >\n                {content}\n              </Tooltip.Content>\n            </Tooltip.Portal>\n          </Tooltip.Root>\n        </Tooltip.Provider>\n      )}\n    </div>\n  );\n};\n\nexport default TooltipWrap;\n"
  },
  {
    "path": "src/pages/Content/toolbar/layout/BlurToolbar.jsx",
    "content": "import React from \"react\";\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\n// Components\nimport ToolTrigger from \"../components/ToolTrigger\";\n\n// Icons\nimport { TransformIcon, TrashIcon } from \"../components/SVG\";\n\nconst BlurToolbar = (props) => {\n  return (\n    <Toolbar.Root\n      className={\"DrawingToolbar\" + \" \" + props.visible}\n      aria-label=\"Cursor options\"\n      tabIndex=\"0\"\n    >\n      <Toolbar.ToggleGroup\n        type=\"single\"\n        className=\"ToolbarToggleGroup\"\n        value=\"target\"\n      >\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem className=\"ToolbarToggleItem\" value=\"target\">\n            <TransformIcon />\n          </Toolbar.ToggleItem>\n        </div>\n        <Toolbar.Separator className=\"ToolbarSeparator\" />\n        <ToolTrigger\n          type=\"button\"\n          content={chrome.i18n.getMessage(\"clearBlurredElementsTooltip\")}\n          shortcut=\"0\"\n          onClick={() => {\n            // Remove class screenity-blur from all elements\n            const blurredElements =\n              document.querySelectorAll(\".screenity-blur\");\n            blurredElements.forEach((element) => {\n              element.classList.remove(\"screenity-blur\");\n            });\n          }}\n        >\n          <TrashIcon />\n        </ToolTrigger>\n      </Toolbar.ToggleGroup>\n    </Toolbar.Root>\n  );\n};\n\nexport default BlurToolbar;\n"
  },
  {
    "path": "src/pages/Content/toolbar/layout/CursorToolbar.jsx",
    "content": "import React, { useState, useEffect, useContext, useRef } from \"react\";\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\nimport TooltipWrap from \"../components/TooltipWrap\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\n// Icons\nimport {\n  CursorIcon,\n  TargetCursorIcon,\n  HighlightCursorIcon,\n  SpotlightCursorIcon,\n  HideCursorIcon,\n} from \"../components/SVG\";\n\nconst CursorToolbar = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const lastClickedEffectRef = useRef(contentState.cursorMode || \"none\");\n\n  useEffect(() => {\n    if (contentState.cursorMode) {\n      lastClickedEffectRef.current = contentState.cursorMode;\n    }\n  }, [contentState.cursorMode]);\n\n  const deriveCursorMode = (effects, fallback) => {\n    if (effects.length === 0) return \"none\";\n    if (effects.length === 1) return effects[0];\n    if (fallback && effects.includes(fallback)) return fallback;\n    return effects[0] || \"none\";\n  };\n\n  const applyCursorSelection = (effect, shiftKey) => {\n    if (effect === \"none\") {\n      lastClickedEffectRef.current = \"none\";\n      setContentState((prev) => ({\n        ...prev,\n        cursorEffects: [],\n        cursorMode: \"none\",\n      }));\n      if (!shiftKey) {\n        props.setMode(false);\n      }\n      chrome.storage.local.set({\n        cursorEffects: [],\n        cursorMode: \"none\",\n      });\n      return;\n    }\n\n    const currentEffects = Array.isArray(contentState.cursorEffects)\n      ? contentState.cursorEffects\n      : [];\n\n    let nextEffects = [];\n    if (shiftKey) {\n      if (currentEffects.includes(effect)) {\n        nextEffects = currentEffects.filter((item) => item !== effect);\n      } else {\n        nextEffects = [...currentEffects, effect];\n      }\n    } else {\n      nextEffects = [effect];\n    }\n\n    lastClickedEffectRef.current = effect;\n    const nextMode = deriveCursorMode(\n      nextEffects,\n      lastClickedEffectRef.current\n    );\n\n    setContentState((prev) => ({\n      ...prev,\n      cursorEffects: nextEffects,\n      cursorMode: nextMode,\n    }));\n    if (!shiftKey) {\n      props.setMode(false);\n    }\n    chrome.storage.local.set({\n      cursorEffects: nextEffects,\n      cursorMode: nextMode,\n    });\n  };\n\n  const handleClick = (effect) => (event) => {\n    applyCursorSelection(effect, Boolean(event.shiftKey));\n  };\n\n  const cursorEffects = Array.isArray(contentState.cursorEffects)\n    ? contentState.cursorEffects\n    : [];\n  const isDefault = cursorEffects.length === 0;\n  const toggleValues = isDefault ? [\"none\"] : cursorEffects;\n  const isEffectActive = (effect) =>\n    effect === \"none\" ? isDefault : cursorEffects.includes(effect);\n\n  return (\n    <Toolbar.Root\n      className={\"DrawingToolbar\" + \" \" + props.visible}\n      aria-label=\"Cursor options\"\n      tabIndex=\"0\"\n    >\n      <Toolbar.ToggleGroup\n        type=\"multiple\"\n        className=\"ToolbarToggleGroup\"\n        value={toggleValues}\n        onValueChange={() => {}}\n      >\n        <TooltipWrap content=\"Default\" shortcut=\"0\">\n          <div className=\"ToolbarToggleWrap\">\n            <Toolbar.ToggleItem\n              className=\"ToolbarToggleItem\"\n              value=\"none\"\n              data-state={isEffectActive(\"none\") ? \"on\" : \"off\"}\n              onClick={handleClick(\"none\")}\n            >\n              <CursorIcon />\n            </Toolbar.ToggleItem>\n          </div>\n        </TooltipWrap>\n        <Toolbar.Separator className=\"ToolbarSeparator\" />\n        <TooltipWrap\n          content={chrome.i18n.getMessage(\"highlightClicksTooltip\")}\n          shortcut=\"1\"\n        >\n          <div className=\"ToolbarToggleWrap\">\n            <Toolbar.ToggleItem\n              className=\"ToolbarToggleItem\"\n              value=\"target\"\n              data-state={isEffectActive(\"target\") ? \"on\" : \"off\"}\n              onClick={handleClick(\"target\")}\n            >\n              <TargetCursorIcon />\n            </Toolbar.ToggleItem>\n          </div>\n        </TooltipWrap>\n        <TooltipWrap\n          content={chrome.i18n.getMessage(\"highlightCursorTooltip\")}\n          shortcut=\"2\"\n        >\n          <div className=\"ToolbarToggleWrap\">\n            <Toolbar.ToggleItem\n              className=\"ToolbarToggleItem\"\n              value=\"highlight\"\n              data-state={isEffectActive(\"highlight\") ? \"on\" : \"off\"}\n              onClick={handleClick(\"highlight\")}\n            >\n              <HighlightCursorIcon />\n            </Toolbar.ToggleItem>\n          </div>\n        </TooltipWrap>\n        <TooltipWrap\n          content={chrome.i18n.getMessage(\"spotlightCursorTooltip\")}\n          shortcut=\"3\"\n        >\n          <div className=\"ToolbarToggleWrap\">\n            <Toolbar.ToggleItem\n              className=\"ToolbarToggleItem\"\n              value=\"spotlight\"\n              data-state={isEffectActive(\"spotlight\") ? \"on\" : \"off\"}\n              onClick={handleClick(\"spotlight\")}\n            >\n              <SpotlightCursorIcon />\n            </Toolbar.ToggleItem>\n          </div>\n        </TooltipWrap>\n      </Toolbar.ToggleGroup>\n    </Toolbar.Root>\n  );\n};\n\nexport default CursorToolbar;\n"
  },
  {
    "path": "src/pages/Content/toolbar/layout/DrawingToolbar.jsx",
    "content": "import React, {\n  useRef,\n  useEffect,\n  useCallback,\n  useContext,\n  useState,\n} from \"react\";\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\n// Components\nimport ToolTrigger from \"../components/ToolTrigger\";\nimport RadialMenu from \"../components/RadialMenu\";\nimport ShapeToolbar from \"./ShapeToolbar\";\n\n// Canvas utils\nimport {\n  undoCanvas,\n  redoCanvas,\n  saveCanvas,\n} from \"../../canvas/modules/History\";\n\n// Icons\nimport {\n  DrawIcon,\n  EraserIcon,\n  ArrowIcon,\n  ImageIcon,\n  UndoIcon,\n  RedoIcon,\n  TransformIcon,\n  HighlighterIcon,\n  TextIcon,\n  RectangleIcon,\n  TriangleIcon,\n  CircleIcon,\n  RectangleFilledIcon,\n  CircleFilledIcon,\n  TriangleFilledIcon,\n  TrashIcon,\n} from \"../components/SVG\";\n\n// Rewrite imports above with the chrome-extension URL inline\n\nimport TooltipWrap from \"../components/TooltipWrap\";\nimport ImageTool from \"../../canvas/modules/ImageTool\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst DrawingToolbar = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [tool, setTool] = useState(\"\");\n  const contentStateRef = useRef(contentState);\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  const imageFileInput = useRef(null);\n\n  useEffect(() => {\n    setTool(contentState.tool);\n  }, [contentState.tool]);\n\n  const handleImageChange = useCallback(\n    (e) => {\n      const reader = new FileReader();\n      reader.onload = (e) => {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          isAddingImage: true,\n        }));\n\n        // De-select all objects\n        contentState.canvas.discardActiveObject();\n        contentState.canvas.requestRenderAll();\n\n        // Make all objects unselectable\n        contentState.canvas.forEachObject((obj) => {\n          obj.selectable = false;\n        });\n\n        const imgTool = ImageTool(\n          contentState.canvas,\n          e.target.result,\n          contentState,\n          setContentState,\n          saveCanvas,\n          contentStateRef.current\n        );\n\n        imageFileInput.current.value = \"\";\n\n        return () => {\n          imgTool.removeEventListeners();\n        };\n      };\n\n      reader.readAsDataURL(e.target.files[0]);\n    },\n    [contentState, setContentState, saveCanvas]\n  );\n\n  return (\n    <Toolbar.Root\n      className={\"DrawingToolbar\" + \" \" + props.visible}\n      aria-label=\"Drawing tools\"\n    >\n      <Toolbar.ToggleGroup\n        type=\"single\"\n        className=\"ToolbarToggleGroup\"\n        value={tool}\n        onValueChange={(value) => {\n          if (value)\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              tool: value,\n            }));\n        }}\n      >\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"select\"\n          content={chrome.i18n.getMessage(\"selectToolTooltip\")}\n          shortcut=\"1\"\n        >\n          <TransformIcon />\n        </ToolTrigger>\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"pen\"\n          content={chrome.i18n.getMessage(\"penToolTooltip\")}\n          shortcut=\"2\"\n        >\n          <DrawIcon />\n        </ToolTrigger>\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"highlighter\"\n          content={chrome.i18n.getMessage(\"highlighterToolTooltip\")}\n          shortcut=\"3\"\n        >\n          <HighlighterIcon />\n        </ToolTrigger>\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"eraser\"\n          content={chrome.i18n.getMessage(\"eraserToolTooltip\")}\n          shortcut=\"4\"\n        >\n          <EraserIcon />\n        </ToolTrigger>\n        <RadialMenu shortcut=\"5\" />\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"text\"\n          content={chrome.i18n.getMessage(\"textToolTooltip\")}\n          shortcut=\"6\"\n        >\n          <TextIcon />\n        </ToolTrigger>\n        <ShapeToolbar visible={tool === \"shape\" ? \"show-toolbar\" : \"\"} />\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"shape\"\n          content={chrome.i18n.getMessage(\"shapeToolTooltip\")}\n          shortcut=\"7\"\n        >\n          {contentState.shape === \"rectangle\" && contentState.shapeFill ? (\n            <RectangleFilledIcon />\n          ) : contentState.shape === \"circle\" && contentState.shapeFill ? (\n            <CircleFilledIcon />\n          ) : contentState.shape === \"triangle\" && contentState.shapeFill ? (\n            <TriangleFilledIcon />\n          ) : contentState.shape === \"rectangle\" && !contentState.shapeFill ? (\n            <RectangleIcon />\n          ) : contentState.shape === \"circle\" && !contentState.shapeFill ? (\n            <CircleIcon />\n          ) : contentState.shape === \"triangle\" && !contentState.shapeFill ? (\n            <TriangleIcon />\n          ) : null}\n        </ToolTrigger>\n        <ToolTrigger\n          type=\"toggle\"\n          value=\"arrow\"\n          content={chrome.i18n.getMessage(\"arrowToolTooltip\")}\n          shortcut=\"8\"\n        >\n          <ArrowIcon />\n        </ToolTrigger>\n        <ToolTrigger\n          type=\"button\"\n          value=\"image\"\n          content={chrome.i18n.getMessage(\"imageToolTooltip\")}\n          shortcut=\"9\"\n          onClick={(e) => imageFileInput.current.click()}\n        >\n          <ImageIcon />\n          <input\n            type=\"file\"\n            id=\"file\"\n            accept=\"image/*\"\n            style={{ display: \"none\" }}\n            ref={imageFileInput}\n            data-image-upload=\"true\"\n            onChange={handleImageChange}\n          />\n        </ToolTrigger>\n      </Toolbar.ToggleGroup>\n      <Toolbar.Separator className=\"ToolbarSeparator\" />\n      <ToolTrigger\n        type=\"button\"\n        content={chrome.i18n.getMessage(\"undoTooltip\")}\n        disabled={contentState.undoStack.length === 0 ? true : false}\n        onClick={() => undoCanvas(contentState, setContentState)}\n      >\n        <UndoIcon />\n      </ToolTrigger>\n      <ToolTrigger\n        type=\"button\"\n        content={chrome.i18n.getMessage(\"redoTooltip\")}\n        disabled={contentState.redoStack.length === 0 ? true : false}\n        onClick={() => redoCanvas(contentState, setContentState)}\n      >\n        <RedoIcon />\n      </ToolTrigger>\n      <ToolTrigger\n        type=\"button\"\n        content={chrome.i18n.getMessage(\"clearCanvasTooltip\")}\n        shortcut=\"0\"\n        disabled={\n          contentState.canvas\n            ? contentState.canvas.getObjects().length === 0\n              ? true\n              : false\n            : true\n        }\n        onClick={() => {\n          if (!contentState.canvas) return;\n\n          contentState.canvas.clear();\n          contentState.canvas.renderAll();\n          contentState.canvas.requestRenderAll();\n          saveCanvas(contentState, setContentState);\n        }}\n      >\n        <TrashIcon />\n      </ToolTrigger>\n    </Toolbar.Root>\n  );\n};\n\nexport default DrawingToolbar;\n"
  },
  {
    "path": "src/pages/Content/toolbar/layout/ShapeToolbar.jsx",
    "content": "import React, { useEffect, useState, useContext, useRef } from \"react\";\n\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\nimport ToolTrigger from \"../components/ToolTrigger\";\n\n// Icons\nimport {\n  RectangleIcon,\n  CircleIcon,\n  TriangleIcon,\n  RectangleFilledIcon,\n  CircleFilledIcon,\n  TriangleFilledIcon,\n} from \"../components/SVG\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\nconst ShapeToolbar = (props) => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n\n  return (\n    <div\n      aria-label=\"Cursor options\"\n      tabIndex=\"0\"\n      className={\"shapeToolbar \" + props.visible}\n    >\n      <Toolbar.ToggleGroup\n        type=\"single\"\n        className=\"ToolbarToggleGroup\"\n        value={contentState.shape}\n        onValueChange={(value) => {\n          if (value)\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              shape: value,\n            }));\n          chrome.storage.local.set({ shape: value });\n        }}\n      >\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem className=\"ToolbarToggleItem\" value=\"rectangle\">\n            {contentState.shapeFill ? (\n              <RectangleFilledIcon />\n            ) : (\n              <RectangleIcon />\n            )}\n          </Toolbar.ToggleItem>\n        </div>\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem className=\"ToolbarToggleItem\" value=\"circle\">\n            {contentState.shapeFill ? <CircleFilledIcon /> : <CircleIcon />}\n          </Toolbar.ToggleItem>\n        </div>\n        <div className=\"ToolbarToggleWrap\">\n          <Toolbar.ToggleItem className=\"ToolbarToggleItem\" value=\"triangle\">\n            {contentState.shapeFill ? <TriangleFilledIcon /> : <TriangleIcon />}\n          </Toolbar.ToggleItem>\n        </div>\n      </Toolbar.ToggleGroup>\n      <Toolbar.Separator className=\"ToolbarSeparator\" />\n      <ToolTrigger\n        type=\"button\"\n        value=\"fill\"\n        content={chrome.i18n.getMessage(\"toggleFillTooltip\")}\n        onClick={() => {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            shapeFill: !contentState.shapeFill,\n          }));\n          chrome.storage.local.set({ shapeFill: !contentState.shapeFill });\n        }}\n      >\n        {contentState.shape === \"rectangle\" && contentState.shapeFill ? (\n          <RectangleIcon />\n        ) : contentState.shape === \"circle\" && contentState.shapeFill ? (\n          <CircleIcon />\n        ) : contentState.shape === \"triangle\" && contentState.shapeFill ? (\n          <TriangleIcon />\n        ) : contentState.shape === \"rectangle\" && !contentState.shapeFill ? (\n          <RectangleFilledIcon />\n        ) : contentState.shape === \"circle\" && !contentState.shapeFill ? (\n          <CircleFilledIcon />\n        ) : contentState.shape === \"triangle\" && !contentState.shapeFill ? (\n          <TriangleFilledIcon />\n        ) : null}\n      </ToolTrigger>\n    </div>\n  );\n};\n\nexport default ShapeToolbar;\n"
  },
  {
    "path": "src/pages/Content/toolbar/layout/ToolbarWrap.jsx",
    "content": "import React, {\n  useLayoutEffect,\n  useEffect,\n  useContext,\n  useState,\n  useRef,\n} from \"react\";\nimport * as Toolbar from \"@radix-ui/react-toolbar\";\n\nimport { Rnd } from \"react-rnd\";\n\n// Layout\nimport DrawingToolbar from \"./DrawingToolbar\";\nimport CursorToolbar from \"./CursorToolbar\";\nimport BlurToolbar from \"./BlurToolbar\";\n\n// Components\nimport ToolTrigger from \"../components/ToolTrigger\";\nimport Toast from \"../components/Toast\";\n\nimport { CloseIconPopup } from \"../components/SVG\";\n\n// Context\nimport { contentStateContext } from \"../../context/ContentState\";\n\n// Icons\nimport {\n  GrabIcon,\n  StopIcon,\n  DrawIcon,\n  PauseIcon,\n  ResumeIcon,\n  CursorIcon,\n  TargetCursorIcon,\n  HighlightCursorIcon,\n  SpotlightCursorIcon,\n  RestartIcon,\n  DiscardIcon,\n  CameraIcon,\n  BlurIcon,\n  OnboardingArrow,\n  CloseButtonToolbar,\n} from \"../components/SVG\";\nimport MicToggle from \"../components/MicToggle\";\n\nconst ToolbarWrap = () => {\n  const [contentState, setContentState, t, setT] =\n    useContext(contentStateContext);\n  const [mode, setMode] = React.useState(\"\");\n  const modeRef = React.useRef(mode);\n  const [hovering, setHovering] = React.useState(false);\n  const DragRef = React.useRef(null);\n  const ToolbarRef = React.useRef(null);\n  const [side, setSide] = React.useState(\"ToolbarTop\");\n  const [elastic, setElastic] = React.useState(\"\");\n  const [shake, setShake] = React.useState(\"\");\n  const [dragging, setDragging] = React.useState(\"\");\n  const [timer, setTimer] = React.useState(0);\n  const [timestamp, setTimestamp] = React.useState(\"00:00\");\n  const [transparent, setTransparent] = React.useState(false);\n  const [forceTransparent, setForceTransparent] = React.useState(\"\");\n  const [visuallyHidden, setVisuallyHidden] = useState(false);\n  const timeRef = React.useRef(\"\");\n\n  useEffect(() => {\n    modeRef.current = mode;\n  }, [mode]);\n\n  useEffect(() => {\n    setContentState((prev) => ({\n      ...prev,\n      setToolbarMode: setMode,\n      toolbarMode: mode,\n    }));\n  }, [mode, setContentState]);\n\n  useEffect(() => {\n    if (contentState.toolbarHover && contentState.hideUI) {\n      setTransparent(\"ToolbarTransparent\");\n    } else {\n      setTransparent(false);\n      setForceTransparent(\"\");\n    }\n  }, [contentState.toolbarHover, contentState.hideUI]);\n\n  // If mouse is down and toolbarHover is true, set forceTransparent\n  useEffect(() => {\n    if (!contentState.toolbarHover) return;\n    if (!contentState.shadowRef) return;\n    if (!contentState.hideUI) return;\n    const handleMouseDown = (e) => {\n      if (contentState.toolbarHover && contentState.hideUI) {\n        // check if mouse is over toolbar\n        if (ToolbarRef.current && ToolbarRef.current.contains(e.target)) return;\n        if (\n          contentState.shadowRef &&\n          (contentState.shadowRef.contains(e.target) ||\n            contentState.shadowRef === e.target ||\n            contentState.shadowRef === e.target.parentNode)\n        )\n          return;\n\n        setForceTransparent(\"ForceTransparent\");\n      }\n    };\n\n    const handleMouseUp = (e) => {\n      setForceTransparent(\"\");\n    };\n\n    document.addEventListener(\"mousedown\", handleMouseDown);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleMouseDown);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n  }, [contentState.toolbarHover, contentState.shadowRef, contentState.hideUI]);\n\n  useEffect(() => {\n    if (!isNaN(t)) {\n      setTimer(t);\n      const clampedT = Math.max(0, t); // prevent negative values\n      const hours = Math.floor(clampedT / 3600);\n      const minutes = Math.floor((clampedT % 3600) / 60);\n      const seconds = clampedT % 60;\n\n      // Determine the timestamp format based on the total duration (t)\n      let newTimestamp =\n        hours > 0\n          ? `${hours.toString().padStart(2, \"0\")}:${minutes\n              .toString()\n              .padStart(2, \"0\")}:${seconds.toString().padStart(2, \"0\")}`\n          : `${minutes.toString().padStart(2, \"0\")}:${seconds\n              .toString()\n              .padStart(2, \"0\")}`;\n\n      // Adjust the width of the time display based on the duration\n      if (hours > 0) {\n        // Adjust for HH:MM:SS format when hours are present\n        timeRef.current.style.width = \"58px\"; // You might need to adjust this value based on your actual UI\n      } else {\n        // Adjust for MM:SS format when there are no hours\n        timeRef.current.style.width = \"42px\"; // Adjust this value as needed\n      }\n\n      setTimestamp(newTimestamp);\n    }\n  }, [t]);\n\n  useLayoutEffect(() => {\n    function setToolbarPosition(e) {\n      let xpos = DragRef.current.getDraggablePosition().x;\n      let ypos = DragRef.current.getDraggablePosition().y;\n\n      // Width and height of toolbar\n      const width = ToolbarRef.current.getBoundingClientRect().width;\n      const height = ToolbarRef.current.getBoundingClientRect().height;\n\n      // Keep toolbar positioned relative to the bottom and right of the screen, proportionally\n      if (xpos + width + 30 > window.innerWidth) {\n        xpos = window.innerWidth - width - 30;\n      }\n      if (ypos + height - 60 > window.innerHeight) {\n        ypos = window.innerHeight - height + 60;\n      }\n\n      DragRef.current.updatePosition({ x: xpos, y: ypos });\n    }\n    window.addEventListener(\"resize\", setToolbarPosition);\n    setToolbarPosition();\n    return () => window.removeEventListener(\"resize\", setToolbarPosition);\n  }, []);\n\n  const handleChange = (value) => {\n    setMode(value);\n  };\n\n  const handleDragStart = (e, d) => {\n    setDragging(\"ToolbarDragging\");\n  };\n\n  const handleDrag = (e, d) => {\n    // Width and height\n    const width = ToolbarRef.current.getBoundingClientRect().width;\n    const height = ToolbarRef.current.getBoundingClientRect().height;\n\n    if (d.y < 130) {\n      setSide(\"ToolbarBottom\");\n    } else {\n      setSide(\"ToolbarTop\");\n    }\n\n    if (\n      d.x < -25 ||\n      d.x + width > window.innerWidth ||\n      d.y < 60 ||\n      d.y + height - 80 > window.innerHeight\n    ) {\n      setShake(\"ToolbarShake\");\n    } else {\n      setShake(\"\");\n    }\n  };\n\n  const handleDrop = (e, d) => {\n    setShake(\"\");\n    setDragging(\"\");\n    let xpos = d.x;\n    let ypos = d.y;\n\n    // Width and height\n    const width = ToolbarRef.current.getBoundingClientRect().width;\n    const height = ToolbarRef.current.getBoundingClientRect().height;\n\n    // Check if toolbar is off screen\n    if (d.x < -10) {\n      setElastic(\"ToolbarElastic\");\n      xpos = -10;\n    } else if (d.x + width + 30 > window.innerWidth) {\n      setElastic(\"ToolbarElastic\");\n      xpos = window.innerWidth - width - 30;\n    }\n\n    if (d.y < 130) {\n      setSide(\"ToolbarBottom\");\n    } else {\n      setSide(\"ToolbarTop\");\n    }\n\n    if (d.y < 80) {\n      setElastic(\"ToolbarElastic\");\n      ypos = 80;\n    } else if (d.y + height - 60 > window.innerHeight) {\n      setElastic(\"ToolbarElastic\");\n      ypos = window.innerHeight - height + 60;\n    }\n    DragRef.current.updatePosition({ x: xpos, y: ypos });\n\n    setTimeout(() => {\n      setElastic(\"\");\n    }, 250);\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      toolbarPosition: {\n        ...prevContentState.toolbarPosition,\n        offsetX: xpos,\n        offsetY: ypos,\n        left: xpos < window.innerWidth / 2 ? true : false,\n        right: xpos < window.innerWidth / 2 ? false : true,\n        top: ypos < window.innerHeight / 2 ? true : false,\n        bottom: ypos < window.innerHeight / 2 ? false : true,\n      },\n    }));\n\n    // Is it on the left or right, also top or bottom\n\n    let left = xpos < window.innerWidth / 2 ? true : false;\n    let right = xpos < window.innerWidth / 2 ? false : true;\n    let top = ypos < window.innerHeight / 2 ? true : false;\n    let bottom = ypos < window.innerHeight / 2 ? false : true;\n    let offsetX = xpos;\n    let offsetY = ypos;\n\n    if (right) {\n      offsetX = window.innerWidth - xpos;\n    }\n    if (bottom) {\n      offsetY = window.innerHeight - ypos;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      toolbarPosition: {\n        ...prevContentState.toolbarPosition,\n        offsetX: offsetX,\n        offsetY: offsetY,\n        left: left,\n        right: right,\n        top: top,\n        bottom: bottom,\n      },\n    }));\n\n    chrome.storage.local.set({\n      toolbarPosition: {\n        offsetX: offsetX,\n        offsetY: offsetY,\n        left: left,\n        right: right,\n        top: top,\n        bottom: bottom,\n      },\n    });\n  };\n\n  useEffect(() => {\n    let x = contentState.toolbarPosition.offsetX;\n    let y = contentState.toolbarPosition.offsetY;\n\n    if (contentState.toolbarPosition.bottom) {\n      y = window.innerHeight - contentState.toolbarPosition.offsetY;\n    }\n\n    if (contentState.toolbarPosition.right) {\n      x = window.innerWidth - contentState.toolbarPosition.offsetX;\n    }\n\n    DragRef.current.updatePosition({ x: x, y: y });\n\n    handleDrop(null, { x: x, y: y });\n  }, []);\n\n  useEffect(() => {\n    if (!contentState.openToast) return;\n    if (contentState.drawingMode) {\n      contentState.openToast(chrome.i18n.getMessage(\"drawingModeToast\"), () => {\n        setMode(\"\");\n      });\n    }\n    if (contentState.blurMode) {\n      contentState.openToast(chrome.i18n.getMessage(\"blurModeToast\"), () => {\n        setMode(\"\");\n      });\n    }\n  }, [contentState.drawingMode, contentState.blurMode, contentState.openToast]);\n\n  // useEffect(() => {\n  //   let nextMode = modeRef.current;\n  //   if (contentState.drawingMode) {\n  //     nextMode = \"draw\";\n  //   } else if (contentState.blurMode) {\n  //     nextMode = \"blur\";\n  //   } else if (modeRef.current === \"draw\" || modeRef.current === \"blur\") {\n  //     nextMode = \"\";\n  //   }\n\n  //   if (nextMode !== modeRef.current) {\n  //     setMode(nextMode);\n  //   }\n  // }, [contentState.drawingMode, contentState.blurMode]);\n\n  useEffect(() => {\n    // one-time init\n    if (contentState.drawingMode) setMode(\"draw\");\n    else if (contentState.blurMode) setMode(\"blur\");\n    else setMode(\"\");\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (mode === \"draw\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        drawingMode: true,\n        showOnboardingArrow: false,\n      }));\n    } else {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        drawingMode: false,\n      }));\n    }\n    if (mode === \"blur\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        blurMode: true,\n        drawingMode: false,\n      }));\n    } else {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        blurMode: false,\n      }));\n    }\n  }, [mode]);\n\n  const enableCamera = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      cameraActive: true,\n    }));\n    chrome.storage.local.set({\n      cameraActive: true,\n    });\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      pipEnded: true,\n    }));\n  };\n\n  return (\n    <div>\n      <Toast />\n      <div\n        className={\n          contentState.paused && contentState.recording\n            ? \"ToolbarPaused\"\n            : \"ToolbarPaused hidden\"\n        }\n      ></div>\n      <div className={\"ToolbarBounds\" + \" \" + shake}></div>\n      <Rnd\n        default={{\n          x: 200,\n          y: 500,\n        }}\n        className={\n          \"react-draggable\" + \" \" + elastic + \" \" + shake + \" \" + dragging\n        }\n        dragHandleClassName=\"grab\"\n        enableResizing={false}\n        onDragStart={handleDragStart}\n        onDrag={handleDrag}\n        onDragStop={handleDrop}\n        ref={DragRef}\n        id=\"pro-onboarding-recording-toolbar\"\n      >\n        <Toolbar.Root\n          id=\"pro-onboarding-recording-toolbar-root\"\n          className={\n            \"ToolbarRoot\" +\n            \" \" +\n            side +\n            \" \" +\n            transparent +\n            \" \" +\n            forceTransparent +\n            (visuallyHidden ? \" visually-hidden-toolbar\" : \"\")\n          }\n          ref={ToolbarRef}\n          onMouseOver={() => {\n            setHovering(true);\n          }}\n          onMouseLeave={() => {\n            setHovering(false);\n          }}\n        >\n          <ToolTrigger grab type=\"button\" content=\"\">\n            <GrabIcon />\n          </ToolTrigger>\n          {!contentState.recording && (\n            <div\n              className={`popup-controls toolbar-controls ${\n                hovering ? \"open\" : \"\"\n              }`}\n              onClick={() => {\n                // Show the toast first\n                if (contentState.openToast) {\n                  contentState.openToast(\n                    chrome.i18n.getMessage(\"reopenToolbarToast\"),\n                    () => {},\n                  );\n                }\n\n                // Visually hide the toolbar\n                setVisuallyHidden(true);\n\n                setContentState((prev) => ({\n                  ...prev,\n\n                  drawingMode: false,\n                  blurMode: false,\n                }));\n                // After toast finishes (~3s), apply real hiding logic\n                setTimeout(() => {\n                  setContentState((prev) => ({\n                    ...prev,\n                    hideToolbar: true,\n                    drawingMode: false,\n                    blurMode: false,\n                    hideUIAlerts: false,\n                    toolbarHover: false,\n                    hideUI: true,\n                  }));\n\n                  chrome.storage.local.set({\n                    hideToolbar: true,\n                    hideUIAlerts: false,\n                    toolbarHover: false,\n                    hideUI: true,\n                  });\n                }, 3000); // match your toast duration\n              }}\n            >\n              <div className=\"popup-control popup-close\">\n                <CloseIconPopup />\n              </div>\n            </div>\n          )}\n          <div\n            className={\"ToolbarRecordingControls\"}\n            id=\"pro-onboarding-recording-toolbar-controls\"\n          >\n            <ToolTrigger\n              type=\"button\"\n              content={chrome.i18n.getMessage(\"finishRecordingTooltip\")}\n              disabled={!contentState.recording}\n              onClick={() => {\n                contentState.stopRecording();\n              }}\n            >\n              <StopIcon width=\"20\" height=\"20\" />\n            </ToolTrigger>\n            <div\n              className={`ToolbarRecordingTime ${\n                contentState.timeWarning ? \"TimerWarning\" : \"\"\n              }`}\n              ref={timeRef}\n            >\n              {timestamp}\n            </div>\n            <ToolTrigger\n              type=\"button\"\n              content={chrome.i18n.getMessage(\"restartRecordingTooltip\")}\n              disabled={!contentState.recording}\n              onClick={() => {\n                contentState.tryRestartRecording();\n              }}\n            >\n              <RestartIcon />\n            </ToolTrigger>\n            {!contentState.paused && (\n              <ToolTrigger\n                type=\"button\"\n                content={chrome.i18n.getMessage(\"pauseRecordingTooltip\")}\n                disabled={!contentState.recording}\n                onClick={() => {\n                  contentState.pauseRecording();\n                }}\n              >\n                <PauseIcon />\n              </ToolTrigger>\n            )}\n            {contentState.recording && contentState.paused && (\n              <ToolTrigger\n                type=\"button\"\n                resume\n                content={chrome.i18n.getMessage(\"resumeRecordingTooltip\")}\n                disabled={!contentState.recording}\n                onClick={() => {\n                  contentState.resumeRecording();\n                }}\n              >\n                <ResumeIcon />\n              </ToolTrigger>\n            )}\n            <ToolTrigger\n              type=\"button\"\n              content={chrome.i18n.getMessage(\"cancelRecordingTooltip\")}\n              disabled={!contentState.recording}\n              onClick={() => {\n                if (contentState.tryDismissRecording !== undefined) {\n                  contentState.tryDismissRecording();\n                }\n              }}\n            >\n              <DiscardIcon />\n            </ToolTrigger>\n          </div>\n          <Toolbar.Separator className=\"ToolbarSeparator\" />\n          <Toolbar.ToggleGroup\n            type=\"single\"\n            className=\"ToolbarToggleGroup\"\n            value={mode}\n            onValueChange={handleChange}\n          >\n            <div className=\"ToolbarToggleWrap\">\n              {contentState.showOnboardingArrow && (\n                <div className=\"OnboardingArrow\">\n                  <div className=\"OnboardingText\">\n                    {chrome.i18n.getMessage(\"clickHereDrawOnboarding\")}\n                  </div>\n                  <div className=\"ArrowShape\">\n                    <OnboardingArrow />\n                  </div>\n                </div>\n              )}\n              <ToolTrigger\n                type=\"mode\"\n                content={chrome.i18n.getMessage(\"toggleDrawingToolsTooltip\")}\n                value=\"draw\"\n                shortcut={contentState.toggleDrawingModeShortcut}\n              >\n                {mode === \"draw\" && <CloseButtonToolbar />}\n                {mode !== \"draw\" && <DrawIcon />}\n              </ToolTrigger>\n              <DrawingToolbar visible={mode === \"draw\" ? \"show-toolbar\" : \"\"} />\n            </div>\n            <div className=\"ToolbarToggleWrap\">\n              <ToolTrigger\n                type=\"mode\"\n                content={chrome.i18n.getMessage(\"toggleBlurToolTooltip\")}\n                value=\"blur\"\n                shortcut={contentState.toggleBlurModeShortcut}\n              >\n                {mode === \"blur\" && <CloseButtonToolbar />}\n                {mode !== \"blur\" && <BlurIcon />}\n              </ToolTrigger>\n              <BlurToolbar visible={mode === \"blur\" ? \"show-toolbar\" : \"\"} />\n            </div>\n\n            <div className=\"ToolbarToggleWrap\">\n              <ToolTrigger\n                type=\"mode\"\n                content={chrome.i18n.getMessage(\"toggleCursorOptionsTooltip\")}\n                value=\"cursor\"\n                shortcut={contentState.toggleCursorModeShortcut}\n              >\n                {contentState.cursorMode === \"target\" && <TargetCursorIcon />}\n                {contentState.cursorMode === \"highlight\" && (\n                  <HighlightCursorIcon />\n                )}\n                {contentState.cursorMode === \"spotlight\" && (\n                  <SpotlightCursorIcon />\n                )}\n                {contentState.cursorMode === \"none\" && <CursorIcon />}\n              </ToolTrigger>\n              <CursorToolbar\n                visible={mode === \"cursor\" ? \"show-toolbar\" : \"\"}\n                mode={mode}\n                setMode={setMode}\n              />\n            </div>\n            <Toolbar.Separator className=\"ToolbarSeparator\" />\n            <MicToggle />\n            {(!contentState.cameraActive ||\n              contentState.defaultVideoInput === \"none\") &&\n              (!contentState.isSubscribed || !contentState.recording) &&\n              contentState.recordingType != \"camera-only\" && (\n                <ToolTrigger\n                  type=\"button\"\n                  content={\n                    contentState.cameraActive && contentState.cameraPermission\n                      ? chrome.i18n.getMessage(\"disableCameraTooltip\")\n                      : !contentState.cameraActive &&\n                        contentState.cameraPermission\n                      ? chrome.i18n.getMessage(\"enableCameraTooltip\")\n                      : chrome.i18n.getMessage(\"noCameraPermissionsTooltip\")\n                  }\n                  value=\"camera\"\n                  onClick={enableCamera}\n                  disabled={\n                    !contentState.cameraPermission ||\n                    contentState.defaultVideoInput === \"none\"\n                  }\n                >\n                  <CameraIcon />\n                </ToolTrigger>\n              )}\n          </Toolbar.ToggleGroup>\n        </Toolbar.Root>\n      </Rnd>\n    </div>\n  );\n};\n\nexport default ToolbarWrap;\n"
  },
  {
    "path": "src/pages/Content/toolbar/styles/_Page.scss",
    "content": "@use '../../styles/_variables' as *;\n@use './layout/Toolbar';\n@use './layout/DrawingToolbar';\n@use './layout/ShapeToolbar';\n@use './components/Tooltip';\n@use './components/RadialMenu';\n@use './components/ColorWheel';\n@use './components/StrokeWidth';\n@use './components/Toast';\n\n.toolbar-page {\n\twidth: 100%;\n\theight: 100%;\n\tpointer-events: none!important;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/components/_ColorWheel.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.wheel-trigger {\n\ttransition: transform .2s cubic-bezier(.61,.11,.08,.96), width .2s cubic-bezier(.61,.11,.08,.96), height .2s cubic-bezier(.61,.11,.08,.96), opacity .25s cubic-bezier(.61,.11,.08,.96);\n  position: absolute;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\tbottom: 0;\n\topacity: 0;\n\tmargin: auto;\n\twidth: 18px;\n\tbox-sizing: border-box;\n\theight: 18px;\n\tz-index: 9999;\n\n\t\tbox-sizing: border-box;\n    background-blend-mode: screen;\n    border-radius: 50%;\n\n\t\t\n\t\t&:hover {\n\t\t\tcursor: pointer;\n\t\t}\n}\n.wheel-trigger .radial-menu-item-child {\n\ttransform: rotate(30deg);\n\t&:after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\tleft: 0;\n\t\tright: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tz-index: 9999999;\n\t\tborder-radius: 50%;\n\t\tbox-sizing: border-box;\n\t\tborder: 1px solid rgba(0,0,0,.2);\n\t}\n}\n\n.color-wheel .wheel-trigger {\n\twidth: 100px!important;\n\theight: 100px!important;\n\ttransform: rotate(320deg) translate(0px)!important;\n\tz-index: 999999999999!important;\n\tfilter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n}\n.color-wheel-handle {\n\twidth: 12px!important;\n\theight: 12px!important;\n\tborder-radius: 50%;\n\tleft: 20px;\n\ttop: 20px;\n\topacity: 0;\n\tbackground-color: #F17FD7;\n\tborder: 2px solid white;\n\tz-index: 999999999999;\n\tdisplay: none;\n\ttransition: width .25s cubic-bezier(.61,.11,.08,.96), height .25s cubic-bezier(.61,.11,.08,.96), margin-left .25s cubic-bezier(.61,.11,.08,.96), margin-top .25s cubic-bezier(.61,.11,.08,.96);\n\n\t&:hover {\n\t\twidth: 18px!important;\n\t\theight: 18px!important;\n\t\tmargin-left: -2px;\n\t\tmargin-top: -2px;\n\t}\n}\n.color-wheel .color-wheel-handle {\n\topacity: 0;\n\tdisplay: block;\n\tanimation: fadeIn 0.25s cubic-bezier(.61,.11,.08,.96) forwards;\n\tanimation-delay: .5s;\n}\n.w-color-wheel {\n\tpointer-events: none;\n\tposition: absolute!important;\n\twidth: 100%!important;\n\theight: 100%!important;\n\tz-index: 99999999!important;\n\n\t&::after {\n\t\tcontent: \"\";\n\t\tbox-sizing: border-box;\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tposition: absolute;\n\t\tz-index: 9999999;\n\t\tborder: 1px solid rgba(0,0,0,.2);\n    box-sizing: border-box;\n    border-radius: 50%;\n\t}\n}\n.color-wheel .w-color-wheel {\n\tpointer-events: all!important;\n}\n.w-color-wheel-fill {\n\tbox-shadow: none!important;\n\tborder: 2px solid white!important;\n\twidth: 14px!important;\n\theight: 14px!important;\n\ttransition: width .2s cubic-bezier(.61,.11,.08,.96), height .2s cubic-bezier(.61,.11,.08,.96), margin .2s cubic-bezier(.61,.11,.08,.96)!important;\n\tbox-sizing: border-box!important;\n\tmargin-left: -2px!important;\n\tmargin-top: -2px!important;\n\tz-index: 9999999999!important;\n}\n.w-color-wheel-pointer {\n\tz-index: 99999999999!important;\n\topacity: 0;\n\tanimation: none!important\n}\n.color-wheel .w-color-wheel-pointer {\n\tanimation: fadeInScale .25s cubic-bezier(0.215, 0.610, 0.355, 1) forwards!important;\n\tanimation-delay: .28s!important;\n}\n.w-color-wheel-fill:hover {\n\twidth: 18px!important;\n\theight: 18px!important;\n\tmargin-left: -4px!important;\n\tmargin-top: -4px!important;\n}\n/* Fade in keyframes */\n@keyframes fadeInScale {\n\t0% {\n\t\topacity: 0;\n\t}\n\t100% {\n\t\topacity: 1;\n\t}\n}\n\n.color-wheel, .color-wheel .wheel-trigger, .color-wheel .wheel-trigger .radial-menu-item-child, .color-wheel-handle {\n\tcursor: pointer!important;\n}\n\n.color-wheel-input {\n\tbackground: #000;\n\tborder-radius: 30px;\n\theight: 29px;\n\tcolor: #FFF;\n\ttext-align: center;\n\tline-height: 29px;\n\tpadding-left: 8px;\n\tpadding-right: 8px;\n\tposition: absolute;\n\tmargin-top: -35px;\n\tfont-family: $font-medium;\n\tleft: 50%;\n\ttransform: translate(-50%, 0);\n\topacity: 0;\n}\n.color-wheel .color-wheel-input {\n\tanimation: fadeIn .3s cubic-bezier(.61,.11,.08,.96) forwards;\n\tanimation-delay: .2s;\n\tpointer-events: all!important;\n}\n.color-wheel-input {\n\tpointer-events: none;\n}\n\n@keyframes fadeIn {\n\t0% {\n\t\topacity: 0;\n\t\tmargin-top: -35px;\n\t}\n\t100% {\n\t\topacity: 1;\n\t\tmargin-top: -40px;\n\t}\n}\n\n.color-active .color-preview {\n\topacity: 1;\n}\n.radial-menu[data-state='closed'] .color-preview {\n\topacity: 0!important;\n}\n.color-preview {\n\twidth: 90%;\n\theight: 90%;\n\tbox-sizing: border-box;\n\tborder-radius: 50%;\n\tposition: absolute;\n\tleft: 50%;\n\ttop: 50%;\n\tz-index: 9999999999;\n\ttransform: translate(-50%, -50%);\n\topacity: 0;\n\tanimation: none;\n\tpointer-events: none;\n\tborder: 1px solid #FFF;\n\tbox-sizing: border-box;\n}\n.color-wheel .color-preview {\n\topacity: 0!important;\n}\n.wheel-trigger .color-active {\n\tbox-shadow: none!important;\n}\n.color-active .w-color-wheel {\n\ttransform: scale(1.15)!important;\n\t&::after {\n\t\tborder: none!important;\n\t}\n}\n.color-wheel .w-color-wheel {\n\ttransform: scale(1)!important;\n\t&::after {\n\t\tborder: 1px solid rgba(0,0,0,.2)!important;\n\t}\n}\n.radial-menu[data-state='closed'] .w-color-wheel\n{\n\ttransform: scale(1)!important;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/components/_RadialMenu.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.radial-menu {\n\tposition: absolute;\n\tz-index: 9999999999999;\n\twidth: 100px;\n\theight: 100px;\n\ttop: -66px;\n\tleft: -49px;\n\tpointer-events: none;\n\topacity: 1;\n\ttransform: scale(1);\n\ttransition: transform .3s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity .25s ease-in-out;\n\n\t&::after {\n\t\tcontent: '';\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tz-index: -1;\n\t\tborder: 28px solid #FFF;\n\t\tbox-sizing: border-box;\n\t\tborder-radius: 50%;\n\t\tbackdrop-filter: blur(40px);\n\t\topacity: 0;\n\t\tfilter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n\t\ttransform: scale(0);\n\t\ttransition: transform .25s cubic-bezier(0.18, -0.55, 0.265, 1.45), opacity .2s ease-in-out;\n\t\ttransition-delay: .05s;\n\t}\n\t&[data-state='open'] {\n\t\ttransform: scale(1);\n\t\topacity: 1;\n\t\tpointer-events: all!important;\n\t}\n\t&[data-state='open']::after {\n\t\ttransform: scale(1);\n\t\tborder: 28px solid #FFF;\n\t\topacity: 1;\n\t}\n}\n.color-wheel::after {\n\topacity: 0!important;\n}\n.eyedropper {\n\tposition: absolute;\n\tleft: 0px;\n\ttop: 0px;\n\tright: 0px;\n\tbottom: 0px;\n\tmargin: auto;\n\twidth: 16px;\n\theight: 16px;\n\tpadding: 8px;\n\tz-index: 999999999;\n\tbackground-color: #FFF;\n\tborder-radius: 50%;\n\topacity: 0;\n\ttext-align: center;\n\ttransition: transform .25s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity .3s ease-in-out, background-color .25s ease-in-out!important;\n\ttransform: scale(0);\n\toverflow: hidden;\n\ttransform-style: preserve-3d;\n\n\tsvg {\n\t\tcolor: $color-icon;\n\t}\n\t&:focus-visible {\n\t\toutline: none;\n\t\tbackground-color: #E6E7EA!important;\n\t}\n\t&:hover {\n\t\tcursor: pointer;\n\t\tbackground-color: #E6E7EA;\n\t}\n}\n.eye-active {\n\tbackground-color: $color-primary!important;\n\tsvg {\n\t\tcolor: #FFF!important;\n\t\tfill: #FFF!important;\n\t}\n}\n.color-wheel .eyedropper {\n\topacity: 0!important;\n\tpointer-events: none!important;\n\ttransform: scale(0)!important;\n}\n.radial-menu[data-state='open'] {\n\t.eyedropper {\n\t\ttransform: scale(1)!important;\n\t\topacity: 1;\n\t}\n}\n\n.radial-menu-items {\n\ttransform: rotate(10deg);\n\tz-index: 99999999;\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\tbottom: 0;\n\tmargin: auto;\n}\n.radial-menu-item {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\tbottom: 0;\n\tmargin: auto;\n\twidth: 18px;\n\theight: 18px;\n\tz-index: 999;\n\tborder-radius: 50%;\n\ttext-align: center;\n\tbox-sizing: border-box;\n\tline-height: 50px;\n\tcolor: white;\n\tborder: 1px solid rgba(0,0,0,.2);\n\ttransition: transform .25s cubic-bezier(.61,.11,.08,.96), opacity .25s cubic-bezier(.61,.11,.08,.96);\n\topacity: 0;\n\n\t&:hover {\n\t\tcursor: pointer;\n\t}\n}\n.color-wheel .radial-menu-item {\n\topacity: 0!important;\n}\n\n.radial-menu-item-child {\n\twidth: 100%;\n\theight: 100%;\n\tposition: absolute;\n\tbox-sizing: border-box;\n\tborder: 0px;\n\tbox-shadow: none;\n\ttop: 0px;\n\tpointer-events: none;\n\tz-index: 9999999;\n\tleft: 0px;\n\tborder-radius: 50%;\n\tbackground-size: cover;\n\n\t&:focus-visible {\n\t\toutline: none;\n\t\tbox-shadow: $focus-border;\n\t}\n}\n\n.radial-menu[data-state='open'] .radial-menu-item-child {\n\tpointer-events: all!important;\n}\n\n$elements: 9;\n@for $i from 0 to $elements {\n  .radial-menu-item:nth-child(#{$i + 1}), .wheel-trigger {\n\t\ttransform: rotate(#{360 / $elements * $i}deg) translate(0px)\n\t}\n\t.radial-menu[data-state='open'] .radial-menu-item:nth-child(#{$i + 1}), .radial-menu[data-state='open'] .wheel-trigger {\n\t\ttransition-delay:calc(.25s - #{.02 * $i}s);\n\t\ttransform: rotate(#{360 / $elements * $i}deg) translate(36px);\n\t\topacity: 1;\n\t}\n}\n\n$stops: ();\n$totalStops:12;\n$radius:0.9;\n\n@for $i from 0 through $totalStops{\n  $stops:  append($stops,hsl($i *(360deg/$totalStops),100%,50%),comma);\n}\n\n.color-active {\n\tborder: 1px solid #FFFFFF;\n\tbox-shadow: 0px 0px 0px 2px #0D99FF;\n}\n\n.color-wheel .color-active {\n\tborder: none!important;\n\tbox-shadow: none!important;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/components/_StrokeWidth.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.stroke-width-item {\n\tspan {\n\t\twidth: 18px;\n\t\theight: 18px;\n\t\tdisplay: block;\n\t}\n\tdiv[data-state='on'] {\n\n\t\t\tbackground: $color-primary!important;\n\n\t\tsvg {\n\t\t\tcolor: #FFF!important;\n\t\t\tfill: #FFF!important;\n\t\t}\n\t}\n\tdiv[data-state='off'] {\n\t\tsvg {\n\t\t\tfill: #201F1D;\n\t\t}\n\t}\n}\n\n.stroke-icon svg {\n\ttext-align: center;\n\tmargin: auto;\n\tdisplay: block;\n\twidth: 100%;\n\theight: 100%;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/components/_Toast.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n\n.ToastViewport {\n  --viewport-padding: 25px;\n  position: fixed;\n  bottom: 0;\n  right: 0;\n\tleft: 0;\n\tmargin: auto!important;\n  display: flex;\n  flex-direction: column;\n  padding: var(--viewport-padding);\n  gap: 14px;\n  max-width: 100vw;\n\twidth: fit-content;\n  list-style: none;\n  z-index: 2147483647;\n  outline: none;\n\tpointer-events: all!important;\n}\n\n.ToastRoot {\n  background-color: $color-text-primary;\n\tcolor: $color-text-contrast;\n  border-radius: $container-border-radius;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  padding: 10px 14px;\n  display: flex;\n\tflex-direction: row;\n\tgap: 8px;\n\tfont-size: 15px;\n\tline-height: 1.5;\n\tmax-width: 100%;\n\toverflow: hidden;\n\tjustify-content: center;\n\talign-items: center;\n}\n.ToastRoot[data-state='open'] {\n  animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n.ToastRoot[data-state='closed'] {\n  animation: hide 100ms ease-in;\n}\n.ToastRoot[data-swipe='move'] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n.ToastRoot[data-swipe='cancel'] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n.ToastRoot[data-swipe='end'] {\n  animation: swipeOut 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n  }\n}\n\n@keyframes slideIn {\n  from {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n\n@keyframes swipeOut {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(100% + var(--viewport-padding)));\n  }\n}\n\n.ToastTitle {\n  color: $color-text-contrast;\n\tfont-family: $font-medium;\n}\n\n.ToastDescription {\n  color: var(--slate-11);\n\tfont-family: $font-medium;\n}\n\n.ToastAction {\n\tcolor: $color-text-contrast;\n  font-family: $font-medium;\n\ttext-align: right;\n\tbackground-color: #51515F;\n\tpadding: 0px 12px!important;\n\theight: 24px!important;\n\tcursor: pointer;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/components/_Tooltip.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n\n.TooltipContent {\n  border-radius: $container-border-radius;\n  background-color: $color-text-primary;\n  padding: 10px 15px;\n  font-size: 12px;\n  margin-bottom: 10px;\n  bottom: 100px;\n  line-height: 1;\n  font-family: $font-medium;\n  z-index: 99999999 !important;\n  color: $color-text-contrast;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  user-select: none;\n  transition: opacity 0.3 ease-in-out !important;\n  will-change: transform, opacity;\n  animation-duration: 400ms;\n  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);\n  will-change: transform, opacity;\n}\n\n.hide-tooltip {\n  display: none !important;\n}\n\n.tooltip-tall {\n  margin-bottom: 20px;\n}\n\n.tooltip-small {\n  margin-bottom: 5px;\n}\n\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"top\"] {\n  animation-name: slideDownAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"right\"] {\n  animation-name: slideLeftAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"bottom\"] {\n  animation-name: slideUpAndFade;\n}\n.TooltipContent[data-state=\"delayed-open\"][data-side=\"left\"] {\n  animation-name: slideRightAndFade;\n}\n\n@keyframes slideUpAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideRightAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n@keyframes slideDownAndFade {\n  from {\n    opacity: 0;\n    transform: translateY(-2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes slideLeftAndFade {\n  from {\n    opacity: 0;\n    transform: translateX(2px);\n  }\n  to {\n    opacity: 1;\n    transform: translateX(0);\n  }\n}\n\n#screenity-ui [data-radix-popper-content-wrapper] {\n  z-index: $z-index-max !important;\n}\n\n.override {\n  display: none !important;\n  opacity: 0 !important;\n  visibility: hidden !important;\n}\n"
  },
  {
    "path": "src/pages/Content/toolbar/styles/layout/_DrawingToolbar.scss",
    "content": "@use '../../../styles/_variables' as *;\n\nbody {\n\tbackground-color: white!important;\n}\n.DrawingToolbar.show-toolbar {\n\topacity: 1!important;\n\tpointer-events: all!important;\n\ttransform: scale(1) translate(calc(-50% + 16px), 0px)!important;\n}\n.ToolbarBottom .DrawingToolbar {\n\ttransform-origin: 0 -100%!important;\n}\n\n.DrawingToolbar {\n\topacity: 0;\n\tpointer-events: none;\n\talign-items: center;\n\tdisplay: flex;\n  min-width: max-content;\n\tpadding-left: 10px;\n\tpadding-right: 10px;\n  border-radius: 6px;\n  box-shadow: 0 2px 10px var(--blackA7);\n\tposition: absolute;\n\theight: 44px;\n\tleft: 0px;\n\ttransform: translate(calc(-50% + 16px));\n\ttransform-origin: 0 100%;\n\tborder-radius: 15px;\n\tz-index: 99999999;\n\ttransition: transform 0.25s cubic-bezier(.61,.11,.08,.96), opacity 0.25s cubic-bezier(.61,.11,.08,.96);\n\ttransform: scale(0.5) translate(calc(-50% + 16px), 10px);\n\n\t&::after {\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tfilter: blur(10px);\n\t\topacity: .15;\n\t\tbackground-color: #000;\n\t\tposition: absolute;\n\t\tleft: 0px;\n\t\ttop: 0px;\n\t\tz-index: -999999999999999!important;\n\t}\n\t&::before {\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: rgba(242, 241, 242, 0.85);\n\t\tbackdrop-filter: blur(5px);\n\t\tbackground-clip: content-box;\n\t\t-webkit-mask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n\t\tmask-image: radial-gradient(circle at 50% 59px, transparent 20px, #000 20px);\n\t\tbackground-position: center bottom 50px;\n\t\tborder-radius: 15px;\n\t\tposition: absolute;\n\t\ttop: 0px;\n\t\tleft: 0px;\n\t\tz-index: -2;\n\t}\n\t.ToolbarSeparator {\n\t\tbackground-color: #dddcdc;\n\t}\n}\n\n.ToolbarTop .DrawingToolbar {\n\tbottom: 49px!important;\n}\n.ToolbarBottom .DrawingToolbar {\n\ttop: 48px!important;\n\n\t&::before {\n\t\t-webkit-mask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px)!important;\n\t\tmask-image: radial-gradient(circle at 50% -14px, transparent 20px, #000 20px);\n\t\tbackground-position: center bottom 50px!important;\n\t}\n}\n\n.ColorPicker {\n\twidth: 14px;\n\theight: 14px;\n\tbackground: #ED6C3A;\n\tborder: 1.5px solid rgba(0, 0, 0, 0.2);\n\tborder-radius: 50%;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/layout/_ShapeToolbar.scss",
    "content": "@use '../../../styles/_variables' as *;\n\n.shapeToolbar {\n\tposition: absolute;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n  box-shadow: 0 2px 10px var(--blackA7);\n\tleft: 165px;\n\tpadding: $spacing-02;\n\topacity: 0;\n\tbottom: 45px;\n\ttransform: translateY(calc(-50% + 16px));\n\ttransform-origin: 0 100%;\n\tborder-radius: 15px;\n\tz-index: 99999999;\n\ttransition: transform 0.25s cubic-bezier(.61,.11,.08,.96), opacity 0.25s cubic-bezier(.61,.11,.08,.96);\n\ttransform: scale(0.5) translatY(calc(-50% + 16px), 10px);\n\tpointer-events: none;\n\n\t&::after {\n\t\tcontent: '';\n\t\tdisplay: block;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tbackground: #FFF;\n\t\tz-index: -9999999;\n\t\tposition: absolute;\n\t\tleft: 0px;\n\t\ttop: 0px;\n\t\tborder-radius: 15px;\n\t\tfilter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n\t\ttransition: filter .2s ease-in-out;\n\t}\n}\n.shapeToolbar.show-toolbar {\n\topacity: 1!important;\n\tpointer-events: all!important;\n\ttransform: scale(1) translateY(calc(-50% + 16px), 0px)!important;\n}"
  },
  {
    "path": "src/pages/Content/toolbar/styles/layout/_Toolbar.scss",
    "content": "@use \"../../../styles/_variables\" as *;\n@use \"sass:color\";\n\n/* reset */\na,\nbutton {\n  all: unset;\n}\n\niframe {\n  width: 100%;\n  height: 100%;\n  position: fixed;\n  overflow: scroll;\n  z-index: -9999;\n  top: 0px;\n  left: 0px;\n  border: 0px;\n  pointer-events: all !important;\n}\n.container {\n  pointer-events: none !important;\n}\n.visually-hidden-toolbar {\n  opacity: 0 !important;\n  visibility: hidden !important;\n  pointer-events: none !important;\n  transition: opacity 0.3s ease, visibility 0s linear 0.3s;\n}\n.toolbar-controls {\n  opacity: 0;\n  cursor: pointer;\n  position: absolute;\n  top: -10px;\n  right: -10px;\n  box-sizing: border-box;\n  border-radius: $container-border-radius;\n  border: 1px solid $color-border;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  z-index: 99999999999999;\n  background: rgba(240, 238, 238, 1);\n  backdrop-filter: blur(10px);\n  padding-left: 8px;\n  padding-right: 8px;\n  padding-top: 6px;\n  padding-bottom: 6px;\n  transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n  pointer-events: none;\n\n  &.open {\n    opacity: 1 !important;\n    pointer-events: auto;\n  }\n\n  .popup-control {\n    svg {\n      color: $color-icon;\n      margin-bottom: -2px;\n    }\n  }\n}\n.ToolbarBounds {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  box-sizing: border-box;\n  width: 100%;\n  height: 100%;\n  border: 10px solid $color-primary;\n  pointer-events: none;\n  transform: scale(1.2);\n  opacity: 0;\n  transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96),\n    opacity 0.25s ease-in-out;\n}\n.ToolbarBounds.ToolbarShake {\n  transform: scale(1);\n  opacity: 0.4;\n}\n\n.react-draggable {\n  pointer-events: all;\n}\n.ToolbarShake .react-draggable {\n  width: 100%;\n  height: 100%;\n}\n.ToolbarElastic {\n  transition: all 0.25s cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n.ToolbarShake .ToolbarRoot {\n  animation: subtleshake 0.9s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;\n  animation-iteration-count: infinite !important;\n  background-color: white !important;\n}\n.ToolbarDragging .ToolbarRoot {\n  transform: scale(1.02);\n\n  &::after {\n    filter: drop-shadow(0px 20px 50px rgba(0, 0, 0, 0.5));\n  }\n}\n\n// Shake animation for the toolbar\n@keyframes shake {\n  0% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  20% {\n    transform: translate(-3px, 0px) rotate(1deg);\n  }\n  30% {\n    transform: translate(3px, 2px) rotate(0deg);\n  }\n  40% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  50% {\n    transform: translate(-1px, 2px) rotate(-1deg);\n  }\n  60% {\n    transform: translate(-3px, 1px) rotate(0deg);\n  }\n  70% {\n    transform: translate(3px, 1px) rotate(-1deg);\n  }\n  80% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  90% {\n    transform: translate(1px, 2px) rotate(0deg);\n  }\n  100% {\n    transform: translate(1px, -2px) rotate(-1deg);\n  }\n}\n\n// Subtle shake animation, smooth\n@keyframes subtleshake {\n  0% {\n    transform: translate(0px, 0px) rotate(0deg);\n  }\n  10% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n  20% {\n    transform: translate(-1px, -1px) rotate(-1deg);\n  }\n  30% {\n    transform: translate(1px, 0px) rotate(0deg);\n  }\n  40% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  50% {\n    transform: translate(1px, -1px) rotate(1deg);\n  }\n  60% {\n    transform: translate(-1px, 1px) rotate(-1deg);\n  }\n  70% {\n    transform: translate(-1px, -1px) rotate(1deg);\n  }\n  80% {\n    transform: translate(1px, 1px) rotate(0deg);\n  }\n  90% {\n    transform: translate(0px, -1px) rotate(-1deg);\n  }\n  100% {\n    transform: translate(-1px, 1px) rotate(1deg);\n  }\n}\n\n.ToolbarTransparent {\n  opacity: 0;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n.ToolbarHoverZone {\n  display: inline-block;\n  position: relative;\n\n  // optional: give it a bit of invisible hover padding\n  padding: 4px;\n\n  // if you want to *visually* match the toolbar area only:\n  width: fit-content;\n  height: fit-content;\n}\n\n.ToolbarRoot {\n  display: flex;\n  align-items: center;\n  padding-left: 10px;\n  transition: opacity 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96),\n    transform 0.2s cubic-bezier(0.61, 0.11, 0.08, 0.96);\n  min-width: max-content;\n  background-color: white;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  padding-right: 10px;\n  height: 48px;\n  position: absolute;\n  bottom: 20px;\n  left: 20px;\n  border-radius: $container-border-radius;\n\n  &::after {\n    content: \"\";\n    display: block;\n    width: 100%;\n    height: 100%;\n    background: #fff;\n    z-index: -9999999;\n    position: absolute;\n    left: 0px;\n    top: 0px;\n    border-radius: $container-border-radius;\n    filter: drop-shadow(0px 4px 50px rgba(0, 0, 0, 0.3));\n    transition: filter 0.2s ease-in-out;\n  }\n}\n\n.ForceTransparent {\n  opacity: 0 !important;\n}\n.ToolbarRecordingControls {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #f6f7fb;\n  border-radius: $container-border-radius;\n  font-family: $font-medium;\n  height: calc(100% - 12px);\n  padding-left: 2px;\n  padding-right: 2px;\n}\n.ToolbarRecordingTime {\n  margin-right: 4px;\n  width: 42px;\n  color: $color-text-primary;\n  font-size: 13px;\n  line-height: 1;\n  user-select: none;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n}\n.TimerWarning {\n  color: #ff4c4c; /* or add a pulsing effect */\n  font-weight: bold;\n  animation: pulse 1s infinite alternate;\n}\n@keyframes pulse {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0.6;\n  }\n}\n.ToolbarToggleGroup {\n  display: flex;\n  align-items: center;\n}\n.ToolbarToggleWrap {\n  flex: 1 1 auto;\n  align-items: center;\n  justify-content: flex-start;\n  position: relative;\n  flex: 0 0 auto;\n  width: 32px;\n  height: 32px;\n  display: inline-flex;\n  line-height: 1;\n  align-items: center;\n  justify-content: center;\n}\n.ToolbarToggleItem,\n.ToolbarModeItem,\n.ToolbarModeItemSingle,\n.ToolbarLink,\n.ToolbarButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #000;\n  height: 32px;\n  width: 32px;\n  text-align: center;\n  font-size: 13px;\n  line-height: 1;\n  border-radius: 50%;\n  transition: background-color 0.25s ease-in-out;\n  background-color: rgba(124, 139, 165, 0);\n\n  svg {\n    color: $color-icon;\n  }\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed !important;\n    background: none !important;\n  }\n\n  &.resume {\n    svg {\n      color: #f7387d !important;\n    }\n  }\n}\n.ToolbarToggleItem:hover,\n.ToolbarModeItem:hover,\n.ToolbarModeItemSingle:hover,\n.ToolbarLink:hover,\n.ToolbarButton:hover {\n  cursor: pointer;\n  background-color: rgba(124, 139, 165, 0.1) !important;\n}\n.ToolbarToggleItem:focus-visible,\n.ToolbarModeItemSingle:focus-visible,\n.ToolbarModeItem:focus-visible,\n.ToolbarLink:focus-visible,\n.ToolbarButton:focus-visible {\n  position: relative;\n  box-shadow: $focus-border;\n}\n\n.ToolbarModeItemSingle {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n\n  &:first-child {\n    margin-left: 0;\n  }\n  &[data-state=\"on\"] {\n    background: rgba(120, 192, 114, 0.1);\n\n    svg {\n      color: #78c072;\n    }\n\n    &::before {\n      transform: translateY(0px) scale(1) !important;\n      opacity: 1 !important;\n    }\n  }\n}\n\n.ToolbarModeItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n\n  &:first-child {\n    margin-left: 0;\n  }\n  &::before {\n    content: \"\";\n    display: block;\n    width: 100%;\n    height: 50%;\n    border-radius: 80px 80px 0% 0%;\n    box-sizing: border-box;\n    position: absolute;\n    top: -16px;\n    left: 0;\n    z-index: -999999;\n    transition: transform 0.25s cubic-bezier(0.61, 0.11, 0.08, 0.96),\n      opacity 0.25s ease-in-out;\n    transform: translateY(5px) scale(0) !important;\n    border-right: 3px solid white;\n    border-top: 9px solid white;\n    border-left: 3px solid white;\n    background-color: transparent;\n    opacity: 0;\n  }\n  &[data-state=\"on\"] {\n    background: rgba(56, 126, 247, 0.1);\n\n    svg {\n      color: $color-primary;\n    }\n\n    &::before {\n      transform: translateY(0px) scale(1) !important;\n      opacity: 1 !important;\n    }\n  }\n}\n\n.ToolbarBottom .ToolbarModeItem {\n  &::before {\n    transform: translateY(-5px) scale(0.5) !important;\n    bottom: -16px;\n    top: unset !important;\n    border-bottom: 9px solid white !important;\n    border-radius: 0% 0% 80px 80px !important;\n    border-top: none !important;\n  }\n  &[data-state=\"on\"] {\n    &::before {\n      transform: translateY(0px) scale(1) !important;\n    }\n  }\n}\n\n.ToolbarToggleItem {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 99999;\n  position: relative;\n\n  &:first-child {\n    margin-left: 0;\n  }\n  &[data-state=\"on\"] {\n    background: $gradient-primary;\n    color: #fff;\n\n    svg {\n      color: #fff;\n    }\n  }\n}\n\n.ToolbarSeparator {\n  width: 1px;\n  height: 19px;\n  background-color: $color-border;\n  margin: 0 8px;\n}\n\n.ToolbarLink {\n  background-color: transparent;\n  color: var(--mauve11);\n  display: inline-flex;\n  justify-content: center;\n  align-items: center;\n}\n.ToolbarLink:hover {\n  background-color: transparent;\n  cursor: pointer;\n}\n\n.ToolbarPaused {\n  position: fixed;\n  top: 0px;\n  left: 0px;\n  width: 100%;\n  height: 100%;\n  box-sizing: border-box;\n  border: 10px solid #f7387d;\n  pointer-events: none;\n  opacity: 0.5;\n\n  &.hidden {\n    display: none;\n  }\n}\n\n.OnboardingArrow {\n  position: absolute;\n  z-index: $z-index-max;\n  width: max-content;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 16px;\n  left: -69px;\n  bottom: 23px;\n  transform: rotate(12deg);\n  pointer-events: none !important;\n}\n\n// Hide legacy \"click here to draw\" hint while Driver.js onboarding is active.\nbody.driver-active .OnboardingArrow {\n  display: none !important;\n}\n\n.OnboardingText {\n  font-size: 32px;\n  color: $color-icon;\n  font-family: \"Gloria-Hallelujah\", sans-serif !important;\n}\n.ArrowShape {\n  margin-left: -15px;\n}\n\n// Onboarding (driver.js)\n.driver-popover.ScreenityOnboardingPopover,\n.ScreenityOnboardingPopover,\n.onboarding-popover {\n  border-radius: $container-border-radius !important;\n  background: $color-background !important;\n  color: $color-text-primary !important;\n  font-family: $font-regular !important;\n  box-shadow: $container-soft-shadow !important;\n  padding: 20px !important;\n  max-width: 340px !important;\n  font-size: $font-size-normal !important;\n  z-index: $z-index-max !important;\n\n  .driver-popover-title {\n    font-size: 1rem !important;\n    font-family: $font-bold !important;\n    margin-bottom: 12px !important;\n    color: $color-text-primary !important;\n  }\n\n  .driver-popover-description {\n    font-size: $font-size-normal !important;\n    font-family: $font-medium !important;\n    color: $color-text-secondary !important;\n    line-height: 1.5 !important;\n    margin-bottom: 18px !important;\n\n    a {\n      color: $color-primary !important;\n      font-family: $font-bold !important;\n      text-decoration: none !important;\n      cursor: pointer !important;\n    }\n  }\n\n  .driver-popover-close-btn {\n    display: none !important;\n    top: 14px;\n    right: 12px;\n    color: $color-text-secondary !important;\n    font-size: 1.5rem !important;\n  }\n\n  .driver-popover-arrow {\n    width: 0px;\n    height: 0px;\n    // border: 4px solid #fff;\n    background: none !important;\n    box-shadow: none !important;\n    box-sizing: border-box;\n  }\n\n  .driver-popover-arrow-side-top {\n    border-left: 8px solid transparent !important;\n    border-right: 8px solid transparent !important;\n    border-top: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-bottom {\n    border-left: 8px solid transparent !important;\n    border-right: 8px solid transparent !important;\n    border-bottom: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-left {\n    border-top: 8px solid transparent !important;\n    border-bottom: 8px solid transparent !important;\n    border-left: 8px solid $color-background !important;\n  }\n\n  .driver-popover-arrow-side-right {\n    border-top: 8px solid transparent !important;\n    border-bottom: 8px solid transparent !important;\n    border-right: 8px solid $color-background !important;\n  }\n  .driver-popover-arrow-align-start {\n    top: 35px !important;\n  }\n  .driver-popover-arrow-align-end {\n    bottom: 35px !important;\n  }\n  .driver-popover-arrow-align-left {\n    left: 35px !important;\n  }\n  .driver-popover-arrow-align-right {\n    right: 35px !important;\n  }\n  .driver-popover-progress-text {\n    font-size: $font-size-detail !important;\n    font-family: $font-medium !important;\n    color: $color-text-secondary !important;\n    opacity: 0.7 !important;\n    margin-top: 2px !important;\n  }\n  .driver-popover-footer {\n    display: flex !important;\n    justify-content: flex-end;\n    gap: 8px;\n    margin-top: 16px;\n\n    .driver-popover-navigation-btns {\n      gap: 6px !important;\n\n      .driver-popover-next-btn,\n      .driver-popover-prev-btn,\n      .driver-popover-close-btn {\n        background-color: $color-primary !important;\n        color: white !important;\n        border: none !important;\n        padding: 10px 14px !important;\n        border-radius: 30px !important;\n        font-family: $font-medium !important;\n        font-weight: 500 !important;\n        cursor: pointer !important;\n        font-size: 0.875rem !important;\n        text-shadow: none !important;\n      }\n      .driver-popover-next-btn {\n        &:hover {\n          background: color.adjust($color-primary, $lightness: -5%) !important;\n        }\n      }\n\n      .driver-popover-prev-btn {\n        background-color: transparent !important;\n        color: $color-text-primary !important;\n        border: 1px solid $color-border !important;\n\n        &:hover {\n          background: $color-light-grey !important;\n        }\n      }\n\n      .driver-popover-close-btn {\n        background-color: transparent !important;\n        color: $color-text-secondary !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/pages/Content/utils/BlurTool.jsx",
    "content": "import React, {\n  useLayoutEffect,\n  useState,\n  useRef,\n  useContext,\n  useEffect,\n} from \"react\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst BlurTool = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const hoveredElementRef = useRef(null);\n  const blurModeRef = useRef(null);\n  const [showOutline, setShowOutline] = useState(false);\n  const [outlineRect, setOutlineRect] = useState(null);\n\n  useEffect(() => {\n    blurModeRef.current = contentState.blurMode;\n  }, [contentState.blurMode]);\n\n  useEffect(() => {\n    if (!contentState.showExtension) {\n      setShowOutline(false);\n      // Remove blur from all elements\n      const elements = document.querySelectorAll(\".screenity-blur\");\n      elements.forEach((element) => {\n        element.classList.remove(\"screenity-blur\");\n      });\n    }\n  }, [contentState.showExtension]);\n\n  useLayoutEffect(() => {\n    const updateOutline = (target) => {\n      if (!target) return;\n      hoveredElementRef.current = target;\n      const rect = target.getBoundingClientRect();\n      setOutlineRect({\n        top: rect.top + window.scrollY,\n        left: rect.left + window.scrollX,\n        width: target.offsetWidth,\n        height: target.offsetHeight,\n      });\n      setShowOutline(true);\n    };\n\n    const handleMouseMove = (event) => {\n      if (!blurModeRef.current) {\n        setShowOutline(false);\n        setOutlineRect(null);\n        return;\n      }\n      const target = event.target;\n      if (\n        !target.classList.contains(\"screenity-outline\") &&\n        !target.closest(\"#screenity-ui #screenity-ui *\")\n      ) {\n        updateOutline(target);\n        document.body.style.cursor = \"pointer\";\n      } else {\n        document.body.style.cursor = \"auto\";\n        setShowOutline(false);\n        setOutlineRect(null);\n      }\n    };\n\n    const handleMouseOut = () => {\n      setShowOutline(false);\n      setOutlineRect(null);\n    };\n\n    const handleMouseDown = (event) => {\n      if (!blurModeRef.current) {\n        setShowOutline(false);\n        return;\n      }\n\n      const target = event.target;\n      if (target.closest(\"#screenity-ui, #screenity-ui *\")) {\n        return;\n      }\n\n      event.preventDefault();\n      event.stopPropagation();\n    };\n\n    const handleElementClick = (event) => {\n      if (!blurModeRef.current) {\n        setShowOutline(false);\n        return;\n      }\n\n      const target = event.target;\n      if (target.closest(\"#screenity-ui, #screenity-ui *\")) {\n        return;\n      }\n\n      event.preventDefault();\n      event.stopPropagation();\n      target.classList.toggle(\"screenity-blur\");\n    };\n\n    const handleMouseUp = (event) => {\n      if (!blurModeRef.current) {\n        setShowOutline(false);\n        setOutlineRect(null);\n        return;\n      }\n\n      const target = event.target;\n      if (target.closest(\"#screenity-ui, #screenity-ui *\")) {\n        return;\n      }\n      event.preventDefault();\n      event.stopPropagation();\n    };\n\n    const handleScroll = () => {\n      if (!blurModeRef.current || !hoveredElementRef.current) return;\n      updateOutline(hoveredElementRef.current);\n    };\n\n    document.body.addEventListener(\"mousemove\", handleMouseMove, true);\n    document.body.addEventListener(\"mousedown\", handleMouseDown, true);\n    document.body.addEventListener(\"mouseout\", handleMouseOut, true);\n    document.body.addEventListener(\"mouseup\", handleMouseUp, true);\n    document.body.addEventListener(\"click\", handleElementClick, true);\n    document.addEventListener(\"scroll\", handleScroll, true);\n\n    return () => {\n      document.body.removeEventListener(\"mousemove\", handleMouseMove, true);\n      document.body.removeEventListener(\"mousedown\", handleMouseDown, true);\n      document.body.removeEventListener(\"mouseout\", handleMouseOut, true);\n      document.body.removeEventListener(\"mouseup\", handleMouseUp, true);\n      document.body.removeEventListener(\"click\", handleElementClick, true);\n      document.removeEventListener(\"scroll\", handleScroll, true);\n    };\n  }, []);\n\n  return (\n    <div>\n      {showOutline && outlineRect && (\n        <div\n          className=\"screenity-outline\"\n          style={{\n            top: outlineRect.top + \"px\",\n            left: outlineRect.left + \"px\",\n            width: outlineRect.width + \"px\",\n            height: outlineRect.height + \"px\",\n          }}\n        ></div>\n      )}\n    </div>\n  );\n};\n\nexport default BlurTool;\n"
  },
  {
    "path": "src/pages/Content/utils/CursorModes.jsx",
    "content": "import React, { useState, useEffect, useContext, useRef } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst CursorModes = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const effectsRef = useRef(new Set());\n\n  useEffect(() => {\n    const nextEffects = Array.isArray(contentState.cursorEffects)\n      ? contentState.cursorEffects\n      : [];\n    effectsRef.current = new Set(nextEffects);\n  }, [contentState.cursorEffects]);\n\n  const mouseDownHandler = (e) => {\n    if (effectsRef.current.has(\"target\")) {\n      const target = document.querySelector(\".cursor-click-target\");\n      if (!target) return;\n      target.style.transform =\n        \"translate(-50%, -50%) scale(1)\";\n      target.style.opacity = \"1\";\n    }\n  };\n\n  const mouseUpHandler = (e) => {\n    if (effectsRef.current.has(\"target\")) {\n      const target = document.querySelector(\".cursor-click-target\");\n      if (!target) return;\n      target.style.transform =\n        \"translate(-50%, -50%) scale(0)\";\n      target.style.opacity = \"0\";\n\n      window.setTimeout(() => {\n        const targetReset = document.querySelector(\".cursor-click-target\");\n        if (!targetReset) return;\n        targetReset.style.transform =\n          \"translate(-50%, -50%) scale(1)\";\n      }, 350);\n    }\n  };\n\n  const [lastMousePosition, setLastMousePosition] = useState({ x: 0, y: 0 });\n  const lastMouseRef = useRef(lastMousePosition);\n\n  useEffect(() => {\n    lastMouseRef.current = lastMousePosition;\n  }, [lastMousePosition]);\n\n  const updateCursorPosition = () => {\n    const scrollTop = window.scrollY;\n    const scrollLeft = window.scrollX;\n\n    if (effectsRef.current.has(\"target\")) {\n      const target = document.querySelector(\".cursor-click-target\");\n      if (target) {\n        target.style.top = lastMouseRef.current.y + scrollTop + \"px\";\n        target.style.left = lastMouseRef.current.x + scrollLeft + \"px\";\n      }\n    }\n\n    if (effectsRef.current.has(\"highlight\")) {\n      const highlight = document.querySelector(\".cursor-highlight\");\n      if (highlight) {\n        highlight.style.top = lastMouseRef.current.y + scrollTop + \"px\";\n        highlight.style.left = lastMouseRef.current.x + scrollLeft + \"px\";\n      }\n    }\n\n    if (effectsRef.current.has(\"spotlight\")) {\n      const spotlight = document.querySelector(\".spotlight\");\n      if (spotlight) {\n        spotlight.style.top = lastMouseRef.current.y + scrollTop + \"px\";\n        spotlight.style.left = lastMouseRef.current.x + scrollLeft + \"px\";\n      }\n    }\n  };\n\n  const mouseMoveHandler = (e) => {\n    const next = { x: e.clientX, y: e.clientY };\n    lastMouseRef.current = next;\n    setLastMousePosition(next);\n    updateCursorPosition();\n  };\n\n  const scrollHandler = () => {\n    updateCursorPosition();\n  };\n\n  // Show click target when user clicks anywhere for 1 second, animate scale up and fade out\n  useEffect(() => {\n    document.addEventListener(\"mousedown\", mouseDownHandler);\n    document.addEventListener(\"mousemove\", mouseMoveHandler);\n    document.addEventListener(\"mouseup\", mouseUpHandler);\n    document.addEventListener(\"scroll\", scrollHandler);\n\n    return () => {\n      document.removeEventListener(\"mousedown\", mouseDownHandler);\n      document.removeEventListener(\"mousemove\", mouseMoveHandler);\n      document.removeEventListener(\"mouseup\", mouseUpHandler);\n      document.removeEventListener(\"scroll\", scrollHandler);\n    };\n  }, []);\n\n  return (\n    <div>\n      <div\n        className=\"cursor-highlight\"\n        style={{\n          display: \"block\",\n          visibility:\n            contentState.cursorEffects?.includes(\"highlight\")\n              ? \"visible\"\n              : \"hidden\",\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          width: \"80px\",\n          height: \"80px\",\n          pointerEvents: \"none\",\n          zIndex: 99999999999,\n          background: \"yellow\",\n          opacity: \".5\",\n          transform: \"translate(-50%, -50%)\",\n          borderRadius: \"50%\",\n          animation: \"none\",\n        }}\n      ></div>\n      <div\n        className=\"cursor-click-target\"\n        style={{\n          display: \"block\",\n          visibility:\n            contentState.cursorEffects?.includes(\"target\")\n              ? \"visible\"\n              : \"hidden\",\n          position: \"absolute\",\n          top: 0,\n          opacity: 0,\n          left: 0,\n          width: \"40px\",\n          height: \"40px\",\n          transform: \"translate(-50%, -50%) scale(1)\",\n          pointerEvents: \"none\",\n          zIndex: 99999999999,\n          border: \"3px solid red\",\n          transform: \"none\",\n          borderRadius: \"50%\",\n          animation: \"none\",\n          transition:\n            \"opacity .5s cubic-bezier(.25,.8,.25,1), transform .35s cubic-bezier(.25,.8,.25,1)\",\n        }}\n      ></div>\n      <div\n        className=\"spotlight\"\n        style={{\n          position: \"absolute\",\n          display: contentState.cursorEffects?.includes(\"spotlight\")\n            ? \"block\"\n            : \"none\",\n          top: lastMousePosition.y + \"px\",\n          left: lastMousePosition.x + \"px\",\n          width: \"100px\",\n          height: \"100px\",\n          borderRadius: \"50%\",\n          boxShadow: \"0 0 0 9999px rgba(0, 0, 0, 0.5)\",\n          transform: \"translate(-50%, -50%)\",\n          pointerEvents: \"none\",\n          zIndex: 99999999999,\n        }}\n      ></div>\n      <style>\n        {`\n\t\t\t\t\t@keyframes scaleDown {\n\t\t\t\t\t\t\tfrom {\n\t\t\t\t\t\t\t\t\ttransform: translate(-50%, -50%) scale(1);\n\t\t\t\t\t\t\t\t\topacity: 1;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tto {\n\t\t\t\t\t\t\t\t\ttransform: translate(-50%, -50%) scale(0);\n\t\t\t\t\t\t\t\t\topacity: 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default CursorModes;\n"
  },
  {
    "path": "src/pages/Content/utils/ZoomContainer.jsx",
    "content": "import React, { useState, useEffect, useRef, useContext } from \"react\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst ZoomContainer = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [zoomLevel, setZoomLevel] = useState(1);\n  const scaleRef = useRef(1);\n  const translateXRef = useRef(0);\n  const translateYRef = useRef(0);\n  const cursorXRef = useRef(0);\n  const cursorYRef = useRef(0);\n  const isKeyDownRef = useRef(false);\n  const isKeyUpRef = useRef(false);\n  const zoomSelector = useRef(null);\n  const oldPosition = useRef(null);\n  const oldWidth = useRef(null);\n  const oldHeight = useRef(null);\n  const oldOverflow = useRef(null);\n  const oldTop = useRef(null);\n  const oldLeft = useRef(null);\n  const contentStateRef = useRef(contentState);\n  const observer = useRef(null);\n\n  useEffect(() => {\n    oldPosition.current = document.body.style.position;\n    oldWidth.current = document.body.style.width;\n    oldHeight.current = document.body.style.height;\n    oldOverflow.current = document.body.style.overflow;\n    oldTop.current = document.body.style.top;\n    oldLeft.current = document.body.style.left;\n  }, []);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  const handleKeyDown = (e) => {\n    // Alt / Option + Shift + Z\n    if (e.code === \"KeyE\" && e.altKey && e.shiftKey) {\n      //if (!contentStateRef.current.recording) return;\n      if (!contentStateRef.current.zoomEnabled) return;\n      if (isKeyDownRef.current) return;\n      isKeyDownRef.current = true;\n      zoomIn();\n    }\n  };\n\n  const handleKeyUp = (e) => {\n    if (e.code === \"KeyE\" || e.altKey || e.shiftKey) {\n      isKeyDownRef.current = false;\n      isKeyUpRef.current = true;\n      zoomOut();\n\n      setTimeout(() => {\n        isKeyUpRef.current = false;\n        setTimeout(() => {\n          enableScrolling();\n        }, 500);\n      }, 500);\n    }\n  };\n\n  const handleMouseMove = (e) => {\n    //if (!contentStateRef.current.recording) return;\n    if (!contentStateRef.current.zoomEnabled) return;\n\n    const { top, left } = document.documentElement.getBoundingClientRect();\n\n    cursorXRef.current = e.clientX - left;\n    cursorYRef.current = e.clientY - top;\n    applyTransform();\n  };\n\n  const zoomIn = () => {\n    scaleRef.current *= 1.5;\n    setZoomLevel(scaleRef.current);\n    applyTransformWithTransition();\n    preventScrolling();\n  };\n\n  const zoomOut = () => {\n    scaleRef.current = 1;\n    translateXRef.current = 0;\n    translateYRef.current = 0;\n    setZoomLevel(scaleRef.current);\n    //applyTransformWithTransition();\n    //enableScrolling();\n  };\n\n  const applyTransform = () => {\n    if (!zoomSelector.current) return;\n    //if (!contentStateRef.current.recording) return;\n    const { current: scale } = scaleRef;\n    const { current: translateX } = translateXRef;\n    const { current: translateY } = translateYRef;\n\n    const originX = cursorXRef.current - translateX;\n    const originY = cursorYRef.current - translateY;\n\n    zoomSelector.current.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;\n    zoomSelector.current.style.transformOrigin = `${originX}px ${originY}px`;\n\n    // I also need to apply the transform to the #canvas-wrapper, if it exists\n    const canvasWrapper = document.querySelector(\"#canvas-wrapper-screenity\");\n\n    // Substract scroll position\n    const fixedOriginX = originX - window.scrollX;\n    const fixedOriginY = originY - window.scrollY;\n    if (canvasWrapper) {\n      canvasWrapper.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;\n      canvasWrapper.style.transformOrigin = `${fixedOriginX}px ${fixedOriginY}px`;\n    }\n\n    // Also to #mockup-wrapper\n    //const mockupWrapper = document.querySelector(\"#mockup-wrapper\");\n    //if (mockupWrapper) {\n    //  mockupWrapper.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;\n    //  mockupWrapper.style.transformOrigin = `${originX}px ${originY}px`;\n    //}\n  };\n\n  const applyTransformWithTransition = () => {\n    if (!zoomSelector.current) return;\n\n    zoomSelector.current.style.transition = \"transform 0.5s\";\n    if (document.querySelector(\"#canvas-wrapper-screenity\")) {\n      document.querySelector(\"#canvas-wrapper-screenity\").style.transition =\n        \"transform 0.5s\";\n    }\n    //if (document.querySelector(\"#mockup-wrapper\")) {\n    //  document.querySelector(\"#mockup-wrapper\").style.transition =\n    //    \"transform 0.5s\";\n    //}\n    applyTransform();\n  };\n\n  const preventScrolling = () => {\n    /*\n    zoomSelector.current.style.position = \"fixed\";\n    zoomSelector.current.style.top = \"0\";\n    zoomSelector.current.style.left = \"0\";\n    zoomSelector.current.style.overflow = \"hidden\";\n    zoomSelector.current.style.width = \"100vw\";\n    zoomSelector.current.style.height = \"100vh\";\n\t\t*/\n  };\n\n  const enableScrolling = () => {\n    if (!zoomSelector.current) return;\n    zoomSelector.current.style.position = oldPosition.current;\n    zoomSelector.current.style.top = oldTop.current;\n    zoomSelector.current.style.left = oldLeft.current;\n    zoomSelector.current.style.overflow = oldOverflow.current;\n    zoomSelector.current.style.width = oldWidth.current;\n    zoomSelector.current.style.height = oldHeight.current;\n  };\n\n  useEffect(() => {\n    window.addEventListener(\"keydown\", handleKeyDown);\n    window.addEventListener(\"keyup\", handleKeyUp);\n    window.addEventListener(\"mousemove\", handleMouseMove);\n\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown);\n      window.removeEventListener(\"keyup\", handleKeyUp);\n      window.removeEventListener(\"mousemove\", handleMouseMove);\n    };\n  }, [contentState.zoomEnabled, contentState.showExtension]);\n\n  useEffect(() => {\n    if (!contentState.zoomEnabled) return;\n    if (!contentState.showPopup) return;\n\n    setTimeout(() => {\n      //if (!contentState.recording) return;\n      if (document.querySelector(\"#screenity-zoom-wrap\")) return;\n      const div = document.createElement(\"div\");\n      div.id = \"screenity-zoom-wrap\";\n      div.style.width = \"100vw\";\n      div.style.height = \"100vh\";\n\n      // Move the body's children into this wrapper\n      while (\n        document.body.firstChild &&\n        document.body.firstChild.id !== \"screenity-ui\"\n      ) {\n        if (document.body.firstChild.id !== \"screenity-ui\") {\n          div.appendChild(document.body.firstChild);\n        }\n      }\n\n      // Append the wrapper to the body\n      document.body.prepend(div);\n\n      document.body.appendChild(document.getElementById(\"screenity-ui\"));\n      zoomSelector.current = document.querySelector(\"#screenity-zoom-wrap\");\n\n      observer.current = new MutationObserver((mutations) => {\n        if (!contentState.showExtension) {\n          mutations.forEach((mutation) => {\n            if (mutation.addedNodes.length > 0) {\n              const screenityUi = document.querySelector(\"#screenity-ui\");\n              if (screenityUi) {\n                // Disconnect the observer\n                observer.current.disconnect();\n              }\n            }\n          });\n        }\n      });\n\n      observer.current.observe(document.body, {\n        childList: true,\n        subtree: true,\n      });\n    }, 500);\n\n    return () => {\n      setTimeout(() => {\n        if (observer.current && typeof observer.current === \"object\") {\n          observer.current.disconnect();\n        }\n        const zoomWrap = document.querySelector(\"#screenity-zoom-wrap\");\n        if (zoomWrap) {\n          while (zoomWrap.firstChild) {\n            document.body.prepend(zoomWrap.firstChild);\n          }\n\n          if (document.body.contains(zoomWrap)) {\n            document.body.removeChild(zoomWrap);\n          }\n        }\n        // reset\n        scaleRef.current = 1;\n        translateXRef.current = 0;\n        translateYRef.current = 0;\n        setZoomLevel(scaleRef.current);\n      }, 500);\n    };\n  }, [contentState.zoomEnabled, contentState.showExtension]);\n\n  useEffect(() => {\n    setTimeout(() => {\n      if (!contentState.zoomEnabled || !contentState.showExtension) {\n        const zoomWrap = document.querySelector(\"#screenity-zoom-wrap\");\n        if (zoomWrap) {\n          while (zoomWrap.firstChild) {\n            document.body.prepend(zoomWrap.firstChild);\n          }\n\n          if (document.body.contains(zoomWrap)) {\n            document.body.removeChild(zoomWrap);\n          }\n        }\n        // reset\n        scaleRef.current = 1;\n        translateXRef.current = 0;\n        translateYRef.current = 0;\n        setZoomLevel(scaleRef.current);\n      }\n    }, 500);\n  }, [contentState.zoomEnabled, contentState.showExtension]);\n\n  useEffect(() => {\n    if (!zoomSelector.current) return;\n    //if (!contentStateRef.current.recording) return;\n    if (!contentStateRef.current.zoomEnabled) return;\n    if (isKeyDownRef.current || isKeyUpRef.current) {\n      //preventScrolling();\n      applyTransform();\n    }\n  }, [zoomLevel]);\n\n  return null;\n};\n\nexport default ZoomContainer;\n"
  },
  {
    "path": "src/pages/Content/warning/Warning.jsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  useContext,\n  useCallback,\n  useRef,\n} from \"react\";\n\nimport {\n  AudioIcon,\n  CameraCloseIcon,\n  NotSupportedIcon,\n  CameraIcon,\n} from \"../toolbar/components/SVG\";\n\nimport * as ToastEl from \"@radix-ui/react-toast\";\n\n// Context\nimport { contentStateContext } from \"../context/ContentState\";\n\nconst Warning = () => {\n  const [contentState, setContentState] = useContext(contentStateContext);\n  const [open, setOpen] = useState(false);\n  const [title, setTitle] = useState(\"Record computer audio\");\n  const [description, setDescription] = useState(\"\");\n  const [icon, setIcon] = useState(\"AudioIcon\");\n  const [duration, setDuration] = useState(10000);\n\n  const openWarning = useCallback((title, description, icon, duration) => {\n    setTitle(title);\n    setDescription(description);\n    setIcon(icon);\n    setDuration(duration);\n    setOpen(true);\n  }, []);\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      openWarning: openWarning,\n    }));\n\n    return () => {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        openWarning: null,\n      }));\n    };\n  }, []);\n\n  useEffect(() => {\n    if (icon === \"AudioIcon\") {\n      if (contentState.recordingType === \"region\") {\n        setOpen(false);\n      }\n    }\n  }, [contentState.recordingType]);\n\n  useEffect(() => {\n    if (contentState.recording) {\n      setOpen(false);\n    }\n  }, [contentState.recording]);\n\n  return (\n    <ToastEl.Provider swipeDirection=\"up\" duration={duration}>\n      <ToastEl.Root\n        className=\"warning-root\"\n        open={open}\n        onOpenChange={setOpen}\n        onSwipeEnd={() => {\n          setOpen(false);\n        }}\n      >\n        <div className=\"warning-icon\">\n          {icon === \"AudioIcon\" && <AudioIcon />}\n          {icon === \"NotSupportedIcon\" && <NotSupportedIcon />}\n          {icon === \"CameraIcon\" && <CameraIcon />}\n        </div>\n        <div className=\"warning-content\">\n          <ToastEl.Title className=\"warning-title\">{title}</ToastEl.Title>\n          <ToastEl.Description className=\"warning-description\">\n            {description}\n          </ToastEl.Description>\n        </div>\n        <ToastEl.Close\n          className=\"warning-close\"\n          onClick={() => {\n            setOpen(false);\n          }}\n        >\n          <CameraCloseIcon />\n        </ToastEl.Close>\n      </ToastEl.Root>\n      <ToastEl.Viewport className=\"WarningViewport\" />\n    </ToastEl.Provider>\n  );\n};\n\nexport default Warning;\n"
  },
  {
    "path": "src/pages/Content/warning/styles/_Warning.scss",
    "content": "@use \"../../styles/_variables\" as *;\n\n.WarningViewport {\n  --viewport-padding: 25px;\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  margin: auto !important;\n  display: flex;\n  flex-direction: column;\n  padding: var(--viewport-padding);\n  gap: 14px;\n  max-width: 100vw;\n  width: fit-content;\n  list-style: none;\n  z-index: 2147483647;\n  outline: none;\n  pointer-events: all !important;\n}\n\n.warning-root {\n  background-color: $color-text-primary;\n  color: $color-text-contrast;\n  border-radius: $container-border-radius;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  padding: 14px 20px;\n  display: flex;\n  flex-direction: row;\n  justify-content: center;\n  gap: 8px;\n  font-size: 15px;\n  line-height: 1.5;\n  max-width: 350px;\n  overflow: hidden;\n  align-items: center;\n  text-align: left;\n  align-items: flex-start;\n}\n.warning-content {\n  display: flex;\n  flex-direction: column;\n  justify-content: left;\n  align-items: flex-start;\n  gap: 8px;\n  width: 100%;\n}\n.warning-root[data-state=\"open\"] {\n  animation: slideIn2 150ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n.warning-root[data-state=\"closed\"] {\n  animation: hide 100ms ease-in;\n}\n.warning-root[data-swipe=\"move\"] {\n  transform: translateY(var(--radix-toast-swipe-move-y));\n}\n.warning-root[data-swipe=\"cancel\"] {\n  transform: translateY(0);\n  transition: transform 200ms ease-out;\n}\n.warning-root[data-swipe=\"end\"] {\n  animation: swipeOut2 100ms ease-out;\n}\n\n@keyframes hide {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0 !important;\n  }\n}\n\n@keyframes slideIn2 {\n  from {\n    transform: translateY(calc(-100% - var(--viewport-padding)));\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n\n@keyframes swipeOut2 {\n  from {\n    transform: translateY(var(--radix-toast-swipe-end-y));\n  }\n  to {\n    transform: translateY(calc(-100% - var(--viewport-padding)));\n  }\n}\n\n.warning-title {\n  color: $color-text-contrast;\n  font-family: $font-medium;\n  line-height: 1.4;\n}\n\n.warning-description {\n  color: $color-text-contrast;\n  opacity: 0.8;\n  font-family: $font-medium;\n  line-height: 1.5;\n}\n\n.ToastAction {\n  color: $color-text-contrast;\n  font-family: $font-medium;\n  text-align: right;\n  background-color: #51515f;\n  padding: 0px 12px !important;\n  height: 24px !important;\n  cursor: pointer;\n}\n.warning-close {\n  z-index: 999999;\n}\n.warning-close:hover {\n  cursor: pointer;\n}\n"
  },
  {
    "path": "src/pages/Download/Download.jsx",
    "content": "import React, { useState, useCallback, useEffect } from \"react\";\n\nimport localforage from \"localforage\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\n// Get chunks store\nconst chunksStore = localforage.createInstance({ name: \"chunks\" });\nconst cameraChunksStore = localforage.createInstance({ name: \"cameraChunks\" });\nconst audioChunksStore = localforage.createInstance({ name: \"audioChunks\" });\n\nconst Download = () => {\n  const base64ToUint8Array = (base64) => {\n    const dataUrlRegex = /^data:(.*?);base64,/;\n    const matches = base64.match(dataUrlRegex);\n    if (matches !== null) {\n      // Base64 is a data URL\n      const mimeType = matches[1];\n      const binaryString = atob(base64.slice(matches[0].length));\n      const bytes = new Uint8Array(binaryString.length);\n      for (let i = 0; i < binaryString.length; i++) {\n        bytes[i] = binaryString.charCodeAt(i);\n      }\n      return new Blob([bytes], { type: mimeType });\n    } else {\n      // Base64 is a regular string\n      const binaryString = atob(base64);\n      const bytes = new Uint8Array(binaryString.length);\n      for (let i = 0; i < binaryString.length; i++) {\n        bytes[i] = binaryString.charCodeAt(i);\n      }\n      return new Blob([bytes], { type: \"video/webm\" });\n    }\n  };\n\n  const handleMessage = useCallback((message, sender, sendResponse) => {\n    if (message.type === \"download-video\") {\n      const base64 = message.base64;\n      const blob = base64ToUint8Array(base64);\n      const title = message.title.replace(/[\\/\\\\:?~<>|*\"]/g, \"_\");\n      const url = URL.createObjectURL(blob);\n\n      chrome.downloads\n        .download({\n          url: url,\n          filename: title,\n          saveAs: true,\n        })\n        .then(() => {\n          URL.revokeObjectURL(url);\n          // Close this tab\n          window.close();\n        });\n    } else if (message.type === \"recover-indexed-db\") {\n      // Rewrite in localforage\n      const chunkArray = [];\n      chunksStore\n        .iterate((value, key, iterationNumber) => {\n          chunkArray.push(value.chunk);\n        })\n        .then(() => {\n          const blob = new Blob(chunkArray, { type: \"video/webm\" });\n          const url = URL.createObjectURL(blob);\n          chrome.downloads\n            .download({\n              url: url,\n              filename: \"recovered-video.webm\",\n              saveAs: true,\n            })\n            .then(() => {\n              URL.revokeObjectURL(url);\n              // Close this tab\n              window.close();\n            });\n        });\n    } else if (message.type === \"recover-cloud-indexed-db\") {\n      (async () => {\n        const ts = new Date().toISOString().replace(/[:.]/g, \"-\");\n        let downloaded = 0;\n\n        const downloadTrack = async (store, label, mimeType) => {\n          const entries = [];\n          await store.iterate((value) => {\n            if (value?.chunk) entries.push(value);\n          });\n          entries.sort((a, b) => (a.index || 0) - (b.index || 0));\n          if (!entries.length) return;\n          const blob = new Blob(entries.map((e) => e.chunk), { type: mimeType });\n          const url = URL.createObjectURL(blob);\n          await chrome.downloads.download({\n            url,\n            filename: `Screenity-Recovery-${label}-${ts}.webm`,\n            saveAs: false,\n          });\n          URL.revokeObjectURL(url);\n          downloaded++;\n        };\n\n        await downloadTrack(chunksStore, \"Screen\", \"video/webm\");\n        await downloadTrack(cameraChunksStore, \"Camera\", \"video/webm\");\n        await downloadTrack(audioChunksStore, \"Audio\", \"audio/webm\");\n\n        // Clear all three stores after download is initiated\n        await Promise.allSettled([\n          chunksStore.clear(),\n          cameraChunksStore.clear(),\n          audioChunksStore.clear(),\n        ]);\n\n        const { recorderSession } = await chrome.storage.local.get([\n          \"recorderSession\",\n        ]);\n\n        // Remove stale TUS journal + scene keys so the next recording cannot\n        // accidentally resume the crashed upload or reuse the old scene.\n        // Mirrors clearStaleUploadJournals() in CloudRecorder.jsx.\n        const journalKeysToRemove = [\"sceneId\", \"sceneIdStatus\"];\n        const tracks = recorderSession?.tracks || {};\n        for (const trackData of Object.values(tracks)) {\n          const upl = trackData?.uploader;\n          if (!upl) continue;\n          if (upl.journalKey) journalKeysToRemove.push(upl.journalKey);\n          if (upl.journalLookupKey)\n            journalKeysToRemove.push(upl.journalLookupKey);\n          const pid = upl.projectId || recorderSession?.projectId || null;\n          const sid = upl.sceneId || null;\n          const t = upl.type || upl.trackType || null;\n          if (pid && t) {\n            journalKeysToRemove.push(\n              `bunnyVideoMap-${pid}-${sid || \"none\"}-${t || \"none\"}`,\n            );\n          }\n        }\n        try {\n          await chrome.storage.local.remove([...new Set(journalKeysToRemove)]);\n        } catch (err) {\n          console.warn(\"[CloudRestore] failed to remove stale journal keys\", err);\n        }\n\n        // Mark session as recovered so the popup button disables on next open\n        if (recorderSession) {\n          await chrome.storage.local.set({\n            recorderSession: {\n              ...recorderSession,\n              status: \"recovered\",\n              recoveredAt: Date.now(),\n            },\n          });\n        }\n\n        if (downloaded > 0) window.close();\n      })();\n    } else if (message.type === \"recover-indexed-db-mp4\") {\n      const chunkArray = [];\n      chunksStore\n        .iterate((value) => {\n          if (value && typeof value.index === \"number\" && value.chunk) {\n            chunkArray.push({ index: value.index, chunk: value.chunk });\n          }\n        })\n        .then(() => {\n          chunkArray.sort((a, b) => a.index - b.index);\n          const blob = new Blob(\n            chunkArray.map((entry) => entry.chunk),\n            { type: \"video/mp4\" }\n          );\n          const url = URL.createObjectURL(blob);\n          const filename = `screenity-recording-${Date.now()}.mp4`;\n          chrome.downloads\n            .download({\n              url: url,\n              filename,\n              saveAs: true,\n            })\n            .then(() => {\n              URL.revokeObjectURL(url);\n              window.close();\n            });\n        });\n    }\n  });\n\n  useEffect(() => {\n    // chrome on message\n    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n      handleMessage(message);\n    });\n\n    return () => {\n      chrome.runtime.onMessage.removeListener(\n        (message, sender, sendResponse) => {\n          handleMessage(message);\n        }\n      );\n    };\n  }, []);\n\n  return <div></div>;\n};\n\nexport default Download;\n"
  },
  {
    "path": "src/pages/Download/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Video editor</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <!-- <script src=\"chrome-extension://__MSG_@@extension_id__/assets/vendor/ffmpeg-core.js\"></script> -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Download/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Download from \"./Download\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Download />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Editor/Sandbox.jsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\n// Import all the utils\nimport addAudioToVideo from \"./utils/addAudioToVideo\";\nimport base64ToBlob from \"./utils/base64toBlob\";\nimport blobToArrayBuffer from \"./utils/blobToArrayBuffer\";\nimport cropVideo from \"./utils/cropVideo\";\nimport cutVideo from \"./utils/cutVideo\";\nimport getFrame from \"./utils/getFrame\";\nimport hasAudio from \"./utils/hasAudio\";\nimport muteVideo from \"./utils/muteVideo\";\nimport reencodeVideo from \"./utils/reencodeVideo\";\nimport toGIF from \"./utils/toGIF\";\nimport toWebM from \"./utils/toWebM\";\n\nconst Sandbox = () => {\n  const iframeRef = useRef(null);\n  const scriptLoaded = useRef(false);\n  const triggerLoad = useRef(false);\n  const ffmpegInstance = useRef(null);\n\n  const sendMessage = (message) => {\n    iframeRef.current.contentWindow.postMessage(message, \"*\");\n  };\n\n  const loadFfmpeg = async () => {\n    if (!scriptLoaded.current) return;\n    if (!triggerLoad.current) return;\n    if (ffmpegInstance.current) return;\n\n    try {\n      const { createFFmpeg } = FFmpeg;\n      // Initialize ffmpeg.js\n      ffmpegInstance.current = createFFmpeg({\n        log: false,\n        progress: (params) => {\n          // Send progress updates to parent window\n          if (params.ratio && params.ratio >= 0) {\n            const percentage = Math.min(Math.round(params.ratio * 100), 100);\n            sendMessage({\n              type: \"ffmpeg-progress\",\n              progress: percentage,\n            });\n          }\n        },\n        corePath: \"assets/vendor/ffmpeg-core.js\",\n      });\n      await ffmpegInstance.current.load();\n      sendMessage({ type: \"ffmpeg-loaded\" });\n    } catch (error) {\n      sendMessage({\n        type: \"ffmpeg-load-error\",\n        error: JSON.stringify(error),\n        fallback: false,\n      });\n    }\n  };\n\n  useEffect(() => {\n    const script = document.createElement(\"script\");\n\n    script.src = \"assets/vendor/ffmpeg.min.js\";\n    script.async = true;\n\n    // On load, set scriptLoaded to true\n    script.onload = () => {\n      scriptLoaded.current = true;\n      loadFfmpeg();\n    };\n\n    document.body.appendChild(script);\n\n    return () => {\n      document.body.removeChild(script);\n    };\n  }, []);\n\n  const toBase64 = (blob) => {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.readAsDataURL(blob);\n      reader.onloadend = () => {\n        resolve(reader.result);\n      };\n      reader.onerror = reject;\n    });\n  };\n\n  const onMessage = async (message) => {\n    if (message.type === \"load-ffmpeg\") {\n      triggerLoad.current = true;\n      loadFfmpeg();\n    } else if (message.type === \"add-audio-to-video\") {\n      try {\n        const blob = await addAudioToVideo(\n          ffmpegInstance.current,\n          message.blob,\n          message.audio,\n          message.duration,\n          message.volume,\n          message.replaceAudio,\n        );\n        const base64 = await toBase64(blob);\n        sendMessage({ type: \"updated-blob\", base64: base64, topLevel: true });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"base64-to-blob\") {\n      try {\n        const blob = await base64ToBlob(ffmpegInstance.current, message.base64);\n        const base64 = await toBase64(blob);\n        sendMessage({\n          type: \"updated-blob\",\n          base64: base64,\n          topLevel: true,\n          edited: false,\n        });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"blob-to-array-buffer\") {\n      try {\n        const arrayBuffer = await blobToArrayBuffer(\n          ffmpegInstance.current,\n          message.blob,\n        );\n        sendMessage({ type: \"updated-array-buffer\", arrayBuffer: arrayBuffer });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"crop-video\") {\n      try {\n        const blob = await cropVideo(ffmpegInstance.current, message.blob, {\n          x: message.x,\n          y: message.y,\n          width: message.width,\n          height: message.height,\n        });\n        const base64 = await toBase64(blob);\n        sendMessage({ type: \"updated-blob\", base64: base64, topLevel: true });\n        //sendMessage({ type: \"crop-update\" });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"cut-video\") {\n      try {\n        const blob = await cutVideo(\n          ffmpegInstance.current,\n          message.blob,\n          message.startTime,\n          message.endTime,\n          message.cut,\n          message.duration,\n          message.encode,\n        );\n        const base64 = await toBase64(blob);\n        sendMessage({\n          type: \"updated-blob\",\n          base64: base64,\n          addToHistory: true,\n        });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"get-frame\") {\n      try {\n        const blob = await getFrame(\n          ffmpegInstance.current,\n          message.blob,\n          message.time,\n        );\n        sendMessage({ type: \"new-frame\", frame: blob });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"has-audio\") {\n      try {\n        const audio = await hasAudio(ffmpegInstance.current, message.video);\n        sendMessage({ type: \"updated-has-audio\", hasAudio: audio });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"mute-video\") {\n      try {\n        const blob = await muteVideo(\n          ffmpegInstance.current,\n          message.blob,\n          message.startTime,\n          message.endTime,\n          message.duration,\n        );\n        const base64 = await toBase64(blob);\n        sendMessage({\n          type: \"updated-blob\",\n          base64: base64,\n          addToHistory: true,\n        });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"reencode-video\") {\n      try {\n        const blob = await reencodeVideo(\n          ffmpegInstance.current,\n          message.blob,\n          message.duration,\n        );\n        const base64 = await toBase64(blob);\n        sendMessage({ type: \"updated-blob\", base64: base64, topLevel: true });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"to-gif\") {\n      try {\n        const blob = await toGIF(ffmpegInstance.current, message.blob);\n        const base64 = await toBase64(blob);\n        sendMessage({ type: \"download-gif\", base64: base64 });\n      } catch (error) {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error) });\n      }\n    } else if (message.type === \"to-webm\") {\n      try {\n        const blob = await toWebM(\n          ffmpegInstance.current,\n          message.blob,\n          message.duration,\n        );\n        const base64 = await toBase64(blob);\n\n        sendMessage({\n          type: \"download-webm\",\n          base64,\n          topLevel: true,\n        });\n      } catch (error) {\n        sendMessage({\n          type: \"ffmpeg-error\",\n          error: JSON.stringify(error),\n        });\n      }\n    }\n  };\n\n  useEffect(() => {\n    const handler = (event) => onMessage(event.data);\n    window.addEventListener(\"message\", handler);\n    return () => window.removeEventListener(\"message\", handler);\n  }, []);\n\n  return (\n    <div>\n      <iframe\n        ref={iframeRef}\n        src={`sandbox.html${window.location.search || \"\"}`}\n        allowFullScreen={true}\n        style={{\n          width: \"100%\",\n          border: \"none\",\n          height: \"100vh\",\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n        }}\n      ></iframe>\n    </div>\n  );\n};\n\nexport default Sandbox;\n"
  },
  {
    "path": "src/pages/Editor/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Video editor</title>\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <!-- <script src=\"chrome-extension://__MSG_@@extension_id__/assets/vendor/ffmpeg-core.js\"></script> -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Editor/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Sandbox from \"./Sandbox\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Sandbox />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Editor/utils/addAudioToVideo.js",
    "content": "async function addAudioToVideo(\n  ffmpeg,\n  videoBlob,\n  audioBlob,\n  videoDuration,\n  audioVolume = 1.0,\n  replaceAudio = false\n) {\n  const videoData = new Uint8Array(await videoBlob.arrayBuffer());\n  const audioData = new Uint8Array(await audioBlob.arrayBuffer());\n\n  // Set the input video and audio file names\n  ffmpeg.FS(\"writeFile\", \"input-video.mp4\", videoData);\n  ffmpeg.FS(\"writeFile\", \"input-audio.mp3\", audioData);\n\n  // Set the output video file name\n  const outputFileName = \"output-with-audio.mp4\";\n\n  // Build FFmpeg command for merging video and audio with volume adjustment\n  let ffmpegCommand = [\n    \"-i\",\n    \"input-video.mp4\",\n    \"-i\",\n    \"input-audio.mp3\",\n    \"-filter_complex\",\n    `[0:a]volume=1[a];[1:a]volume=${audioVolume}[b];[a][b]amix=inputs=2:duration=first:dropout_transition=2[v]`,\n    \"-map\",\n    \"0:v\", // Map the original video stream\n    \"-map\",\n    \"[v]\", // Map the merged audio\n    \"-c:v\",\n    \"copy\",\n    \"-c:a\",\n    \"aac\",\n    \"-strict\",\n    \"experimental\",\n    \"-shortest\",\n    outputFileName,\n  ];\n\n  if (replaceAudio) {\n    // Remove the original audio stream and replace it with the audio from the audio blob\n    ffmpegCommand = [\n      \"-i\",\n      \"input-video.mp4\",\n      \"-i\",\n      \"input-audio.mp3\",\n      \"-filter_complex\",\n      \"[0:a]volume=0[a];[1:a]volume=1[b];[a][b]amix=inputs=2:duration=first:dropout_transition=2[v]\",\n      \"-map\",\n      \"0:v\", // Map the original video stream\n      \"-map\",\n      \"[v]\", // Map the merged audio\n      \"-c:v\",\n      \"copy\",\n      \"-c:a\",\n      \"aac\",\n      \"-strict\",\n      \"experimental\",\n      \"-shortest\",\n      outputFileName,\n    ];\n  }\n\n  // Run FFmpeg to merge video and audio with volume adjustment\n  await ffmpeg.run(...ffmpegCommand);\n\n  // Get the merged video data\n  const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n  // Create a Blob from the merged video data\n  const videoWithAudioBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n  // Return the video with merged audio Blob\n  return videoWithAudioBlob;\n}\n\nexport default addAudioToVideo;\n"
  },
  {
    "path": "src/pages/Editor/utils/base64toBlob.js",
    "content": "function base64ToUint8Array(base64) {\n  const dataURLRegex = /^data:.+;base64,/;\n  if (dataURLRegex.test(base64)) {\n    base64 = base64.replace(dataURLRegex, \"\");\n  }\n\n  const binary_string = window.atob(base64);\n  const len = binary_string.length;\n  const bytes = new Uint8Array(len);\n\n  for (let i = 0; i < len; i++) {\n    bytes[i] = binary_string.charCodeAt(i);\n  }\n\n  return bytes;\n}\n\nasync function base64ToBlob(ffmpeg, base64) {\n  const input = base64ToUint8Array(base64);\n  ffmpeg.FS(\"writeFile\", \"input.webm\", input);\n\n  await ffmpeg.run(\n    \"-i\",\n    \"input.webm\",\n    \"-max_muxing_queue_size\",\n    \"512\",\n    \"-preset\",\n    \"superfast\",\n    \"-threads\",\n    \"0\",\n    \"-r\",\n    \"30\",\n    \"-tune\",\n    \"fastdecode\",\n    \"output.mp4\"\n  );\n\n  const data = ffmpeg.FS(\"readFile\", \"output.mp4\");\n  const videoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n  return videoBlob;\n}\n\nexport default base64ToBlob;\n"
  },
  {
    "path": "src/pages/Editor/utils/blobToArrayBuffer.js",
    "content": "async function blobToArrayBuffer(blob) {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      if (reader.result instanceof ArrayBuffer) {\n        resolve(reader.result);\n      } else {\n        reject(new Error(\"Failed to convert Blob to ArrayBuffer\"));\n      }\n    };\n    reader.onerror = reject;\n    reader.readAsArrayBuffer(blob);\n  });\n}\n\nexport default blobToArrayBuffer;\n"
  },
  {
    "path": "src/pages/Editor/utils/cropVideo.js",
    "content": "async function cropVideo(ffmpeg, videoBlob, cropParameters) {\n  const videoData = new Uint8Array(await videoBlob.arrayBuffer());\n\n  // Set the input video file name\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n\n  // Set the output video file name\n  const outputFileName = \"output-cropped.mp4\";\n\n  // Build FFmpeg command for cropping\n  const ffmpegCommand = [\n    \"-i\",\n    \"input.mp4\",\n    \"-vf\",\n    `crop=${cropParameters.width}:${cropParameters.height}:${cropParameters.x}:${cropParameters.y}`,\n    \"-c:a\",\n    \"copy\",\n    \"-preset\",\n    \"superfast\",\n    \"-threads\",\n    \"0\",\n    \"-r\",\n    \"30\",\n    \"-tune\",\n    \"fastdecode\",\n    outputFileName,\n  ];\n\n  // Run FFmpeg to crop the video\n  await ffmpeg.run(...ffmpegCommand);\n\n  // Get the cropped video data\n  const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n  // Create a Blob from the cropped video data\n  const croppedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n  // Return the cropped video Blob\n  return croppedVideoBlob;\n}\n\nexport default cropVideo;\n"
  },
  {
    "path": "src/pages/Editor/utils/cutVideo.js",
    "content": "async function cutVideo(ffmpeg, videoBlob, start, end, cut, duration, encode) {\n  const videoData = new Uint8Array(await videoBlob.arrayBuffer());\n\n  // Set the input video file name\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n\n  // Set the output video file name\n  const outputFileName = cut ? \"output-cut.mp4\" : \"output-trimmed.mp4\";\n  let encodeOptions = [\n    \"-c:v\",\n    \"copy\",\n    \"-c:a\",\n    \"copy\",\n    \"-reset_timestamps\",\n    \"1\",\n  ];\n  if (encode) {\n    encodeOptions = [\n      \"-preset\",\n      \"superfast\",\n      \"-threads\",\n      \"0\",\n      \"-r\",\n      \"30\",\n      \"-tune\",\n      \"fastdecode\",\n    ];\n  }\n\n  if (cut) {\n    if (start > 0 && end < duration) {\n      await ffmpeg.run(\n        \"-ss\",\n        \"0\",\n        \"-i\",\n        \"input.mp4\",\n        \"-to\",\n        start.toString(),\n        ...encodeOptions,\n        \"part1.mp4\"\n      );\n\n      // Then, cut the video from the end time to the end\n      await ffmpeg.run(\n        \"-ss\",\n        end.toString(),\n        \"-i\",\n        \"input.mp4\",\n        \"-to\",\n        duration.toString(),\n        ...encodeOptions,\n        \"part2.mp4\"\n      );\n\n      // Create a text file with the list of input videos\n      ffmpeg.FS(\"writeFile\", \"input.txt\", \"file 'part1.mp4'\\nfile 'part2.mp4'\");\n\n      // Concatenate the two remaining parts\n      await ffmpeg.run(\n        \"-f\",\n        \"concat\",\n        \"-safe\",\n        \"0\",\n        \"-i\",\n        \"input.txt\",\n        \"-c\",\n        \"copy\",\n        outputFileName\n      );\n\n      // Get the edited video data\n      const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n      // Create a Blob from the edited video data\n      const editedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n      // Return the edited video Blob\n      return editedVideoBlob;\n    } else if (start == 0 && end < duration) {\n      await ffmpeg.run(\n        \"-ss\",\n        end.toString(),\n        \"-i\",\n        \"input.mp4\",\n        \"-to\",\n        duration.toString(),\n        ...encodeOptions,\n        outputFileName\n      );\n\n      // Get the edited video data\n      const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n      // Create a Blob from the edited video data\n      const editedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n      // Return the edited video Blob\n      return editedVideoBlob;\n    } else if (start > 0 && end == duration) {\n      await ffmpeg.run(\n        \"-ss\",\n        \"0\",\n        \"-i\",\n        \"input.mp4\",\n        \"-to\",\n        start.toString(),\n        ...encodeOptions,\n        outputFileName\n      );\n\n      // Get the edited video data\n      const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n      // Create a Blob from the edited video data\n      const editedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n      // Return the edited video Blob\n      return editedVideoBlob;\n    }\n  } else {\n    await ffmpeg.run(\n      \"-ss\",\n      start.toString(),\n      \"-i\",\n      \"input.mp4\",\n      \"-t\",\n      (end - start).toString(),\n      ...encodeOptions,\n      outputFileName\n    );\n\n    // Get the edited video data\n    const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n    // Create a Blob from the edited video data\n    const editedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n    // Return the edited video Blob\n    return editedVideoBlob;\n  }\n}\n\nexport default cutVideo;\n"
  },
  {
    "path": "src/pages/Editor/utils/getFrame.js",
    "content": "/* getFrame.js */\nasync function getFrame(ffmpeg, videoBlob, time = 0) {\n  const videoData = new Uint8Array(await videoBlob.arrayBuffer());\n  // Write video data to a file\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n\n  const outputFileName = \"output.jpg\";\n\n  // Use FFmpeg to extract a frame as a JPEG image\n  await ffmpeg.run(\n    \"-i\",\n    \"input.mp4\",\n    \"-ss\",\n    time.toString(),\n    \"-frames:v\",\n    \"1\",\n    \"-preset\",\n    \"superfast\",\n    \"-threads\",\n    \"0\",\n    \"-r\",\n    \"30\",\n    \"-tune\",\n    \"fastdecode\",\n    outputFileName\n  );\n\n  // Read the generated frame image\n  const frameData = ffmpeg.FS(\"readFile\", outputFileName);\n\n  // Create a Blob from the frame data\n  const frameBlob = new Blob([frameData.buffer], {\n    type: \"image/jpeg\",\n  });\n\n  // Clean up\n  ffmpeg.FS(\"unlink\", \"input.mp4\");\n  ffmpeg.FS(\"unlink\", outputFileName);\n\n  return frameBlob;\n}\n\nexport default getFrame;\n"
  },
  {
    "path": "src/pages/Editor/utils/hasAudio.js",
    "content": "const hasAudio = async (videoBlob) => {\n  const videoElement = document.createElement(\"video\");\n  videoElement.src = URL.createObjectURL(videoBlob);\n\n  return new Promise(async (resolve, reject) => {\n    try {\n      videoElement.addEventListener(\"loadedmetadata\", async () => {\n        try {\n          const mediaSource = new MediaSource();\n          videoElement.src = URL.createObjectURL(mediaSource);\n          await mediaSource.addSourceBuffer(videoBlob.type);\n\n          mediaSource.onsourceopen = () => {\n            const audioContext = new AudioContext();\n            const source = audioContext.createMediaElementSource(videoElement);\n\n            source.connect(audioContext.destination);\n            source.onended = () => {\n              // If audio plays and ends, it has audio tracks\n              resolve(true);\n            };\n            source.onerror = () => {\n              // If there's an error, it doesn't have audio tracks\n              resolve(false);\n            };\n\n            // Start playing the video\n            videoElement.play();\n          };\n        } catch (error) {\n          resolve(false); // MediaSource or AudioContext not supported\n        }\n      });\n\n      videoElement.addEventListener(\"error\", (error) => {\n        reject(error);\n      });\n\n      videoElement.load();\n    } catch (error) {\n      reject(error);\n    } finally {\n      URL.revokeObjectURL(videoElement.src);\n    }\n  });\n};\n\nexport default hasAudio;\n"
  },
  {
    "path": "src/pages/Editor/utils/muteVideo.js",
    "content": "async function muteVideo(ffmpeg, videoBlob, start, end) {\n  try {\n    const videoData = new Uint8Array(await videoBlob.arrayBuffer());\n\n    // Set the input video file name\n    ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n\n    // Set the output video file name\n    const outputFileName = \"output.mp4\";\n\n    // Mute the audio in the specified time range\n    await ffmpeg.run(\n      \"-i\",\n      \"input.mp4\",\n      \"-af\",\n      `volume='if(between(t,${start},${end}),0,1)':eval=frame`,\n      \"-c:v\",\n      \"copy\",\n      \"-c:a\",\n      \"aac\",\n      outputFileName\n    );\n\n    // Get the edited video data\n    const data = ffmpeg.FS(\"readFile\", outputFileName);\n\n    // Create a Blob from the edited video data\n    const editedVideoBlob = new Blob([data.buffer], { type: \"video/mp4\" });\n\n    // Return the edited video Blob\n    return editedVideoBlob;\n  } catch (error) {\n    console.error(\"Error muting video:\", error);\n    return null;\n  }\n}\n\nexport default muteVideo;\n"
  },
  {
    "path": "src/pages/Editor/utils/reencodeVideo.js",
    "content": "async function reencodeVideo(ffmpeg, blob) {\n  const videoData = new Uint8Array(await blob.arrayBuffer());\n  const outputFileName = \"output.mp4\";\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n  await ffmpeg.run(\n    \"-i\",\n    \"input.mp4\",\n    \"-preset\",\n    \"superfast\",\n    \"-threads\",\n    \"0\",\n    \"-r\",\n    \"30\",\n    \"-tune\",\n    \"fastdecode\",\n    outputFileName\n  );\n\n  const data = ffmpeg.FS(\"readFile\", outputFileName);\n  const editedVideoBlob = new Blob([data.buffer], {\n    type: \"video/mp4\",\n  });\n  return editedVideoBlob;\n}\n\nexport default reencodeVideo;\n"
  },
  {
    "path": "src/pages/Editor/utils/toGIF.js",
    "content": "async function toGIF(ffmpeg, blob) {\n  const videoData = new Uint8Array(await blob.arrayBuffer());\n  const outputFileName = \"output.gif\";\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", videoData);\n  await ffmpeg.run(\n    \"-i\",\n    \"input.mp4\",\n    \"-preset\",\n    \"superfast\",\n    \"-threads\",\n    \"0\",\n    \"-r\",\n    \"30\",\n    \"-tune\",\n    \"fastdecode\",\n    outputFileName\n  );\n  const data = ffmpeg.FS(\"readFile\", outputFileName);\n  const editedVideoBlob = new Blob([data.buffer], {\n    type: \"image/gif\",\n  });\n  return editedVideoBlob;\n}\n\nexport default toGIF;\n"
  },
  {
    "path": "src/pages/Editor/utils/toWebM.js",
    "content": "async function toWebM(ffmpeg, blob) {\n  const inputData = new Uint8Array(await blob.arrayBuffer());\n  ffmpeg.FS(\"writeFile\", \"input.mp4\", inputData);\n\n  const output = \"output.webm\";\n\n  await ffmpeg.run(\n    \"-i\",\n    \"input.mp4\",\n    \"-c:v\",\n    \"libvpx\", // VP8 (much faster)\n    \"-b:v\",\n    \"2M\", // higher bitrate = fewer CPU cycles\n    \"-crf\",\n    \"10\", // low compression work\n    \"-speed\",\n    \"8\", // EXTREMELY important for WASM\n    \"-threads\",\n    \"0\",\n    \"-auto-alt-ref\",\n    \"0\", // turn off slow filtering\n    output\n  );\n\n  const data = ffmpeg.FS(\"readFile\", output);\n  return new Blob([data.buffer], { type: \"video/webm\" });\n}\n"
  },
  {
    "path": "src/pages/EditorViewer/Sandbox.jsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\nconst Sandbox = () => {\n  const iframeRef = useRef(null);\n\n  const sendMessage = (message) => {\n    if (iframeRef.current?.contentWindow) {\n      iframeRef.current.contentWindow.postMessage(message, \"*\");\n    }\n  };\n\n  const onMessage = async (message) => {\n    if (message.type === \"load-ffmpeg\") {\n      sendMessage({ type: \"ffmpeg-load-error\", fallback: true });\n    } else if (\n      message.type === \"add-audio-to-video\" ||\n      message.type === \"base64-to-blob\" ||\n      message.type === \"crop-video\" ||\n      message.type === \"cut-video\" ||\n      message.type === \"mute-video\" ||\n      message.type === \"reencode-video\" ||\n      message.type === \"to-gif\"\n    ) {\n      sendMessage({\n        type: \"ffmpeg-error\",\n        error:\n          \"Processing not available in viewer mode. Please use a modern browser (Chrome 94+) for editing features.\",\n      });\n    }\n  };\n\n  useEffect(() => {\n    const handler = (event) => onMessage(event.data);\n    window.addEventListener(\"message\", handler);\n    return () => window.removeEventListener(\"message\", handler);\n  }, []);\n\n  return (\n    <div>\n      <iframe\n        ref={iframeRef}\n        src={`sandbox.html${window.location.search || \"\"}`}\n        allowFullScreen\n        style={{\n          width: \"100%\",\n          border: \"none\",\n          height: \"100vh\",\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n        }}\n      />\n    </div>\n  );\n};\n\nexport default Sandbox;\n"
  },
  {
    "path": "src/pages/EditorViewer/index.css",
    "content": "/* EditorViewer shares styles with Editor */\nbody {\n  margin: 0;\n  padding: 0;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n    \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n    sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n  font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n    monospace;\n}\n\n* {\n  box-sizing: border-box;\n}\n\n"
  },
  {
    "path": "src/pages/EditorViewer/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Viewer</title>\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/EditorViewer/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Sandbox from \"./Sandbox\";\nimport \"./index.css\";\n\nconst container = document.querySelector(\"#app-container\");\nconst root = createRoot(container);\nroot.render(<Sandbox />);\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/Sandbox.jsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\nimport addAudioToVideo from \"./utils/addAudioToVideo\";\nimport convertWebmToMp4 from \"./utils/convertWebmToMp4\";\nimport cropVideo from \"./utils/cropVideo\";\nimport cutVideo from \"./utils/cutVideo\";\nimport muteVideo from \"./utils/muteVideo\";\nimport reencodeVideo from \"./utils/reencodeVideo\";\nimport toGIF from \"./utils/toGIF\";\nimport getFrame from \"./utils/getFrame\";\nimport hasAudio from \"./utils/hasAudio\";\nimport convertMp4ToWebm from \"./utils/convertMp4ToWebm\";\nimport blobToArrayBuffer from \"./utils/blobToArrayBuffer\";\n\nconst Sandbox = () => {\n  const iframeRef = useRef(null);\n  const triggerLoad = useRef(false);\n  const ffmpegInstance = useRef(null);\n  const mediabunnyLoaded = useRef(false);\n\n  const sendMessage = (message) => {\n    iframeRef.current?.contentWindow?.postMessage(message, \"*\");\n  };\n\n  const loadFfmpeg = async () => {\n    if (mediabunnyLoaded.current || !triggerLoad.current) return;\n    try {\n      mediabunnyLoaded.current = true;\n      sendMessage({ type: \"ffmpeg-loaded\" });\n    } catch (error) {\n      sendMessage({\n        type: \"ffmpeg-load-error\",\n        error: JSON.stringify(error),\n        fallback: true,\n      });\n    }\n  };\n\n  function isMp4Blob(blob) {\n    return blob\n      .slice(4, 8)\n      .text()\n      .then((t) => t === \"ftyp\");\n  }\n\n  const toBase64 = (blob) =>\n    new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.readAsDataURL(blob);\n      reader.onloadend = () => resolve(reader.result);\n      reader.onerror = reject;\n    });\n\n  const base64ToWebmBlob = (base64) => {\n    const dataURLRegex = /^data:.+;base64,/;\n    if (dataURLRegex.test(base64)) base64 = base64.replace(dataURLRegex, \"\");\n    const binary = window.atob(base64);\n    const len = binary.length;\n    const bytes = new Uint8Array(len);\n    for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);\n    return new Blob([bytes], { type: \"video/webm\" });\n  };\n\n  const onMessage = async (message) => {\n    try {\n      switch (message.type) {\n        case \"load-ffmpeg\":\n          triggerLoad.current = true;\n          await loadFfmpeg();\n          break;\n\n        case \"add-audio-to-video\": {\n          const blob = await addAudioToVideo(\n            ffmpegInstance.current,\n            message.blob,\n            message.audio,\n            message.duration,\n            message.volume,\n            message.replaceAudio,\n            (progress) =>\n              sendMessage({\n                type: \"ffmpeg-progress\",\n                progress: Math.round(progress * 100),\n              })\n          );\n          const base64 = await toBase64(blob);\n          sendMessage({\n            type: \"updated-blob\",\n            base64,\n            topLevel: true,\n            fromAudio: true,\n            _opId: message._opId,\n          });\n          break;\n        }\n\n        case \"compress-video\": {\n          const rawBlob = await fetch(message.base64).then((r) => r.blob());\n\n          const compressed = await reencodeVideo(\n            ffmpegInstance.current,\n            rawBlob,\n            null,\n            (progress) =>\n              sendMessage({\n                type: \"compression-progress\",\n                progress: Math.round(progress * 100),\n              })\n          );\n          const base64 = await toBase64(compressed);\n          sendMessage({\n            type: \"updated-blob\",\n            base64,\n            topLevel: true,\n            _opId: message._opId,\n          });\n          break;\n        }\n\n        case \"base64-to-blob\": {\n          const rawBlob = await fetch(message.base64).then((r) => r.blob());\n\n          const header = await rawBlob.slice(4, 8).text();\n          const looksMp4 = header === \"ftyp\";\n\n          if (looksMp4) {\n            sendMessage({\n              type: \"updated-blob\",\n              base64: message.base64,\n              topLevel: true,\n            });\n            break;\n          }\n\n          const webmBlob = base64ToWebmBlob(message.base64);\n          const mp4Blob = await convertWebmToMp4(webmBlob, (progress) =>\n            sendMessage({\n              type: \"ffmpeg-progress\",\n              progress: Math.round(progress * 100),\n            })\n          );\n\n          const base64 = await toBase64(mp4Blob);\n          sendMessage({ type: \"updated-blob\", base64, topLevel: true });\n          break;\n        }\n\n        case \"blob-to-array-buffer\": {\n          const arrayBuffer = await blobToArrayBuffer(\n            ffmpegInstance.current,\n            message.blob\n          );\n          sendMessage({ type: \"updated-array-buffer\", arrayBuffer });\n          break;\n        }\n\n        case \"crop-video\": {\n          const blob = await cropVideo(\n            ffmpegInstance.current,\n            message.blob,\n            {\n              x: message.x,\n              y: message.y,\n              width: message.width,\n              height: message.height,\n            },\n            (progress) => sendMessage({ type: \"ffmpeg-progress\", progress })\n          );\n          const base64 = await toBase64(blob);\n          sendMessage({ type: \"updated-blob\", base64, topLevel: true, _opId: message._opId });\n          break;\n        }\n\n        case \"cut-video\": {\n          const blob = await cutVideo(\n            ffmpegInstance.current,\n            message.blob,\n            message.startTime,\n            message.endTime,\n            message.cut,\n            message.duration,\n            message.encode,\n            (progress) => sendMessage({ type: \"ffmpeg-progress\", progress })\n          );\n          const base64 = await toBase64(blob);\n          sendMessage({\n            type: \"updated-blob\",\n            base64,\n            addToHistory: true,\n            topLevel: true,\n            _opId: message._opId,\n          });\n          break;\n        }\n\n        case \"get-frame\": {\n          const blob = await getFrame(\n            ffmpegInstance.current,\n            message.blob,\n            message.time\n          );\n          sendMessage({ type: \"new-frame\", frame: blob });\n          break;\n        }\n\n        case \"has-audio\": {\n          const audio = await hasAudio(ffmpegInstance.current, message.video);\n          sendMessage({ type: \"updated-has-audio\", hasAudio: audio });\n          break;\n        }\n\n        case \"mute-video\": {\n          const blob = await muteVideo(\n            ffmpegInstance.current,\n            message.blob,\n            message.startTime,\n            message.endTime,\n            message.duration,\n            (progress) => sendMessage({ type: \"ffmpeg-progress\", progress })\n          );\n          const base64 = await toBase64(blob);\n          sendMessage({\n            type: \"updated-blob\",\n            base64,\n            addToHistory: true,\n            topLevel: true,\n            _opId: message._opId,\n          });\n          break;\n        }\n\n        case \"reencode-video\": {\n          const blob = await reencodeVideo(\n            ffmpegInstance.current,\n            message.blob,\n            message.duration,\n            (progress) =>\n              sendMessage({\n                type: \"ffmpeg-progress\",\n                progress: Math.round(progress * 100),\n              })\n          );\n          const base64 = await toBase64(blob);\n          sendMessage({ type: \"updated-blob\", base64, topLevel: true, _opId: message._opId });\n          break;\n        }\n\n        case \"to-gif\": {\n          const blob = await toGIF(ffmpegInstance.current, message.blob);\n          const base64 = await toBase64(blob);\n          sendMessage({ type: \"download-gif\", base64 });\n          break;\n        }\n\n        case \"to-webm\": {\n          if (message.blob?.type === \"video/webm\") {\n            const base64 = await toBase64(message.blob);\n            sendMessage({ type: \"download-webm\", base64 });\n            return;\n          }\n\n          const result = await convertMp4ToWebm(message.blob, (progress) =>\n            sendMessage({\n              type: \"ffmpeg-progress\",\n              progress: Math.round(progress * 100),\n            })\n          );\n\n          const base64 = await toBase64(result);\n          sendMessage({ type: \"download-webm\", base64 });\n          break;\n        }\n\n        default:\n          break;\n      }\n    } catch (error) {\n      const errMsg = error instanceof Error ? error.message : String(error);\n      if (errMsg.includes(\"too long\")) {\n        sendMessage({ type: \"edit-too-long\", _opId: message._opId });\n      } else {\n        sendMessage({ type: \"ffmpeg-error\", error: JSON.stringify(error), _opId: message._opId });\n      }\n    }\n  };\n\n  useEffect(() => {\n    const handler = (event) => onMessage(event.data);\n    window.addEventListener(\"message\", handler);\n    return () => window.removeEventListener(\"message\", handler);\n  }, []);\n\n  return (\n    <div>\n      <iframe\n        ref={iframeRef}\n        src={`sandbox.html${window.location.search || \"\"}`}\n        allowFullScreen\n        style={{\n          width: \"100%\",\n          border: \"none\",\n          height: \"100vh\",\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n        }}\n      />\n    </div>\n  );\n};\n\nexport default Sandbox;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Video editor</title>\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n    <!-- <script src=\"chrome-extension://__MSG_@@extension_id__/assets/vendor/ffmpeg-core.js\"></script> -->\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Sandbox from \"./Sandbox\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Sandbox />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoAudioMixer.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  BlobSource,\n  Output,\n  BufferTarget,\n  Mp4OutputFormat,\n  VideoSampleSource,\n  AudioSampleSource,\n  VideoSampleSink,\n  AudioSample,\n  ALL_FORMATS,\n  QUALITY_HIGH,\n} from \"mediabunny\";\nimport { videoConverter } from \"./videoConverter\";\n\nexport class VideoAudioMixer {\n  async addAudio(\n    videoBlob,\n    audioBlob,\n    {\n      mode = \"mix\",\n      videoVolume = 0.7,\n      audioVolume = 0.3,\n      loop = false,\n      verbose = false,\n      onProgress,\n    } = {}\n  ) {\n    const input = new Input({\n      source: new BlobSource(videoBlob),\n      formats: ALL_FORMATS,\n    });\n\n    const outputTarget = new BufferTarget();\n    const output = new Output({\n      format: new Mp4OutputFormat({ fastStart: \"in-memory\" }),\n      target: outputTarget,\n    });\n\n    const videoTrack = await input.getPrimaryVideoTrack();\n    const videoSink = new VideoSampleSink(videoTrack);\n    const codecInfo = await videoConverter.detectBestCodec(\"mp4\");\n    const videoCodec = codecInfo?.codec ?? \"avc\";\n    const videoSource = new VideoSampleSource({\n      codec: videoCodec,\n      bitrate: QUALITY_HIGH,\n    });\n    output.addVideoTrack(videoSource);\n\n    const videoDuration = await this._getDuration(videoBlob);\n\n    // Mixing decodes the full audio track + allocates a mixBuffer — cap at\n    // 15 min to avoid OOM in the renderer process.\n    if (videoDuration > 900) {\n      throw new Error(\n        `Video is too long for in-browser audio mixing (${Math.round(videoDuration / 60)} min). ` +\n          `Maximum supported length is 15 minutes.`\n      );\n    }\n\n    const hasVideoAudio = await input\n      .getPrimaryAudioTrack()\n      .then((t) => !!t)\n      .catch(() => false);\n\n    const audioSource = new AudioSampleSource({\n      codec: \"aac\",\n      bitrate: 128000,\n    });\n    output.addAudioTrack(audioSource);\n\n    let bgArrayBuffer = await audioBlob.arrayBuffer();\n    const audioCtx = new AudioContext();\n    let decodedAudio;\n    try {\n      decodedAudio = await audioCtx.decodeAudioData(bgArrayBuffer);\n      bgArrayBuffer = null; // release compressed audio bytes; PCM is now in decodedAudio\n    } finally {\n      audioCtx.close().catch(() => {});\n    }\n    const sr = decodedAudio.sampleRate;\n    const audioDur = decodedAudio.duration;\n\n    await output.start();\n\n    for await (const frame of videoSink.samples(0, videoDuration)) {\n      await videoSource.add(frame);\n      frame.close();\n      if (onProgress && videoDuration > 0)\n        onProgress(frame.timestamp / videoDuration);\n    }\n\n    const mixBuffer = new Float32Array(Math.ceil(sr * videoDuration));\n    const writeMix = (data, offset, vol) => {\n      for (let i = 0; i < data.length && offset + i < mixBuffer.length; i++) {\n        mixBuffer[offset + i] += data[i] * vol;\n      }\n    };\n\n    if (hasVideoAudio && mode === \"mix\") {\n      const videoAudioCtx = new AudioContext();\n      try {\n        const videoArrayBuffer = await videoBlob.arrayBuffer();\n        const decodedVideoAudio = await videoAudioCtx.decodeAudioData(\n          videoArrayBuffer\n        );\n        const videoSamples = decodedVideoAudio.getChannelData(0);\n        writeMix(videoSamples, 0, videoVolume);\n      } catch {\n      } finally {\n        videoAudioCtx.close().catch(() => {});\n      }\n    }\n\n    const loopCount = loop\n      ? Math.ceil(videoDuration / audioDur)\n      : Math.min(1, Math.ceil(videoDuration / audioDur));\n\n    for (let i = 0; i < loopCount; i++) {\n      const offset = Math.floor(i * audioDur * sr);\n      writeMix(decodedAudio.getChannelData(0), offset, audioVolume);\n    }\n\n    const totalSamples = Math.floor(videoDuration * sr);\n    const chunkSize = sr * 2;\n    let written = 0;\n\n    while (written < totalSamples) {\n      const slice = mixBuffer.slice(written, written + chunkSize);\n      const dur = slice.length / sr;\n      const sample = new AudioSample({\n        data: slice,\n        format: \"f32-planar\",\n        numberOfChannels: 1,\n        sampleRate: sr,\n        timestamp: written / sr,\n        duration: dur,\n      });\n      await audioSource.add(sample);\n      sample.close();\n      written += chunkSize;\n    }\n\n    await output.finalize();\n    return new Blob([outputTarget.buffer], { type: \"video/mp4\" });\n  }\n\n  async _getDuration(blob) {\n    return new Promise((resolve, reject) => {\n      const v = document.createElement(\"video\");\n      v.src = URL.createObjectURL(blob);\n      v.onloadedmetadata = () => {\n        resolve(v.duration);\n        URL.revokeObjectURL(v.src);\n      };\n      v.onerror = reject;\n    });\n  }\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoConverter.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  Output,\n  BlobSource,\n  BufferTarget,\n  Mp4OutputFormat,\n  WebMOutputFormat,\n  ALL_FORMATS,\n  Conversion,\n  type VideoCodec,\n  type AudioCodec,\n} from \"mediabunny\";\n\nexport type SupportedFormat = \"mp4\" | \"webm\";\n\nexport interface ConversionOptions {\n  videoBitrate?: number;\n  audioBitrate?: number;\n  preferredVideoCodec?: VideoCodec;\n  audioCodec?: AudioCodec;\n  onProgress?: (progress: number) => void;\n  verbose?: boolean;\n}\n\nexport interface CodecInfo {\n  codec: VideoCodec;\n  displayName: string;\n  isPreferred: boolean;\n}\n\nconst CODEC_DISPLAY_NAMES: Record<VideoCodec, string> = {\n  avc: \"H.264\",\n  hevc: \"H.265 (HEVC)\",\n  vp9: \"VP9\",\n  av1: \"AV1\",\n  vp8: \"VP8\",\n};\n\nexport class VideoConverter {\n  private cachedMP4Codec: VideoCodec | null = null;\n  private cachedWebMCodec: VideoCodec | null = null;\n\n  async convertToMP4(sourceBlob: Blob, options: ConversionOptions = {}): Promise<Blob> {\n    return this.convert(sourceBlob, \"mp4\", options);\n  }\n\n  async convertToWebM(sourceBlob: Blob, options: ConversionOptions = {}): Promise<Blob> {\n    return this.convert(sourceBlob, \"webm\", options);\n  }\n\n  async detectBestCodec(format: SupportedFormat): Promise<CodecInfo | null> {\n    const outputFormat =\n      format === \"mp4\"\n        ? new Mp4OutputFormat({ fastStart: \"in-memory\" })\n        : new WebMOutputFormat();\n    const supportedCodecs = outputFormat.getSupportedVideoCodecs();\n\n    for (const codec of supportedCodecs) {\n      if (await this.canEncodeCodec(codec)) {\n        return {\n          codec,\n          displayName:\n            CODEC_DISPLAY_NAMES[codec as keyof typeof CODEC_DISPLAY_NAMES] ||\n            codec.toUpperCase(),\n          isPreferred: codec === \"avc\" || codec === \"vp9\",\n        };\n      }\n    }\n    return null;\n  }\n\n  async canEncodeCodec(codec: VideoCodec): Promise<boolean> {\n    if (typeof VideoEncoder === \"undefined\") return false;\n    try {\n      const testConfig = this.getEncoderConfig(codec);\n      const support = await VideoEncoder.isConfigSupported(testConfig);\n      return support.supported ?? false;\n    } catch {\n      return false;\n    }\n  }\n\n  clearCache(): void {\n    this.cachedMP4Codec = null;\n    this.cachedWebMCodec = null;\n  }\n\n  private async convert(\n    sourceBlob: Blob,\n    targetFormat: SupportedFormat,\n    options: ConversionOptions\n  ): Promise<Blob> {\n    const {\n      videoBitrate = 5_000_000,\n      audioBitrate = 128_000,\n      preferredVideoCodec,\n      audioCodec,\n      onProgress,\n    } = options;\n\n    const cache = targetFormat === \"mp4\" ? this.cachedMP4Codec : this.cachedWebMCodec;\n    let videoCodec: VideoCodec | null = cache;\n\n    if (!videoCodec) {\n      if (preferredVideoCodec && (await this.canEncodeCodec(preferredVideoCodec))) {\n        videoCodec = preferredVideoCodec;\n      } else {\n        const codecInfo = await this.detectBestCodec(targetFormat);\n        if (!codecInfo) {\n          throw new Error(\n            `Cannot convert to ${targetFormat.toUpperCase()}: no supported video codec.`\n          );\n        }\n        videoCodec = codecInfo.codec;\n      }\n      if (targetFormat === \"mp4\") this.cachedMP4Codec = videoCodec;\n      else this.cachedWebMCodec = videoCodec;\n    }\n\n    const finalAudioCodec = audioCodec || (targetFormat === \"mp4\" ? \"aac\" : \"opus\");\n\n    const input = new Input({\n      formats: ALL_FORMATS,\n      source: new BlobSource(sourceBlob),\n    });\n\n    const outputFormat =\n      targetFormat === \"mp4\"\n        ? new Mp4OutputFormat({ fastStart: \"in-memory\" })\n        : new WebMOutputFormat();\n\n    const target = new BufferTarget();\n    const output = new Output({ format: outputFormat, target });\n\n    const conversion = await Conversion.init({\n      input,\n      output,\n      video: { codec: videoCodec, bitrate: videoBitrate },\n      audio: { codec: finalAudioCodec, bitrate: audioBitrate },\n    });\n\n    if (!conversion.isValid) {\n      const reasons = conversion.discardedTracks\n        .map((t) => `${t.track.type}: ${t.reason}`)\n        .join(\", \");\n      throw new Error(`Conversion failed - ${reasons}`);\n    }\n\n    if (onProgress) conversion.onProgress = onProgress;\n\n    await conversion.execute();\n\n    if (!target.buffer) throw new Error(\"Conversion failed - no output buffer\");\n\n    return new Blob([target.buffer], {\n      type: targetFormat === \"mp4\" ? \"video/mp4\" : \"video/webm\",\n    });\n  }\n\n  private getEncoderConfig(codec: VideoCodec): VideoEncoderConfig {\n    const baseConfig = { width: 1920, height: 1080, bitrate: 5_000_000 };\n    switch (codec) {\n      case \"avc\":\n        return { ...baseConfig, codec: \"avc1.42E01E\" };\n      case \"hevc\":\n        return { ...baseConfig, codec: \"hev1.1.6.L93.B0\" };\n      default:\n        return { ...baseConfig, codec };\n    }\n  }\n}\n\nexport const videoConverter = new VideoConverter();\n\nexport async function convertToMP4(blob: Blob, options?: ConversionOptions): Promise<Blob> {\n  return videoConverter.convertToMP4(blob, options);\n}\n\nexport async function convertToWebM(blob: Blob, options?: ConversionOptions): Promise<Blob> {\n  return videoConverter.convertToWebM(blob, options);\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoCropper.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  Output,\n  BlobSource,\n  BufferTarget,\n  Mp4OutputFormat,\n  WebMOutputFormat,\n  ALL_FORMATS,\n  Conversion,\n  type OutputFormat,\n} from \"mediabunny\";\n\nexport interface CropOptions {\n  left: number;\n  top: number;\n  width: number;\n  height: number;\n  outputFormat?: \"mp4\" | \"webm\";\n  videoBitrate?: number;\n  audioBitrate?: number;\n  onProgress?: (progress: number) => void;\n  verbose?: boolean;\n}\n\nexport interface CropInfo {\n  originalWidth: number;\n  originalHeight: number;\n  croppedWidth: number;\n  croppedHeight: number;\n  cropRegion: {\n    left: number;\n    top: number;\n    width: number;\n    height: number;\n  };\n}\n\nexport class VideoCropper {\n  async crop(sourceBlob: Blob, options: CropOptions): Promise<Blob> {\n    const {\n      left,\n      top,\n      width,\n      height,\n      outputFormat,\n      videoBitrate,\n      audioBitrate,\n      onProgress,\n    } = options;\n\n    if (left < 0 || top < 0) throw new Error(\"Crop position cannot be negative\");\n    if (width <= 0 || height <= 0) throw new Error(\"Crop dimensions must be positive\");\n\n    const input = new Input({\n      formats: ALL_FORMATS,\n      source: new BlobSource(sourceBlob),\n    });\n\n    const inputFormat = await input.getFormat();\n    const tracks = await input.getTracks();\n    const videoTrack = tracks.find((t) => t.type === \"video\");\n\n    if (!videoTrack || !videoTrack.isVideoTrack()) throw new Error(\"No video track found in input\");\n\n    const originalWidth = videoTrack.displayWidth;\n    const originalHeight = videoTrack.displayHeight;\n\n    if (left + width > originalWidth || top + height > originalHeight) {\n      const clampedWidth = Math.min(width, originalWidth - left);\n      const clampedHeight = Math.min(height, originalHeight - top);\n      options.width = clampedWidth;\n      options.height = clampedHeight;\n    }\n\n    let format: OutputFormat;\n    const targetFormat =\n      outputFormat ||\n      (inputFormat.name.toLowerCase().includes(\"mp4\") ? \"mp4\" : \"webm\");\n\n    format =\n      targetFormat === \"mp4\"\n        ? new Mp4OutputFormat({ fastStart: \"in-memory\" })\n        : new WebMOutputFormat();\n\n    const target = new BufferTarget();\n    const output = new Output({ format, target });\n\n    const conversion = await Conversion.init({\n      input,\n      output,\n      video: {\n        crop: { left, top, width, height },\n        ...(videoBitrate && { bitrate: videoBitrate }),\n      },\n      ...(audioBitrate && { audio: { bitrate: audioBitrate } }),\n    });\n\n    if (!conversion.isValid) {\n      const reasons = conversion.discardedTracks\n        .map((t) => `${t.track.type}: ${t.reason}`)\n        .join(\", \");\n      throw new Error(`Cropping failed - ${reasons}`);\n    }\n\n    if (onProgress) conversion.onProgress = onProgress;\n\n    await conversion.execute();\n\n    if (!target.buffer) throw new Error(\"Cropping failed - no output buffer\");\n\n    return new Blob([target.buffer], {\n      type: targetFormat === \"mp4\" ? \"video/mp4\" : \"video/webm\",\n    });\n  }\n\n  async getDimensions(blob: Blob): Promise<{\n    width: number;\n    height: number;\n    codedWidth: number;\n    codedHeight: number;\n  }> {\n    const input = new Input({\n      formats: ALL_FORMATS,\n      source: new BlobSource(blob),\n    });\n\n    const tracks = await input.getTracks();\n    const videoTrack = tracks.find((t) => t.type === \"video\");\n\n    if (!videoTrack || !videoTrack.isVideoTrack()) throw new Error(\"No video track found\");\n\n    return {\n      width: videoTrack.displayWidth,\n      height: videoTrack.displayHeight,\n      codedWidth: videoTrack.codedWidth,\n      codedHeight: videoTrack.codedHeight,\n    };\n  }\n}\n\nexport const videoCropper = new VideoCropper();\n\nexport async function cropVideo(\n  blob: Blob,\n  left: number,\n  top: number,\n  width: number,\n  height: number,\n  options?: Omit<CropOptions, \"left\" | \"top\" | \"width\" | \"height\">\n): Promise<Blob> {\n  return videoCropper.crop(blob, { left, top, width, height, ...options });\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoCutter.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  BlobSource,\n  ALL_FORMATS,\n  Output,\n  BufferTarget,\n  Mp4OutputFormat,\n  QUALITY_HIGH,\n  VideoSampleSource,\n  AudioSampleSource,\n  VideoSampleSink,\n  AudioSampleSink,\n} from \"mediabunny\";\nimport { videoConverter } from \"./videoConverter\";\n\nexport class VideoCutter {\n  async cut(sourceBlob, { cutStart, cutEnd, onProgress }) {\n    const total = await this.getDuration(sourceBlob);\n\n    const input = new Input({\n      source: new BlobSource(sourceBlob),\n      formats: ALL_FORMATS,\n    });\n\n    const outputTarget = new BufferTarget();\n    const output = new Output({\n      format: new Mp4OutputFormat({ fastStart: \"in-memory\" }),\n      target: outputTarget,\n    });\n\n    const videoTrack = await input.getPrimaryVideoTrack();\n    const audioTrack = await input.getPrimaryAudioTrack().catch(() => null);\n\n    const codecInfo = await videoConverter.detectBestCodec(\"mp4\");\n    const videoCodec = codecInfo?.codec ?? \"avc\";\n    const videoSource = new VideoSampleSource({\n      codec: videoCodec,\n      bitrate: QUALITY_HIGH,\n      sizeChangeBehavior: \"passThrough\",\n    });\n    output.addVideoTrack(videoSource);\n\n    let audioSource = null;\n    if (audioTrack) {\n      audioSource = new AudioSampleSource({\n        codec: \"aac\",\n        bitrate: QUALITY_HIGH,\n      });\n      output.addAudioTrack(audioSource);\n    }\n\n    await output.start();\n\n    let outPts = 0;\n    const totalSegments = [\n      [0, cutStart],\n      [cutEnd, total],\n    ];\n    let processedTime = 0;\n\n    const addSegment = async (start, end) => {\n      const segDur = end - start;\n      const baseTimestamp = outPts / 1_000_000;\n\n      const videoSink = new VideoSampleSink(videoTrack);\n      for await (const sample of videoSink.samples(start, end)) {\n        const adjusted = Math.max(0, baseTimestamp + (sample.timestamp - start));\n        sample.setTimestamp(adjusted);\n        await videoSource.add(sample);\n        sample.close();\n        processedTime += sample.duration ?? 1 / 30;\n        if (onProgress) onProgress(processedTime / total);\n      }\n\n      if (audioTrack && audioSource) {\n        const audioSink = new AudioSampleSink(audioTrack);\n        for await (const sample of audioSink.samples(start, end)) {\n          const adjusted = Math.max(0, baseTimestamp + (sample.timestamp - start));\n          sample.setTimestamp(adjusted);\n          await audioSource.add(sample);\n          sample.close();\n          processedTime += sample.duration ?? 0;\n          if (onProgress) onProgress(processedTime / total);\n        }\n      }\n\n      outPts += segDur * 1_000_000;\n    };\n\n    for (let i = 0; i < totalSegments.length; i++) {\n      const [start, end] = totalSegments[i];\n      await addSegment(start, end);\n    }\n\n    await output.finalize();\n\n    const blob = new Blob([outputTarget.buffer], { type: \"video/mp4\" });\n    if (onProgress) onProgress(1);\n    return blob;\n  }\n\n  getDuration(blob) {\n    return new Promise((res, rej) => {\n      const v = document.createElement(\"video\");\n      v.src = URL.createObjectURL(blob);\n      v.onloadedmetadata = () => {\n        URL.revokeObjectURL(v.src);\n        res(v.duration);\n      };\n      v.onerror = () => rej(new Error(\"Failed to load video\"));\n    });\n  }\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoMuter.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  BlobSource,\n  Output,\n  BufferTarget,\n  Mp4OutputFormat,\n  VideoSampleSource,\n  VideoSampleSink,\n  AudioSampleSource,\n  AudioSample,\n  ALL_FORMATS,\n  QUALITY_HIGH,\n} from \"mediabunny\";\nimport { videoConverter } from \"./videoConverter\";\n\nexport class VideoMuter {\n  async mute(\n    videoBlob,\n    {\n      muteStart = 0,\n      muteEnd = 0,\n      videoVolume = 1.0,\n      outputFormat = \"mp4\",\n      onProgress,\n    } = {}\n  ) {\n    const videoDuration = await this._getDuration(videoBlob);\n\n    // Cap at 15 min to avoid OOM — re-encoding needs several copies in memory.\n    if (videoDuration > 900) {\n      throw new Error(\n        `Video is too long for in-browser audio muting (${Math.round(videoDuration / 60)} min). ` +\n          `Maximum supported length is 15 minutes.`\n      );\n    }\n\n    const input = new Input({\n      source: new BlobSource(videoBlob),\n      formats: ALL_FORMATS,\n    });\n\n    const outputTarget = new BufferTarget();\n    const output = new Output({\n      format: new Mp4OutputFormat({ fastStart: \"in-memory\" }),\n      target: outputTarget,\n    });\n\n    const videoTrack = await input.getPrimaryVideoTrack();\n    const videoSink = new VideoSampleSink(videoTrack);\n    const codecInfo = await videoConverter.detectBestCodec(\"mp4\");\n    const videoCodec = codecInfo?.codec ?? \"avc\";\n    const videoSource = new VideoSampleSource({\n      codec: videoCodec,\n      bitrate: QUALITY_HIGH,\n    });\n    output.addVideoTrack(videoSource);\n\n    let decodedAudio = null;\n    const ctx = new AudioContext();\n    try {\n      let buf = await videoBlob.arrayBuffer();\n      decodedAudio = await ctx.decodeAudioData(buf);\n      buf = null; // release compressed bytes; PCM is now in decodedAudio\n    } catch {\n      // decode failed; mute returns original blob below\n    } finally {\n      ctx.close().catch(() => {});\n    }\n\n    const sr = decodedAudio?.sampleRate || 48000;\n    const mixBuffer = new Float32Array(Math.ceil(sr * videoDuration));\n\n    if (decodedAudio) {\n      const channelData = decodedAudio.getChannelData(0);\n      const muteStartSample = Math.floor(muteStart * sr);\n      const muteEndSample = Math.floor(muteEnd * sr);\n\n      for (let i = 0; i < channelData.length; i++) {\n        mixBuffer[i] =\n          i >= muteStartSample && i < muteEndSample ? 0 : channelData[i] * videoVolume;\n      }\n    } else {\n      return videoBlob;\n    }\n\n    const audioSource = new AudioSampleSource({\n      codec: \"aac\",\n      bitrate: 128000,\n    });\n    output.addAudioTrack(audioSource);\n\n    await output.start();\n    for await (const frame of videoSink.samples(0, videoDuration)) {\n      await videoSource.add(frame);\n      frame.close();\n      if (onProgress) onProgress(frame.timestamp / videoDuration);\n    }\n\n    const totalSamples = Math.floor(videoDuration * sr);\n    const chunkSize = sr * 2;\n    let written = 0;\n\n    while (written < totalSamples) {\n      const slice = mixBuffer.slice(written, written + chunkSize);\n      const dur = slice.length / sr;\n      const sample = new AudioSample({\n        data: slice,\n        format: \"f32-planar\",\n        numberOfChannels: 1,\n        sampleRate: sr,\n        timestamp: written / sr,\n        duration: dur,\n      });\n      await audioSource.add(sample);\n      sample.close();\n      written += chunkSize;\n    }\n\n    await output.finalize();\n    // Always MP4 container — use correct MIME type.\n    return new Blob([outputTarget.buffer], { type: \"video/mp4\" });\n  }\n\n  async _getDuration(blob) {\n    return new Promise((resolve, reject) => {\n      const v = document.createElement(\"video\");\n      v.src = URL.createObjectURL(blob);\n      v.onloadedmetadata = () => {\n        resolve(v.duration);\n        URL.revokeObjectURL(v.src);\n      };\n      v.onerror = reject;\n    });\n  }\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/mediabunny/lib/videoTrimmer.ts",
    "content": "// @ts-nocheck\nimport {\n  Input,\n  Output,\n  BlobSource,\n  BufferTarget,\n  Mp4OutputFormat,\n  WebMOutputFormat,\n  ALL_FORMATS,\n  Conversion,\n  type OutputFormat,\n} from \"mediabunny\";\n\nexport interface TrimOptions {\n  startTime: number;\n  endTime: number;\n  outputFormat?: \"mp4\" | \"webm\";\n  videoBitrate?: number;\n  audioBitrate?: number;\n  onProgress?: (progress: number) => void;\n}\n\nexport interface TrimInfo {\n  originalDuration: number;\n  trimmedDuration: number;\n  startTime: number;\n  endTime: number;\n  format: string;\n}\n\nexport class VideoTrimmer {\n  async trim(sourceBlob: Blob, options: TrimOptions): Promise<Blob> {\n    const { startTime, endTime, outputFormat, videoBitrate, audioBitrate, onProgress } = options;\n\n    if (startTime < 0) throw new Error(\"Start time cannot be negative\");\n    if (endTime <= startTime) throw new Error(\"End time must be greater than start time\");\n\n    const input = new Input({\n      formats: ALL_FORMATS,\n      source: new BlobSource(sourceBlob),\n    });\n\n    const inputFormat = await input.getFormat();\n    const targetFormat =\n      outputFormat ||\n      (inputFormat.name.toLowerCase().includes(\"mp4\") ? \"mp4\" : \"webm\");\n\n    const format: OutputFormat =\n      targetFormat === \"mp4\"\n        ? new Mp4OutputFormat({ fastStart: \"in-memory\" })\n        : new WebMOutputFormat();\n\n    const target = new BufferTarget();\n    const output = new Output({ format, target });\n\n    const conversion = await Conversion.init({\n      input,\n      output,\n      trim: { start: startTime, end: endTime },\n      ...(videoBitrate && { video: { bitrate: videoBitrate } }),\n      ...(audioBitrate && { audio: { bitrate: audioBitrate } }),\n    });\n\n    if (!conversion.isValid) {\n      const reasons = conversion.discardedTracks\n        .map((t) => `${t.track.type}: ${t.reason}`)\n        .join(\", \");\n      throw new Error(`Trimming failed - ${reasons}`);\n    }\n\n    if (onProgress) conversion.onProgress = onProgress;\n\n    await conversion.execute();\n\n    if (!target.buffer) throw new Error(\"Trimming failed - no output buffer\");\n\n    return new Blob([target.buffer], {\n      type: targetFormat === \"mp4\" ? \"video/mp4\" : \"video/webm\",\n    });\n  }\n\n  async getInfo(blob: Blob): Promise<{\n    format: string;\n    hasVideo: boolean;\n    hasAudio: boolean;\n    videoCodec?: string | null;\n    audioCodec?: string | null;\n    codedWidth?: number;\n    codedHeight?: number;\n  }> {\n    const input = new Input({\n      formats: ALL_FORMATS,\n      source: new BlobSource(blob),\n    });\n\n    const format = await input.getFormat();\n    const tracks = await input.getTracks();\n    const videoTrack = tracks.find((t) => t.type === \"video\");\n    const audioTrack = tracks.find((t) => t.type === \"audio\");\n\n    return {\n      format: format.name,\n      hasVideo: !!videoTrack,\n      hasAudio: !!audioTrack,\n      videoCodec: videoTrack?.codec || null,\n      audioCodec: audioTrack?.codec || null,\n      ...(videoTrack?.isVideoTrack() && {\n        codedWidth: videoTrack.codedWidth,\n        codedHeight: videoTrack.codedHeight,\n      }),\n    };\n  }\n}\n\nexport const videoTrimmer = new VideoTrimmer();\n\nexport async function trimVideo(\n  blob: Blob,\n  startTime: number,\n  endTime: number,\n  options?: Omit<TrimOptions, \"startTime\" | \"endTime\">\n): Promise<Blob> {\n  return videoTrimmer.trim(blob, { startTime, endTime, ...options });\n}"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/addAudioToVideo.js",
    "content": "import { VideoAudioMixer } from \"../mediabunny/lib/videoAudioMixer.ts\";\n\nasync function ensureBlob(input, mimeType = \"video/webm\") {\n  if (input instanceof Blob) return input;\n\n  if (input && typeof input === \"object\" && typeof input.size === \"number\") {\n    if (typeof input.arrayBuffer === \"function\") {\n      try {\n        const buffer = await input.arrayBuffer();\n        return new Blob([buffer], { type: input.type || mimeType });\n      } catch {}\n    }\n\n    if (typeof input.slice === \"function\") {\n      try {\n        const sliced = input.slice(0, input.size, input.type || mimeType);\n        if (sliced instanceof Blob) return sliced;\n      } catch {}\n    }\n  }\n\n  throw new Error(\n    `Cannot convert to Blob: ${typeof input}, constructor: ${\n      input?.constructor?.name\n    }`\n  );\n}\n\nasync function addAudioToVideo(\n  ffmpeg,\n  videoBlob,\n  audioBlob,\n  videoDuration,\n  audioVolume = 1.0,\n  replaceAudio = false,\n  onProgress\n) {\n  const video = await ensureBlob(videoBlob, \"video/webm\");\n  const audio = await ensureBlob(audioBlob, \"audio/webm\");\n\n  const mixer = new VideoAudioMixer();\n  return mixer.addAudio(video, audio, {\n    mode: replaceAudio ? \"replace\" : \"mix\",\n    videoVolume: replaceAudio ? 0 : 0.7,\n    audioVolume,\n    loop: false,\n    outputFormat: \"webm\",\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default addAudioToVideo;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/blobToArrayBuffer.js",
    "content": "async function blobToArrayBuffer(blob) {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onloadend = () => {\n      if (reader.result instanceof ArrayBuffer) {\n        resolve(reader.result);\n      } else {\n        reject(new Error(\"Failed to convert Blob to ArrayBuffer\"));\n      }\n    };\n    reader.onerror = reject;\n    reader.readAsArrayBuffer(blob);\n  });\n}\n\nexport default blobToArrayBuffer;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/convertMp4ToWebm.js",
    "content": "import { VideoConverter } from \"../mediabunny/lib/videoConverter.ts\";\n\nasync function convertMp4ToWebm(mp4Blob, onProgress = () => {}) {\n  const converter = new VideoConverter();\n\n  return converter.convertToWebM(mp4Blob, {\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default convertMp4ToWebm;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/convertWebmToMp4.js",
    "content": "import { VideoConverter } from \"../mediabunny/lib/videoConverter.ts\";\n\nasync function convertWebmToMp4(webmBlob, onProgress = () => {}) {\n  const converter = new VideoConverter();\n\n  return converter.convertToMP4(webmBlob, {\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default convertWebmToMp4;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/cropVideo.js",
    "content": "import { VideoCropper } from \"../mediabunny/lib/videoCropper.ts\";\n\nasync function cropVideo(\n  ffmpeg,\n  videoBlob,\n  cropOptions,\n  onProgress = () => {}\n) {\n  const cropper = new VideoCropper();\n\n  return cropper.crop(videoBlob, {\n    left: cropOptions.x,\n    top: cropOptions.y,\n    width: cropOptions.width,\n    height: cropOptions.height,\n    outputFormat: \"webm\",\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default cropVideo;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/cutVideo.js",
    "content": "import { VideoTrimmer } from \"../mediabunny/lib/videoTrimmer.ts\";\nimport { VideoCutter } from \"../mediabunny/lib/videoCutter.ts\";\n\nexport default async function cutVideo(\n  ffmpeg,\n  videoBlob,\n  startTime,\n  endTime,\n  cut,\n  duration,\n  encode,\n  onProgress = () => {}\n) {\n  let result;\n\n  if (cut) {\n    const cutter = new VideoCutter();\n    result = await cutter.cut(videoBlob, {\n      cutStart: startTime,\n      cutEnd: endTime,\n      outputFormat: \"mp4\",\n      videoBitrate: 5_000_000,\n      audioBitrate: 128_000,\n      onProgress,\n    });\n  } else {\n    const trimmer = new VideoTrimmer();\n    result = await trimmer.trim(videoBlob, {\n      startTime,\n      endTime,\n      outputFormat: \"mp4\",\n      videoBitrate: 5_000_000,\n      audioBitrate: 128_000,\n      onProgress,\n    });\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/getFrame.js",
    "content": "async function getFrame(ffmpeg, videoBlob, time) {\n  return new Promise((resolve, reject) => {\n    const video = document.createElement(\"video\");\n    const canvas = document.createElement(\"canvas\");\n    const ctx = canvas.getContext(\"2d\");\n\n    video.addEventListener(\"loadedmetadata\", () => {\n      canvas.width = video.videoWidth;\n      canvas.height = video.videoHeight;\n      video.currentTime = time;\n    });\n\n    video.addEventListener(\"seeked\", () => {\n      try {\n        ctx.drawImage(video, 0, 0);\n        canvas.toBlob((blob) => {\n          URL.revokeObjectURL(video.src);\n          if (blob) resolve(blob);\n          else reject(new Error(\"Failed to create blob from canvas\"));\n        }, \"image/png\");\n      } catch (error) {\n        URL.revokeObjectURL(video.src);\n        reject(error);\n      }\n    });\n\n    video.addEventListener(\"error\", (e) => {\n      URL.revokeObjectURL(video.src);\n      reject(new Error(`Video error: ${e.message || \"Unknown error\"}`));\n    });\n\n    video.src = URL.createObjectURL(videoBlob);\n  });\n}\n\nexport default getFrame;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/hasAudio.js",
    "content": "async function hasAudio(ffmpeg, videoBlob) {\n  return new Promise((resolve, reject) => {\n    const video = document.createElement(\"video\");\n    video.muted = true;\n\n    video.addEventListener(\"loadedmetadata\", () => {\n      if (typeof video.mozHasAudio !== \"undefined\") {\n        const result = video.mozHasAudio;\n        URL.revokeObjectURL(video.src);\n        resolve(result);\n        return;\n      }\n\n      if (typeof video.webkitAudioDecodedByteCount !== \"undefined\") {\n        const result = video.webkitAudioDecodedByteCount > 0;\n        URL.revokeObjectURL(video.src);\n        resolve(result);\n        return;\n      }\n\n      const audioContext = new (window.AudioContext ||\n        window.webkitAudioContext)();\n\n      video\n        .play()\n        .then(() => {\n          const source = audioContext.createMediaElementSource(video);\n          const analyser = audioContext.createAnalyser();\n          source.connect(analyser);\n\n          const dataArray = new Uint8Array(analyser.frequencyBinCount);\n          analyser.getByteFrequencyData(dataArray);\n\n          const hasAudioData = dataArray.some((v) => v > 0);\n\n          video.pause();\n          URL.revokeObjectURL(video.src);\n          audioContext.close();\n\n          resolve(hasAudioData);\n        })\n        .catch(() => {\n          URL.revokeObjectURL(video.src);\n          audioContext.close();\n          resolve(true);\n        });\n    });\n\n    video.addEventListener(\"error\", (e) => {\n      URL.revokeObjectURL(video.src);\n      reject(new Error(`Video error: ${e.message || \"Unknown error\"}`));\n    });\n\n    video.src = URL.createObjectURL(videoBlob);\n    video.load();\n  });\n}\n\nexport default hasAudio;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/muteVideo.js",
    "content": "import { VideoMuter } from \"../mediabunny/lib/videoMuter.ts\";\n\nasync function muteVideo(\n  ffmpeg,\n  videoBlob,\n  startTime,\n  endTime,\n  duration,\n  onProgress = () => {}\n) {\n  const muter = new VideoMuter();\n\n  return muter.mute(videoBlob, {\n    muteStart: startTime,\n    muteEnd: endTime,\n    outputFormat: \"webm\",\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default muteVideo;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/reencodeVideo.js",
    "content": "import { VideoConverter } from \"../mediabunny/lib/videoConverter.ts\";\n\nasync function reencodeVideo(\n  ffmpeg,\n  videoBlob,\n  duration,\n  onProgress = () => {}\n) {\n  const converter = new VideoConverter();\n\n  return converter.convertToMP4(videoBlob, {\n    videoBitrate: 5_000_000,\n    audioBitrate: 128_000,\n    onProgress,\n  });\n}\n\nexport default reencodeVideo;\n"
  },
  {
    "path": "src/pages/EditorWebCodecs/utils/toGIF.js",
    "content": "import GIF from \"gif.js\";\n\nasync function toGIF(ffmpeg, videoBlob, onProgress = () => {}) {\n  return new Promise((resolve, reject) => {\n    const video = document.createElement(\"video\");\n    const canvas = document.createElement(\"canvas\");\n    const ctx = canvas.getContext(\"2d\");\n\n    video.addEventListener(\"loadedmetadata\", async () => {\n      try {\n        const duration = video.duration;\n        const width = 540;\n        const height = Math.round(\n          (video.videoHeight / video.videoWidth) * width\n        );\n        const fps = 12;\n        const quality = 5;\n\n        canvas.width = width;\n        canvas.height = height;\n\n        const gif = new GIF({\n          workers: 2,\n          quality,\n          width,\n          height,\n          workerScript: \"/assets/vendor/gif.js/gif.worker.js\",\n        });\n\n        const frameInterval = 1 / fps;\n        const totalFrames = Math.floor(duration * fps);\n        let frameCount = 0;\n\n        const captureFrame = (time) =>\n          new Promise((resolveFrame) => {\n            const seekHandler = () => {\n              video.removeEventListener(\"seeked\", seekHandler);\n              ctx.drawImage(video, 0, 0, width, height);\n              gif.addFrame(canvas, {\n                copy: true,\n                delay: Math.round(1000 / fps),\n              });\n              frameCount++;\n              onProgress(frameCount / totalFrames);\n              resolveFrame();\n            };\n            video.addEventListener(\"seeked\", seekHandler);\n            video.currentTime = time;\n          });\n\n        for (let i = 0; i < totalFrames; i++) {\n          const time = Math.min(i * frameInterval, duration - 0.001);\n          await captureFrame(time);\n        }\n\n        gif.on(\"finished\", (blob) => {\n          URL.revokeObjectURL(video.src);\n          video.remove();\n          onProgress(1);\n          resolve(blob);\n        });\n\n        gif.on(\"progress\", (progress) => onProgress(progress));\n\n        gif.render();\n      } catch (error) {\n        URL.revokeObjectURL(video.src);\n        video.remove();\n        reject(error);\n      }\n    });\n\n    video.addEventListener(\"error\", (e) => {\n      URL.revokeObjectURL(video.src);\n      reject(new Error(`Video error: ${e.message || \"Unknown error\"}`));\n    });\n\n    video.src = URL.createObjectURL(videoBlob);\n    video.load();\n  });\n}\n\nexport default toGIF;\n"
  },
  {
    "path": "src/pages/Permissions/Permissions.jsx",
    "content": "import React, { useEffect, useState, useRef, useCallback } from \"react\";\n\nconst Recorder = () => {\n  useEffect(() => {\n    window.parent.postMessage(\n      {\n        type: \"screenity-permissions-loaded\",\n      },\n      \"*\"\n    );\n  }, []);\n\n  useEffect(() => {\n    const handleDeviceChange = () => {\n      // Recheck permissions and enumerate devices\n      checkPermissions();\n    };\n\n    navigator.mediaDevices.addEventListener(\"devicechange\", handleDeviceChange);\n\n    return () => {\n      navigator.mediaDevices.removeEventListener(\n        \"devicechange\",\n        handleDeviceChange\n      );\n    };\n  }, []);\n\n  const checkPermissions = async () => {\n    // Individually check the camera and microphone permissions using the Permissions API. Then enumerate devices respectively.\n    try {\n      const cameraPermission = await navigator.permissions.query({\n        name: \"camera\",\n      });\n      const microphonePermission = await navigator.permissions.query({\n        name: \"microphone\",\n      });\n\n      cameraPermission.onchange = () => {\n        checkPermissions();\n      };\n\n      microphonePermission.onchange = () => {\n        checkPermissions();\n      };\n\n      // If the permissions are granted, enumerate devices\n      if (\n        cameraPermission.state === \"granted\" ||\n        microphonePermission.state === \"granted\"\n      ) {\n        enumerateDevices(\n          cameraPermission.state === \"granted\",\n          microphonePermission.state === \"granted\"\n        );\n      } else {\n        // Post message to parent window\n        window.parent.postMessage(\n          {\n            type: \"screenity-permissions\",\n            success: false,\n            error: err.name,\n          },\n          \"*\"\n        );\n        // sendResponse({ success: false, error: err.name });\n      }\n    } catch (err) {\n      enumerateDevices();\n    }\n  };\n\n  const enumerateDevices = async (camGranted = true, micGranted = true) => {\n    try {\n      const stream = await navigator.mediaDevices.getUserMedia({\n        audio: micGranted,\n        video: camGranted,\n      });\n\n      const devicesInfo = await navigator.mediaDevices.enumerateDevices();\n\n      let audioinput = [];\n      let audiooutput = [];\n      let videoinput = [];\n\n      if (micGranted) {\n        // Filter by audio input\n        audioinput = devicesInfo\n          .filter((device) => device.kind === \"audioinput\")\n          .map((device) => ({\n            deviceId: device.deviceId,\n            label: device.label,\n          }));\n\n        // Filter by audio output and extract relevant properties\n        audiooutput = devicesInfo\n          .filter((device) => device.kind === \"audiooutput\")\n          .map((device) => ({\n            deviceId: device.deviceId,\n            label: device.label,\n          }));\n      }\n\n      if (camGranted) {\n        // Filter by video input and extract relevant properties\n        videoinput = devicesInfo\n          .filter((device) => device.kind === \"videoinput\")\n          .map((device) => ({\n            deviceId: device.deviceId,\n            label: device.label,\n          }));\n      }\n\n      // Save in Chrome local storage\n      chrome.storage.local.set({\n        // Set available devices\n        audioinput: audioinput,\n        audiooutput: audiooutput,\n        videoinput: videoinput,\n        cameraPermission: camGranted,\n        microphonePermission: micGranted,\n      });\n\n      // Post message to parent window\n      window.parent.postMessage(\n        {\n          type: \"screenity-permissions\",\n          success: true,\n          audioinput: audioinput,\n          audiooutput: audiooutput,\n          videoinput: videoinput,\n          cameraPermission: camGranted,\n          microphonePermission: micGranted,\n        },\n        \"*\"\n      );\n\n      //sendResponse({ success: true, audioinput, audiooutput, videoinput });\n\n      // End the stream\n      stream.getTracks().forEach(function (track) {\n        track.stop();\n      });\n    } catch (err) {\n      // Post message to parent window\n      window.parent.postMessage(\n        {\n          type: \"screenity-permissions\",\n          success: false,\n          error: err.name,\n        },\n        \"*\"\n      );\n      //sendResponse({ success: false, error: err.name });\n    }\n  };\n\n  const onMessage = (message) => {\n    if (message.type === \"screenity-get-permissions\") {\n      checkPermissions();\n    }\n  };\n\n  // Post message listener\n  useEffect(() => {\n    window.addEventListener(\"message\", (event) => {\n      onMessage(event.data);\n    });\n  }, []);\n\n  return <div></div>;\n};\n\nexport default Recorder;\n"
  },
  {
    "path": "src/pages/Permissions/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Permissions/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Recorder from \"./Permissions\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Recorder />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Playground/Setup.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\nconst Setup = () => {\n  useEffect(() => {\n    // Inject content script\n    const script = document.createElement(\"script\");\n    script.src = chrome.runtime.getURL(\"contentScript.bundle.js\");\n    script.async = true;\n    document.body.appendChild(script);\n\n    // Also inject CSS\n    const style = document.createElement(\"link\");\n    style.rel = \"stylesheet\";\n    style.type = \"text/css\";\n    style.href = chrome.runtime.getURL(\"assets/fonts/fonts.css\");\n    document.body.appendChild(style);\n\n    // Return\n    return () => {\n      document.body.removeChild(script);\n      document.body.removeChild(style);\n    };\n  }, []);\n\n  return (\n    <div className=\"setupBackground\">\n      <img\n        src={chrome.runtime.getURL(\"assets/logo-text.svg\")}\n        className=\"setupLogo\"\n      />\n      <div className=\"setupBackgroundSVG\"></div>\n      <style>\n        {`\n\t\t\t\tbody {\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\tmargin: 0px;\n\t\t\t\t}\n\n\t\t\t\t.setupInfo {\n\t\t\t\t\tmargin-top: 20px;\n\t\t\t\t}\n\t\t\t\ta {\n\t\t\t\t\ttext-decoration: none!important;\n\t\t\t\t\tcolor: #4C7DE2;\n\t\t\t\t}\n\t\t\t\t.setupBackgroundSVG {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground: url('` +\n          chrome.runtime.getURL(\"assets/helper/pattern-svg.svg\") +\n          `') repeat;\n\t\t\t\t\tbackground-size: 62px 23.5px;\n\t\t\t\t\tanimation: moveBackground 138s linear infinite;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes moveBackground {\n\t\t\t\t\t0% {\n\t\t\t\t\t\tbackground-position: 0 0;\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\tbackground-position: 100% 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\n\t\t\t\t.setupLogo {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tbottom: 30px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\twidth: 120px;\n\t\t\t\t}\n\n\n\t\t\t\t.setupBackground {\n\t\t\t\t\tbackground-color: #f5f5f5;\n\t\t\t\t\theight: 100vh;\n\t\t\t\t\twidth: 100vw;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupContainer {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tbottom: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\tz-index: 999;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\twidth: 60%;\n\t\t\t\t\theight: fit-content;\n\t\t\t\t\tbackground-color: #fff;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tpadding: 50px 50px;\n\t\t\t\t\tgap: 80px;\n\t\t\t\t\tfont-family: 'Satoshi-Medium', sans-serif;\n\t\t\t\t}\n\n\t\t\t\t.setupImage {\n\t\t\t\t\twidth: 70%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupImage img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t}\n\n\t\t\t\t.setupText {\n\t\t\t\t\twidth: 50%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\ttext-align: left;\n\t\t\t\t}\n\n\t\t\t\t.setupEmoji {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t}\n\n\t\t\t\t.setupTitle {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tcolor: #29292F;\n\t\t\t\t}\n\n\t\t\t\t.setupDescription {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t}\n\n\t\t\t\t.setupStep {\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t}\n\n\t\t\t\t.setupStep span {\n\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\twidth: 20px;\n\t\t\t\t\theight: 20px;\n\t\t\t\t\tpadding: 2px;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tdisplay: inline-flex;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\tmargin-left: 3px;\n\t\t\t\t\tmargin-right: 3px;\n\t\t\t\t\tbackground-color: #F4F2F2;\n\t\t\t\t}\n\n\t\t\t\t.setupStep img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\n\t\t\t\t.center {\n\t\t\t\t\ttext-align: center!important;\n\t\t\t\t}\n\t\t\t\t.setupText.center {\n\t\t\t\t\twidth: auto!important;\n\t\t\t\t}\n\t\t\t\t.setupContainer.center {\n\t\t\t\t\twidth: 40%!important;\n\t\t\t\t}\n\t\t\t\t\n\n\n\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default Setup;\n"
  },
  {
    "path": "src/pages/Playground/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Screenity - Playground</title>\n\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Gloria-Hallelujah;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular.ttf)\n          format(\"truetype\");\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Playground/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Setup from \"./Setup\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Setup />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Recorder/Recorder.jsx",
    "content": "import React, { useEffect, useState, useRef } from \"react\";\nimport localforage from \"localforage\";\nimport RecorderUI from \"./RecorderUI\";\nimport { createMediaRecorder } from \"./mediaRecorderUtils\";\nimport { sendRecordingError, sendStopRecording } from \"./messaging\";\nimport { getBitrates, getResolutionForQuality } from \"./recorderConfig\";\nimport { WebCodecsRecorder } from \"./webcodecs/WebCodecsRecorder\";\nimport { getUserMediaWithFallback } from \"../utils/mediaDeviceFallback\";\nimport {\n  debugRecordingEvent,\n  resetRecordingDebugSession,\n  isRecordingDebugEnabled,\n  hydrateRecordingDebugFlag,\n} from \"../utils/recordingDebug\";\nimport {\n  probeFastRecorderSupport,\n  shouldUseFastRecorder,\n  getFastRecorderStickyState,\n  markFastRecorderFailure,\n  validateFastRecorderOutputBlob,\n} from \"../../media/fastRecorderGate\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\nconst chunksStore = localforage.createInstance({\n  name: \"chunks\",\n});\n\ndocument.body.style.willChange = \"contents\";\n\n// Debug flag for logging\n//   window.SCREENITY_DEBUG_RECORDER = true;\nconst DEBUG_RECORDER =\n  typeof window !== \"undefined\" ? !!window.SCREENITY_DEBUG_RECORDER : false;\nconst FORCE_MEDIARECORDER =\n  typeof window !== \"undefined\"\n    ? !!window.SCREENITY_FORCE_MEDIARECORDER\n    : false;\nconst logPrefix = \"[Screenity Recorder]\";\n\nfunction debug(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.log(logPrefix, ...args);\n}\n\nfunction debugWarn(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.warn(logPrefix, ...args);\n}\n\nfunction debugError(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.error(logPrefix, ...args);\n}\n\n// Stream lifecycle ring-buffer — persisted to storage, survives tab discards.\nconst SL_KEY = \"streamLifecycleLog\";\nconst SL_MAX = 40; // keep last N entries\nconst _slBuffer = [];\n\nfunction slLog(tag, extra = {}) {\n  const entry = { t: Date.now(), tag, ...extra };\n  if (DEBUG_RECORDER) {\n    // eslint-disable-next-line no-console\n    console.log(\"[Screenity:SL]\", tag, entry);\n  }\n  _slBuffer.push(entry);\n  if (_slBuffer.length > SL_MAX) _slBuffer.splice(0, _slBuffer.length - SL_MAX);\n  // Fire-and-forget persist.\n  try {\n    chrome.storage.local.set({ [SL_KEY]: [..._slBuffer] });\n  } catch { /* storage unavailable — tab possibly being torn down */ }\n}\n\nfunction logCaptureContext(label, stream) {\n  if (!DEBUG_RECORDER && !isRecordingDebugEnabled()) return;\n  const videoTracks =\n    stream && typeof stream.getVideoTracks === \"function\"\n      ? stream.getVideoTracks()\n      : [];\n\n  debug(`${label} environment`, {\n    devicePixelRatio: window.devicePixelRatio,\n    screen: { width: window.screen?.width, height: window.screen?.height },\n    inner: { width: window.innerWidth, height: window.innerHeight },\n  });\n\n  videoTracks.forEach((track, index) => {\n    const settings =\n      typeof track.getSettings === \"function\" ? track.getSettings() : {};\n    const constraints =\n      typeof track.getConstraints === \"function\" ? track.getConstraints() : {};\n    const capabilities =\n      typeof track.getCapabilities === \"function\"\n        ? track.getCapabilities()\n        : {};\n    debug(`${label} videoTrack[${index}]`, {\n      label: track.label,\n      settings,\n      constraints,\n      capabilities,\n    });\n  });\n}\n\nfunction buildTrackSnapshot(track) {\n  if (!track) return null;\n  const settings =\n    typeof track.getSettings === \"function\" ? track.getSettings() : {};\n  const constraints =\n    typeof track.getConstraints === \"function\" ? track.getConstraints() : {};\n  const capabilities =\n    typeof track.getCapabilities === \"function\" ? track.getCapabilities() : {};\n  return {\n    label: track.label,\n    settings,\n    constraints,\n    capabilities,\n  };\n}\n\nfunction logRecordingSnapshot(label, data) {\n  if (!DEBUG_RECORDER && !isRecordingDebugEnabled()) return;\n  debug(`Recording snapshot: ${label}`, data);\n}\n\nconst clamp = (value, min, max) => Math.max(min, Math.min(max, value));\n\nconst QUALITY_ORDER = [\"240p\", \"360p\", \"480p\", \"720p\", \"1080p\", \"4k\"];\n\nconst clampQualityValue = (value, maxValue) => {\n  const current = QUALITY_ORDER.includes(value) ? value : \"1080p\";\n  const max = QUALITY_ORDER.includes(maxValue) ? maxValue : \"1080p\";\n  return QUALITY_ORDER.indexOf(current) <= QUALITY_ORDER.indexOf(max)\n    ? current\n    : max;\n};\n\nconst getFreeCaptureCaps = async () => {\n  try {\n    const { isLoggedIn, isSubscribed } = await chrome.storage.local.get([\n      \"isLoggedIn\",\n      \"isSubscribed\",\n    ]);\n    const isPro = Boolean(isLoggedIn && isSubscribed);\n    return {\n      isPro,\n      maxQuality: \"1080p\",\n      maxFps: 60,\n    };\n  } catch {\n    return {\n      isPro: false,\n      maxQuality: \"1080p\",\n      maxFps: 60,\n    };\n  }\n};\n\nconst computeTargetVideoBps = (width, height, fps) => {\n  const pixels = Number(width) * Number(height);\n  const rate = Number.isFinite(fps) && fps > 0 ? fps : 30;\n  const target = Math.round(pixels * rate * 0.1);\n  return clamp(target, 6_000_000, 24_000_000);\n};\n\nconst selectMimeType = (preferredCodec) => {\n  const preferred = (preferredCodec || \"\").toLowerCase();\n  const mimeTypes = [\n    \"video/webm;codecs=vp9,opus\",\n    \"video/webm;codecs=vp9\",\n    \"video/webm;codecs=vp8,opus\",\n    \"video/webm;codecs=vp8\",\n    \"video/webm;codecs=avc1\",\n    \"video/webm;codecs=h264\",\n    \"video/webm\",\n  ];\n  const ordered = preferred\n    ? mimeTypes\n        .filter((type) => type.includes(preferred))\n        .concat(mimeTypes.filter((type) => !type.includes(preferred)))\n    : mimeTypes;\n  return ordered.find((type) => MediaRecorder.isTypeSupported(type)) || null;\n};\n\nconst getCodecLabel = (mimeType) => {\n  if (!mimeType) return \"unknown\";\n  if (mimeType.includes(\"vp9\")) return \"vp9\";\n  if (mimeType.includes(\"vp8\")) return \"vp8\";\n  if (mimeType.includes(\"avc1\") || mimeType.includes(\"h264\")) return \"h264\";\n  return \"unknown\";\n};\n\nconst Recorder = () => {\n  const isRestarting = useRef(false);\n  const pendingStartAfterRestart = useRef(false);\n  const recordingGeneration = useRef(0);\n  const isFinishing = useRef(false);\n  const sentLast = useRef(false);\n  const lastTimecode = useRef(0);\n  const hasChunks = useRef(false);\n\n  const recdbgSessionRef = useRef(null);\n\n  const lastSize = useRef(0);\n  const index = useRef(0);\n\n  const [started, setStarted] = useState(false);\n  const streamReadyAt = useRef(null);\n\n  // Start gate: defers startRecording() until the stream is ready.\n  const startRequested = useRef(false);\n  const startRequestedAt = useRef(null);\n  // Timeout if the stream never arrives\n  const startGateTimeout = useRef(null);\n  const START_GATE_TIMEOUT_MS = 8000; // max wait for stream after start request\n\n  const liveStream = useRef(null);\n\n  const helperVideoStream = useRef(null);\n  const helperAudioStream = useRef(null);\n\n  const aCtx = useRef(null);\n  const destination = useRef(null);\n  const audioInputSource = useRef(null);\n  const audioOutputSource = useRef(null);\n  const audioInputGain = useRef(null);\n  const audioOutputGain = useRef(null);\n\n  const recorder = useRef(null);\n  const useWebCodecs = useRef(false);\n\n  const isTab = useRef(false);\n  const tabID = useRef(null);\n  const tabPreferred = useRef(false);\n\n  const backupRef = useRef(false);\n\n  const pending = useRef([]);\n  const draining = useRef(false);\n  const lowStorageAbort = useRef(false);\n  const savedCount = useRef(0);\n\n  const lastEstimateAt = useRef(0);\n  const ESTIMATE_INTERVAL_MS = 5000;\n  const MIN_HEADROOM = 25 * 1024 * 1024;\n  const WARN_HEADROOM = 100 * 1024 * 1024; // early warning before hard abort\n  const MAX_PENDING_BYTES = 8 * 1024 * 1024;\n  const pendingBytes = useRef(0);\n  const lowStorageWarned = useRef(false);\n\n  const uiClosing = useRef(false);\n\n  const isRecording = useRef(false);\n  const isStarting = useRef(false);\n  const pausedStateRef = useRef(false);\n  const stopSignalSent = useRef(false);\n\n  // Keep-alive mechanism to prevent Chrome from freezing this background tab\n  const keepAliveAudioCtx = useRef(null);\n  const keepAliveOscillator = useRef(null);\n\n  const recordingStartTime = useRef(null);\n  const sessionHeartbeat = useRef(null);\n  const recordingTick = useRef(null);\n\n  debug(\"Recorder component mounted\");\n  slLog(\"component-mount\");\n\n  const setRecordingTimingState = async (nextState) => {\n    try {\n      await chrome.storage.local.set(nextState);\n    } catch (err) {\n      debugWarn(\"Failed to persist recording timing state\", err);\n    }\n  };\n\n  async function canFitChunk(byteLength) {\n    const now = performance.now();\n    if (now - lastEstimateAt.current < ESTIMATE_INTERVAL_MS) {\n      return !lowStorageAbort.current;\n    }\n    lastEstimateAt.current = now;\n\n    try {\n      const { usage = 0, quota = 0 } = await navigator.storage.estimate();\n      const remaining = quota - usage;\n      const ok = remaining > MIN_HEADROOM + (byteLength || 0);\n      if (DEBUG_RECORDER) {\n        debug(\"Storage estimate\", {\n          usage,\n          quota,\n          remaining,\n          byteLength,\n          ok,\n        });\n      }\n      // Early warning before hard abort\n      if (!lowStorageWarned.current && remaining < WARN_HEADROOM) {\n        lowStorageWarned.current = true;\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageLow\"),\n          timeout: 8000,\n        }).catch(() => {});\n      }\n      return ok;\n    } catch (err) {\n      debugWarn(\"navigator.storage.estimate() failed, assuming OK\", err);\n      return !lowStorageAbort.current;\n    }\n  }\n\n  /**\n   * Start silent audio playback to prevent Chrome from freezing this tab.\n   * Chrome throttles/freezes background tabs after ~5 minutes of inactivity,\n   * which would stop our recording. Playing silent audio keeps the tab active.\n   */\n  const startTabKeepAlive = () => {\n    try {\n      if (keepAliveAudioCtx.current) return; // Already running\n\n      const ctx = new (window.AudioContext || window.webkitAudioContext)();\n      keepAliveAudioCtx.current = ctx;\n\n      // Create a silent oscillator (inaudible frequency at zero gain)\n      const oscillator = ctx.createOscillator();\n      const gainNode = ctx.createGain();\n\n      oscillator.frequency.value = 0; // DC signal (no audible tone)\n      gainNode.gain.value = 0; // Completely silent\n\n      oscillator.connect(gainNode);\n      gainNode.connect(ctx.destination);\n      oscillator.start();\n\n      keepAliveOscillator.current = oscillator;\n\n      // Request Chrome not to discard this tab\n      chrome.runtime\n        .sendMessage({ type: \"set-tab-auto-discardable\", discardable: false })\n        .catch(() => {});\n\n      debug(\"Tab keep-alive started\");\n    } catch (err) {\n      debugWarn(\"Failed to start tab keep-alive:\", err);\n    }\n  };\n\n  /**\n   * Stop the silent audio playback when recording ends.\n   */\n  const stopTabKeepAlive = () => {\n    try {\n      if (keepAliveOscillator.current) {\n        keepAliveOscillator.current.stop();\n        keepAliveOscillator.current.disconnect();\n        keepAliveOscillator.current = null;\n      }\n      if (keepAliveAudioCtx.current) {\n        keepAliveAudioCtx.current.close();\n        keepAliveAudioCtx.current = null;\n      }\n\n      // Allow Chrome to discard this tab again if needed\n      chrome.runtime\n        .sendMessage({ type: \"set-tab-auto-discardable\", discardable: true })\n        .catch(() => {});\n\n      debug(\"Tab keep-alive stopped\");\n    } catch (err) {\n      debugWarn(\"Failed to stop tab keep-alive:\", err);\n    }\n  };\n\n  /**\n   * Persist recording session state for recovery and diagnostics.\n   */\n  const persistSessionState = async (status = \"recording\") => {\n    try {\n      await chrome.storage.local.set({\n        freeRecorderSession: {\n          status,\n          chunkCount: savedCount.current,\n          lastChunkTime: lastTimecode.current,\n          startedAt: recordingStartTime.current,\n          updatedAt: Date.now(),\n        },\n      });\n    } catch (err) {\n      debugWarn(\"Failed to persist session state:\", err);\n    }\n  };\n\n  /**\n   * Start a heartbeat to periodically persist session state.\n   */\n  const startSessionHeartbeat = () => {\n    if (sessionHeartbeat.current) clearInterval(sessionHeartbeat.current);\n    sessionHeartbeat.current = setInterval(() => {\n      if (isRecording.current) {\n        persistSessionState(\"recording\");\n      }\n    }, 10000); // Every 10 seconds\n  };\n\n  /**\n   * Stop the session heartbeat.\n   */\n  const stopSessionHeartbeat = () => {\n    if (sessionHeartbeat.current) {\n      clearInterval(sessionHeartbeat.current);\n      sessionHeartbeat.current = null;\n    }\n  };\n\n  /**\n   * Keep timer UI responsive in content scripts by updating a tick in storage.\n   * This runs in the recorder tab, which is kept alive during recording.\n   */\n  const startRecordingTick = () => {\n    if (recordingTick.current) clearInterval(recordingTick.current);\n    recordingTick.current = setInterval(async () => {\n      if (!isRecording.current || !recordingStartTime.current) return;\n      const { totalPausedMs, pausedAt, paused } =\n        await chrome.storage.local.get([\"totalPausedMs\", \"pausedAt\", \"paused\"]);\n      const now = Date.now();\n      const basePaused = Number(totalPausedMs) || 0;\n      const extraPaused =\n        paused && pausedAt ? Math.max(0, now - Number(pausedAt)) : 0;\n      const elapsed = Math.max(\n        0,\n        now - recordingStartTime.current - basePaused - extraPaused,\n      );\n      chrome.storage.local.set({ recordingNow: now, recordingDuration: elapsed });\n    }, 1000);\n  };\n\n  const stopRecordingTick = () => {\n    if (recordingTick.current) {\n      clearInterval(recordingTick.current);\n      recordingTick.current = null;\n    }\n  };\n\n  // Emit stop to background at most once per recording session.\n  const requestStop = (reason = \"generic\", extra = {}) => {\n    if (stopSignalSent.current) {\n      debugWarn(\"requestStop() ignored; already sent\", { reason });\n      return false;\n    }\n    if (isFinishing.current || !isRecording.current) {\n      debug(\"requestStop() ignored; recorder not active\", {\n        reason,\n        isFinishing: isFinishing.current,\n        isRecording: isRecording.current,\n      });\n      return false;\n    }\n    stopSignalSent.current = true;\n    sendStopRecording(reason, extra);\n    return true;\n  };\n\n  async function saveChunk(e, i) {\n    const ts = e.timecode ?? 0;\n\n    if (!(await canFitChunk(e.data.size))) {\n      debugWarn(\"Low storage, aborting recording\");\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n        lowStorageAbortAt: Date.now(),\n        lowStorageAbortChunks: index.current,\n      });\n      requestStop(\"low-storage\", { memoryError: true, savedChunks: savedCount.current });\n      return false;\n    }\n\n    try {\n      await chunksStore.setItem(`chunk_${i}`, {\n        index: i,\n        chunk: e.data,\n        timestamp: ts,\n      });\n      if (DEBUG_RECORDER) {\n        debug(\"Saved chunk to IndexedDB\", {\n          key: `chunk_${i}`,\n          size: e.data.size,\n          ts,\n        });\n      }\n    } catch (err) {\n      debugError(\"Failed to save chunk, aborting recording\", err);\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n        lowStorageAbortAt: Date.now(),\n        lowStorageAbortChunks: index.current,\n      });\n      requestStop(\"chunk-save-failed\", { memoryError: true, savedChunks: savedCount.current });\n      return false;\n    }\n\n    lastTimecode.current = ts;\n    lastSize.current = e.data.size;\n    savedCount.current += 1;\n\n    if (backupRef.current) {\n      chrome.runtime.sendMessage({ type: \"write-file\", index: i });\n    }\n    return true;\n  }\n\n  async function drainQueue() {\n    if (draining.current) return;\n    draining.current = true;\n\n    try {\n      if (DEBUG_RECORDER) {\n        debug(\"Draining queue start\", {\n          pending: pending.current.length,\n          pendingBytes: pendingBytes.current,\n        });\n      }\n      while (pending.current.length) {\n        if (lowStorageAbort.current) {\n          debugWarn(\"Low storage while draining, clearing queue\");\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          break;\n        }\n\n        const e = pending.current.shift();\n        pendingBytes.current -= e.data.size;\n\n        if (!(await canFitChunk(e.data.size))) {\n          debugWarn(\"Low storage during drain, stopping recording\");\n          if (!lowStorageAbort.current) {\n            chrome.runtime.sendMessage({\n              type: \"show-toast\",\n              message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n              timeout: 8000,\n            });\n          }\n          lowStorageAbort.current = true;\n          chrome.storage.local.set({\n            recording: false,\n            restarting: false,\n            tabRecordedID: null,\n            memoryError: true,\n            lowStorageAbortAt: Date.now(),\n            lowStorageAbortChunks: index.current,\n          });\n          requestStop(\"low-storage\", { memoryError: true, savedChunks: savedCount.current });\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          break;\n        }\n\n        const i = index.current;\n        const saved = await saveChunk(e, i);\n        if (saved) index.current = i + 1;\n      }\n    } finally {\n      if (DEBUG_RECORDER) {\n        debug(\"Draining queue finished\", {\n          pending: pending.current.length,\n          pendingBytes: pendingBytes.current,\n        });\n      }\n      draining.current = false;\n    }\n  }\n\n  async function waitForDrain() {\n    if (DEBUG_RECORDER) {\n      debug(\"waitForDrain() called\");\n    }\n    while (draining.current || pending.current.length) {\n      await new Promise((r) => setTimeout(r, 10));\n    }\n    if (DEBUG_RECORDER) {\n      debug(\"waitForDrain() resolved\");\n    }\n  }\n\n  useEffect(() => {\n    chrome.storage.local.get([\"backup\"], (result) => {\n      backupRef.current = !!result.backup;\n      debug(\"Loaded backup flag from storage\", backupRef.current);\n    });\n  }, []);\n\n  // Check whether the stream is in a startable state.\n  function getStreamReadiness() {\n    if (!helperVideoStream.current) return { ready: false, reason: \"stream-ref-null\" };\n    const vt = helperVideoStream.current.getVideoTracks();\n    if (vt.length === 0) return { ready: false, reason: \"zero-video-tracks\" };\n    if (vt[0].readyState !== \"live\") return { ready: false, reason: \"track-not-live\", trackState: vt[0].readyState };\n    return { ready: true };\n  }\n\n  function buildStreamDiagInfo(bucket) {\n    const stream = helperVideoStream.current;\n    const vt = stream?.getVideoTracks?.() ?? [];\n    return {\n      bucket,\n      started,\n      streamReadyAt: streamReadyAt.current,\n      msSinceReady: streamReadyAt.current ? Date.now() - streamReadyAt.current : null,\n      msSinceStartRequested: startRequestedAt.current ? Date.now() - startRequestedAt.current : null,\n      streamExists: !!stream,\n      streamActive: stream?.active ?? null,\n      streamId: stream?.id ?? null,\n      videoTrackCount: vt.length,\n      audioTrackCount: stream?.getAudioTracks?.()?.length ?? null,\n      trackReadyState: vt[0]?.readyState ?? null,\n      trackEnabled: vt[0]?.enabled ?? null,\n      trackMuted: vt[0]?.muted ?? null,\n      docHidden: document.hidden,\n      docVisibility: document.visibilityState,\n    };\n  }\n\n  /** Arm a timeout that errors if the stream never shows up. */\n  function armStartGateTimeout() {\n    clearStartGateTimeout();\n    startGateTimeout.current = setTimeout(() => {\n      if (!startRequested.current) return; // request was fulfilled or cancelled\n      const readiness = getStreamReadiness();\n      if (readiness.ready) {\n        // Race: stream arrived just before timeout\n        slLog(\"start-gate-timeout-race-ok\");\n        tryStartIfReady();\n        return;\n      }\n      const diagInfo = buildStreamDiagInfo(\"start-gate-timeout\");\n      console.warn(\"[Screenity:startRec] stream never became ready\", diagInfo);\n      slLog(\"start-gate-timeout\", diagInfo);\n      chrome.storage.local.set({ lastStreamCheckFail: diagInfo });\n      // Full reset so no gate state leaks into a future session.\n      resetGateState();\n      sendRecordingError(\n        \"Recording not ready: screen stream is missing (tab may have been suspended)\",\n      );\n    }, START_GATE_TIMEOUT_MS);\n  }\n\n  function clearStartGateTimeout() {\n    if (startGateTimeout.current) {\n      clearTimeout(startGateTimeout.current);\n      startGateTimeout.current = null;\n    }\n  }\n\n  /** Reset gate state on stop/dismiss/restart. */\n  function resetGateState() {\n    startRequested.current = false;\n    startRequestedAt.current = null;\n    isStarting.current = false;\n    clearStartGateTimeout();\n  }\n\n  /** Request recording start — immediate if stream is ready, deferred otherwise. */\n  function requestStart() {\n    if (recorder.current !== null || isStarting.current) {\n      debugWarn(\"requestStart() called but recorder already exists or start in flight\");\n      slLog(\"requestStart-bail-already-active\", {\n        hasRecorder: recorder.current !== null,\n        isStarting: isStarting.current,\n      });\n      return;\n    }\n    if (isRestarting.current) {\n      pendingStartAfterRestart.current = true;\n      slLog(\"requestStart-queued-restart\");\n      debug(\"Queued start request while restarting\");\n      return;\n    }\n    if (startRequested.current) {\n      // Already waiting — ignore duplicate.\n      slLog(\"requestStart-bail-already-requested\");\n      return;\n    }\n\n    startRequested.current = true;\n    startRequestedAt.current = Date.now();\n    slLog(\"requestStart\", {\n      hasStream: !!helperVideoStream.current,\n      started,\n      docHidden: document.hidden,\n      docVisibility: document.visibilityState,\n    });\n\n    const readiness = getStreamReadiness();\n    if (readiness.ready) {\n      slLog(\"requestStart-immediate\", readiness);\n      startRecording();\n    } else {\n      slLog(\"requestStart-deferred\", readiness);\n      armStartGateTimeout();\n    }\n  }\n\n  /** If a start was requested and the stream is ready, fire startRecording(). */\n  function tryStartIfReady() {\n    if (!startRequested.current) return;\n    const readiness = getStreamReadiness();\n    slLog(\"tryStartIfReady\", readiness);\n    if (readiness.ready) {\n      clearStartGateTimeout();\n      startRecording();\n    }\n    // Otherwise the gate timeout handles it.\n  }\n\n  // startRecording — stream must be ready (verified by the gate above).\n  async function startRecording() {\n    slLog(\"startRecording-enter\", {\n      hasRecorder: recorder.current !== null,\n      hasStream: !!helperVideoStream.current,\n      started,\n      streamReadyAt: streamReadyAt.current,\n      docHidden: document.hidden,\n    });\n\n    // Acting on the request now — reset gate state.\n    resetGateState();\n\n    if (recorder.current !== null || isStarting.current) {\n      debugWarn(\"startRecording() called but recorder already exists or start in flight\");\n      slLog(\"startRecording-bail-already-active\", {\n        hasRecorder: recorder.current !== null,\n        isStarting: isStarting.current,\n      });\n      return;\n    }\n\n    isStarting.current = true;\n    debug(\"startRecording()\");\n    recordingGeneration.current += 1;\n    const runGeneration = recordingGeneration.current;\n\n    // Start silent audio to prevent Chrome from freezing this background tab\n    startTabKeepAlive();\n\n    // Record the start time for session tracking\n    recordingStartTime.current = Date.now();\n\n    // Final preflight — edge-case guard in case state changed since the gate.\n    navigator.storage.persist();\n    if (!helperVideoStream.current) {\n      const diagInfo = buildStreamDiagInfo(\"stream-ref-null\");\n      console.warn(\"[Screenity:startRec] helperVideoStream is null\", diagInfo);\n      slLog(\"startRecording-fail-stream-null\", diagInfo);\n      chrome.storage.local.set({ lastStreamCheckFail: diagInfo });\n      sendRecordingError(\n        \"Recording not ready: screen stream is missing (tab may have been suspended)\",\n      );\n      stopTabKeepAlive();\n      isStarting.current = false;\n      return;\n    }\n    const videoTracks = helperVideoStream.current.getVideoTracks();\n    if (videoTracks.length === 0) {\n      const diagInfo = buildStreamDiagInfo(\"stream-zero-video-tracks\");\n      console.warn(\"[Screenity:startRec] helperVideoStream has 0 video tracks\", diagInfo);\n      slLog(\"startRecording-fail-zero-tracks\", diagInfo);\n      chrome.storage.local.set({ lastStreamCheckFail: diagInfo });\n      sendRecordingError(\"No video tracks available\");\n      stopTabKeepAlive();\n      isStarting.current = false;\n      return;\n    }\n    {\n      const vt = videoTracks[0];\n      const diagInfo = {\n        bucket: \"stream-ok\",\n        trackReadyState: vt.readyState,\n        trackEnabled: vt.enabled,\n        trackMuted: vt.muted,\n        msSinceReady: streamReadyAt.current\n          ? Date.now() - streamReadyAt.current\n          : null,\n      };\n      slLog(\"startRecording-preflight-ok\", diagInfo);\n      if (vt.readyState === \"ended\") {\n        console.warn(\"[Screenity:startRec] video track present but ended\", diagInfo);\n        slLog(\"startRecording-fail-track-ended\", diagInfo);\n        chrome.storage.local.set({ lastStreamCheckFail: diagInfo });\n        sendRecordingError(\n          \"Recording not ready: screen stream ended (tab may have been suspended)\",\n        );\n        stopTabKeepAlive();\n        isStarting.current = false;\n        return;\n      }\n    }\n\n    await chunksStore.clear();\n    debug(\"Cleared chunksStore\");\n\n    lastTimecode.current = 0;\n    lastSize.current = 0;\n    hasChunks.current = false;\n    savedCount.current = 0;\n    pending.current = [];\n    draining.current = false;\n    lowStorageAbort.current = false;\n    pendingBytes.current = 0;\n    sentLast.current = false;\n    isFinishing.current = false;\n    stopSignalSent.current = false;\n\n    const { qualityValue } = await chrome.storage.local.get([\"qualityValue\"]);\n    const { isPro, maxQuality, maxFps } = await getFreeCaptureCaps();\n    const effectiveQualityValue = isPro\n      ? qualityValue\n      : clampQualityValue(qualityValue, maxQuality);\n    const { audioBitsPerSecond, videoBitsPerSecond: bitratePreset } =\n      getBitrates(effectiveQualityValue);\n    let videoBitsPerSecond = bitratePreset;\n\n    debug(\"Bitrates resolved\", {\n      qualityValue: effectiveQualityValue,\n      audioBitsPerSecond,\n      videoBitsPerSecond,\n    });\n\n    const recordingId = `${Date.now()}-${Math.random()\n      .toString(16)\n      .slice(2, 8)}`;\n    await chrome.storage.local.set({\n      fastRecorderActiveRecordingId: recordingId,\n      fastRecorderInUse: false,\n      fastRecorderValidationFailed: false,\n      fastRecorderValidation: null,\n    });\n\n    const { useWebCodecsRecorder } = await chrome.storage.local.get([\n      \"useWebCodecsRecorder\",\n    ]);\n    const userSetting = useWebCodecsRecorder === true ? true : false;\n    const stickyState = await getFastRecorderStickyState();\n    const probeResult = await probeFastRecorderSupport();\n    const shouldUseFast = shouldUseFastRecorder(\n      userSetting,\n      probeResult,\n      stickyState,\n    );\n    const selectedVideoConfig =\n      probeResult?.details?.selectedVideoConfig || null;\n\n    await chrome.storage.local.set({\n      fastRecorderDecision: {\n        shouldUseFast,\n        reasons: probeResult.reasons,\n        stickyDisabled: stickyState.disabled,\n      },\n      fastRecorderStatus: {\n        userSetting,\n        probe: {\n          ok: probeResult.ok,\n          reasons: probeResult.reasons,\n          details: probeResult.details,\n          at: probeResult.at || Date.now(),\n        },\n        decision: {\n          useFast: shouldUseFast,\n          why:\n            userSetting === false\n              ? \"user_disabled\"\n              : stickyState?.disabled && userSetting !== true\n              ? \"sticky_disabled\"\n              : probeResult.ok\n              ? \"probe_ok\"\n              : \"probe_failed\",\n          at: Date.now(),\n        },\n        disabled: Boolean(stickyState?.disabled),\n        disabledReason: stickyState?.reason || null,\n        disabledDetails: stickyState?.details || null,\n        disabledAt: null,\n        updatedAt: Date.now(),\n      },\n      fastRecorderSelectedVideoConfig: selectedVideoConfig,\n    });\n\n    debugRecordingEvent(recdbgSessionRef, \"fast-recorder-probe\", {\n      probe: probeResult,\n      stickyState,\n      userSetting,\n      shouldUseFast,\n    });\n\n    const canUseWebCodecs = shouldUseFast;\n\n    const videoTrack = liveStream.current?.getVideoTracks()[0] ?? null;\n    const settings = videoTrack?.getSettings() || {};\n    const { width: qualityWidth, height: qualityHeight } =\n      getResolutionForQuality(effectiveQualityValue);\n    const trackWidth = settings.width ?? qualityWidth ?? 1920;\n    const trackHeight = settings.height ?? qualityHeight ?? 1080;\n    const width = Math.min(trackWidth, qualityWidth ?? trackWidth);\n    const height = Math.min(trackHeight, qualityHeight ?? trackHeight);\n\n    const { fpsValue } = await chrome.storage.local.get([\"fpsValue\"]);\n    let fps = parseInt(fpsValue);\n    if (Number.isNaN(fps)) fps = 30;\n\n    if (!isPro) {\n      fps = Math.min(fps, maxFps);\n    }\n    const computedBps = computeTargetVideoBps(width, height, fps);\n    videoBitsPerSecond = !isPro\n      ? Math.min(computedBps, bitratePreset)\n      : computedBps;\n    debug(\"Video bitrate target\", {\n      bitratePreset,\n      videoBitsPerSecond,\n    });\n\n    debug(\"Recorder capabilities\", {\n      canUseWebCodecs,\n      FORCE_MEDIARECORDER,\n      width,\n      height,\n      fps,\n    });\n\n    // Reset pre-recording state. Recording-committed state (isRecording,\n    // heartbeat, timing) is set after the recorder is created successfully.\n    pausedStateRef.current = false;\n    chrome.storage.local.set({ restarting: false });\n    isRestarting.current = false;\n    index.current = 0;\n\n    const handleChunk = async (data, timestampMs) => {\n      if (runGeneration !== recordingGeneration.current) {\n        return;\n      }\n      if (useWebCodecs.current) {\n        const blob =\n          data instanceof Blob ? data : new Blob([data], { type: \"video/mp4\" });\n\n        const ts = timestampMs ?? 0;\n        const i = index.current;\n\n        try {\n          await chunksStore.setItem(`chunk_${i}`, {\n            index: i,\n            chunk: blob,\n            timestamp: ts,\n          });\n          index.current = i + 1;\n          hasChunks.current = true;\n          savedCount.current += 1;\n\n          if (DEBUG_RECORDER) {\n            debug(\"WebCodecs chunk saved\", {\n              i,\n              ts,\n              size: blob.size,\n              savedCount: savedCount.current,\n            });\n          }\n\n          if (backupRef.current) {\n            chrome.runtime.sendMessage({ type: \"write-file\", index: i });\n          }\n        } catch (err) {\n          debugError(\"Failed to save WebCodecs chunk\", err);\n        }\n\n        return;\n      }\n\n      if (lowStorageAbort.current) return;\n\n      const blob =\n        data instanceof Blob ? data : new Blob([data], { type: \"video/mp4\" });\n\n      const e = {\n        data: blob,\n        timecode: timestampMs ?? 0,\n      };\n\n      if (!hasChunks.current) {\n        hasChunks.current = true;\n        lastTimecode.current = e.timecode;\n        lastSize.current = e.data.size;\n      }\n\n      pending.current.push(e);\n      pendingBytes.current += e.data.size;\n\n      if (DEBUG_RECORDER) {\n        debug(\"Queued MediaRecorder chunk\", {\n          size: blob.size,\n          timecode: e.timecode,\n          pending: pending.current.length,\n          pendingBytes: pendingBytes.current,\n        });\n      }\n\n      if (pendingBytes.current > MAX_PENDING_BYTES) {\n        debugWarn(\n          \"Pending bytes exceeded threshold, pausing MediaRecorder and draining queue\",\n        );\n        try {\n          if (\n            recorder.current instanceof MediaRecorder &&\n            recorder.current.state !== \"paused\"\n          ) {\n            recorder.current.pause();\n          }\n          await drainQueue();\n          if (\n            recorder.current instanceof MediaRecorder &&\n            recorder.current.state === \"paused\"\n          ) {\n            recorder.current.resume();\n          }\n        } catch (err) {\n          debugError(\n            \"Error while draining queue with MediaRecorder paused\",\n            err,\n          );\n          await drainQueue();\n        }\n      }\n\n      void drainQueue();\n    };\n\n    // Abort if stop/dismiss was requested during async setup.\n    if (isFinishing.current || uiClosing.current) {\n      slLog(\"startRecording-abort-interrupted\", {\n        isFinishing: isFinishing.current,\n        uiClosing: uiClosing.current,\n      });\n      isStarting.current = false;\n      stopTabKeepAlive();\n      return;\n    }\n\n    try {\n      if (canUseWebCodecs) {\n        useWebCodecs.current = true;\n\n        const hasAudioTrack =\n          !!liveStream.current &&\n          typeof liveStream.current.getAudioTracks === \"function\" &&\n          liveStream.current.getAudioTracks().length > 0;\n\n        debug(\"Initializing WebCodecsRecorder\", {\n          hasAudioTrack,\n          videoBitsPerSecond,\n          audioBitsPerSecond,\n        });\n\n        recorder.current = new WebCodecsRecorder(liveStream.current, {\n          width,\n          height,\n          fps,\n          videoBitrate: videoBitsPerSecond,\n          audioBitrate: hasAudioTrack ? audioBitsPerSecond : undefined,\n          enableAudio: hasAudioTrack,\n          videoEncoderConfig: selectedVideoConfig,\n          debug: DEBUG_RECORDER,\n          onFinalized: async () => {\n            debug(\"WebCodecsRecorder onFinalized()\");\n            await waitForDrain();\n            await updateFreeFinalizeStatus(\"chunks_ready\", 95);\n            let validation = null;\n            try {\n              const blob = await rebuildBlobFromChunks();\n              validation = await validateFastRecorderOutputBlob(blob, {\n                minBytes: 64 * 1024,\n                timeoutMs: 4000,\n                videoCodec: recorder.current?.selectedVideoCodec || undefined,\n                audioCodec: hasAudioTrack ? \"mp4a.40.2\" : null,\n                recordingId,\n              });\n              debugRecordingEvent(recdbgSessionRef, \"fast-recorder-validate\", {\n                validation,\n              });\n            } catch (err) {\n              validation = {\n                ok: false,\n                hardFail: true,\n                reasons: [\"validation-exception\"],\n                details: { error: String(err) },\n              };\n            }\n\n            if (validation && !validation.ok) {\n              await markFastRecorderFailure(\"validation-failed\", validation);\n              await chrome.storage.local.set({\n                useWebCodecsRecorder: false,\n                lastWebCodecsFailureAt: Date.now(),\n                lastWebCodecsFailureCode: \"validation-failed\",\n              });\n              const hardFail = Boolean(validation.hardFail);\n              await chrome.storage.local.set({\n                fastRecorderValidationFailed: hardFail,\n                fastRecorderValidation: validation,\n              });\n              await updateFreeFinalizeStatus(\n                \"failed\",\n                100,\n                validation.reasons || \"validation-failed\",\n              );\n              chrome.runtime.sendMessage({\n                type: \"show-toast\",\n                message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n              });\n              if (hardFail) {\n                chrome.runtime.sendMessage({\n                  type: \"fast-recorder-hard-fail\",\n                  recordingId,\n                });\n                if (!sentLast.current) {\n                  sentLast.current = true;\n                  isFinishing.current = false;\n                  chrome.runtime.sendMessage({ type: \"video-ready\" });\n                }\n                return;\n              }\n            } else {\n              await chrome.storage.local.set({\n                fastRecorderValidationFailed: false,\n                fastRecorderValidation: validation,\n              });\n            }\n            await updateFreeFinalizeStatus(\"ready\", 100);\n            if (!sentLast.current) {\n              sentLast.current = true;\n              isFinishing.current = false;\n              chrome.runtime.sendMessage({ type: \"video-ready\" });\n            }\n          },\n          onChunk: (chunkData, timestampUs) => {\n            const blob = new Blob([chunkData], { type: \"video/mp4\" });\n            const timestampMs = timestampUs\n              ? Math.floor(timestampUs / 1000)\n              : 0;\n            if (DEBUG_RECORDER) {\n              debug(\"WebCodecsRecorder onChunk\", {\n                size: blob.size,\n                timestampUs,\n                timestampMs,\n              });\n            }\n            handleChunk(blob, timestampMs);\n          },\n          onError: (err) => {\n            debugError(\"WebCodecsRecorder error\", err);\n            markFastRecorderFailure(\"webcodecs-error\", {\n              error: String(err),\n            });\n            chrome.storage.local.set({\n              useWebCodecsRecorder: false,\n              lastWebCodecsFailureAt: Date.now(),\n              lastWebCodecsFailureCode: \"webcodecs-error\",\n            });\n            updateFreeFinalizeStatus(\"failed\", 100, String(err));\n            chrome.runtime.sendMessage({\n              type: \"show-toast\",\n              message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n            });\n            sendRecordingError(String(err));\n          },\n          onStop: async () => {\n            debug(\"WebCodecsRecorder onStop()\");\n            await waitForDrain();\n          },\n        });\n\n        const ok = await recorder.current.start();\n\n        debug(\"WebCodecsRecorder.start() result\", ok);\n\n        if (!ok) {\n          debugWarn(\n            \"Falling back to MediaRecorder because WebCodecsRecorder failed\",\n          );\n          useWebCodecs.current = false;\n          await chrome.storage.local.set({ fastRecorderInUse: false });\n          await chrome.storage.local.set({\n            useWebCodecsRecorder: false,\n            lastWebCodecsFailureAt: Date.now(),\n            lastWebCodecsFailureCode: \"start-failed\",\n          });\n          chrome.runtime.sendMessage({\n            type: \"show-toast\",\n            message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n          });\n          recorder.current = null;\n          return await startRecording();\n        }\n        await chrome.storage.local.set({ fastRecorderInUse: true });\n\n        debugRecordingEvent(recdbgSessionRef, \"recorder-start\", {\n          encoder: \"webcodecs\",\n          codec: \"webcodecs\",\n          width,\n          height,\n          fps,\n          videoBitrate: videoBitsPerSecond,\n          audioBitrate: hasAudioTrack ? audioBitsPerSecond : undefined,\n        });\n\n        setTimeout(() => {\n          const afterTrack = liveStream.current?.getVideoTracks?.()[0] ?? null;\n          logRecordingSnapshot(\"after-start-500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n          debugRecordingEvent(recdbgSessionRef, \"after-start-500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n        }, 500);\n\n        setTimeout(() => {\n          const afterTrack = liveStream.current?.getVideoTracks?.()[0] ?? null;\n          debugRecordingEvent(recdbgSessionRef, \"after-start-1500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n        }, 1500);\n      } else {\n        debug(\"Using MediaRecorder fallback\");\n        useWebCodecs.current = false;\n        await chrome.storage.local.set({ fastRecorderInUse: false });\n        let recorderToken = 0;\n        let codecFallbackTriggered = false;\n        let mediaRecorderStartAt = Date.now();\n        let recdbgChunkCount = 0;\n        let recdbgTotalBytes = 0;\n        let activeCodec = \"vp9\";\n        let activeMimeType = null;\n\n        try {\n          // Ensure recorder.current is initialized (createMediaRecorder may\n          // be used elsewhere; create a MediaRecorder here if missing).\n          if (!recorder.current) {\n            try {\n              recorder.current = createMediaRecorder(liveStream.current, {\n                audioBitsPerSecond,\n                videoBitsPerSecond: videoBitsPerSecond,\n              });\n              debug(\"Created MediaRecorder instance for fallback\");\n            } catch (initErr) {\n              debugError(\"Failed to create MediaRecorder\", initErr);\n              sendRecordingError(\n                \"Failed to start recording: \" + String(initErr),\n              );\n              isStarting.current = false;\n              stopTabKeepAlive();\n              return;\n            }\n          }\n\n          recorder.current.start(1000);\n          debug(\"MediaRecorder.start(1000) called\");\n        } catch (err) {\n          debugError(\"Failed to start MediaRecorder\", err);\n          sendRecordingError(\"Failed to start recording: \" + String(err));\n          recorder.current = null;\n          isStarting.current = false;\n          stopTabKeepAlive();\n          return;\n        }\n\n        recorder.current.onerror = (ev) => {\n          debugError(\"MediaRecorder.onerror\", ev);\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"mediarecorder\",\n            why: String(ev?.error || \"unknown\"),\n          });\n        };\n\n        recorder.current.onstop = async () => {\n          debug(\"MediaRecorder.onstop\");\n          try {\n            recorder.current.requestData();\n          } catch {}\n          if (isRestarting.current) return;\n          await waitForDrain();\n          if (!sentLast.current) {\n            sentLast.current = true;\n            isFinishing.current = false;\n            chrome.runtime.sendMessage({ type: \"video-ready\" });\n          }\n        };\n\n        recorder.current.ondataavailable = async (e) => {\n          if (runGeneration !== recordingGeneration.current) {\n            return;\n          }\n          if (!e || !e.data || !e.data.size) {\n            debugWarn(\"MediaRecorder.ondataavailable with empty data\", e);\n            if (\n              recorder.current instanceof MediaRecorder &&\n              recorder.current.state === \"inactive\"\n            ) {\n              chrome.storage.local.set({\n                recording: false,\n                restarting: false,\n                tabRecordedID: null,\n              });\n              requestStop(\"mediarecorder-empty-inactive\");\n            }\n            return;\n          }\n\n          if (DEBUG_RECORDER) {\n            debug(\"MediaRecorder.ondataavailable\", {\n              size: e.data.size,\n              timecode: e.timecode ?? 0,\n            });\n          }\n\n          await handleChunk(e.data, e.timecode ?? 0);\n        };\n      }\n    } catch (err) {\n      debugError(\"startRecording() top-level error\", err);\n      if (useWebCodecs.current) {\n        await chrome.storage.local.set({\n          useWebCodecsRecorder: false,\n          lastWebCodecsFailureAt: Date.now(),\n          lastWebCodecsFailureCode: \"start-exception\",\n        });\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n        });\n      }\n      sendRecordingError(String(err));\n      isStarting.current = false;\n      stopTabKeepAlive();\n      return;\n    }\n\n    // Recorder created successfully — now advertise \"recording\" to the system.\n    await setRecordingTimingState({\n      recording: true,\n      paused: false,\n      recordingStartTime: recordingStartTime.current,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    isStarting.current = false;\n    isRecording.current = true;\n    startSessionHeartbeat();\n    startRecordingTick();\n    persistSessionState(\"recording\");\n\n    if (helperAudioStream.current) {\n      const track = helperAudioStream.current.getAudioTracks()[0];\n      if (track) {\n        track.onended = () => {\n          if (isFinishing.current || !isRecording.current) return;\n          // Log detailed diagnostics for debugging\n          const diagnosticInfo = {\n            reason: \"audio-track-ended\",\n            savedChunks: savedCount.current,\n            lastTimecode: lastTimecode.current,\n            recordingDuration: recordingStartTime.current\n              ? Date.now() - recordingStartTime.current\n              : null,\n            trackLabel: track?.label || null,\n            trackReadyState: track?.readyState || null,\n          };\n          console.warn(\n            \"[Recorder] Audio track ended unexpectedly\",\n            diagnosticInfo,\n          );\n          // Notify via stream-ended-warning toast\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"stream-ended\",\n            why: chrome.i18n.getMessage(\"audioTrackEndedToast\"),\n          }).catch(() => {});\n          chrome.storage.local.set({\n            recording: false,\n            lastTrackEndEvent: diagnosticInfo,\n          });\n          requestStop(\"audio-track-ended\");\n        };\n      }\n    }\n\n    const liveVideoTrack = liveStream.current?.getVideoTracks?.()[0] || null;\n    if (liveVideoTrack) {\n      liveVideoTrack.onended = () => {\n        if (isFinishing.current || !isRecording.current) return;\n        const track = liveStream.current?.getVideoTracks?.()[0] || null;\n        // Log detailed diagnostics for debugging\n        const diagnosticInfo = {\n          reason: \"liveStream-video-track-ended\",\n          savedChunks: savedCount.current,\n          lastTimecode: lastTimecode.current,\n          recordingDuration: recordingStartTime.current\n            ? Date.now() - recordingStartTime.current\n            : null,\n          trackLabel: track?.label || null,\n          trackReadyState: track?.readyState || null,\n        };\n        console.warn(\"[Recorder] liveStream video track ended\", diagnosticInfo);\n        // Notify via stream-ended-warning toast\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-ended\",\n          why: chrome.i18n.getMessage(\"videoTrackEndedToast\"),\n        }).catch(() => {});\n        chrome.storage.local.set({\n          recording: false,\n          restarting: false,\n          tabRecordedID: null,\n          lastTrackEndEvent: diagnosticInfo,\n        });\n        requestStop(\"live-video-track-ended\");\n      };\n    }\n\n    const helperVideoTrack =\n      helperVideoStream.current?.getVideoTracks?.()[0] || null;\n    if (helperVideoTrack) {\n      helperVideoTrack.onended = () => {\n        if (isFinishing.current || !isRecording.current) return;\n        const track = helperVideoStream.current?.getVideoTracks?.()[0] || null;\n        // Log detailed diagnostics for debugging\n        const diagnosticInfo = {\n          reason: \"helperVideoStream-video-track-ended\",\n          savedChunks: savedCount.current,\n          lastTimecode: lastTimecode.current,\n          recordingDuration: recordingStartTime.current\n            ? Date.now() - recordingStartTime.current\n            : null,\n          trackLabel: track?.label || null,\n          trackReadyState: track?.readyState || null,\n        };\n        console.warn(\n          \"[Recorder] helperVideoStream video track ended\",\n          diagnosticInfo,\n        );\n        // Notify via stream-ended-warning toast\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-ended\",\n          why: chrome.i18n.getMessage(\"videoTrackEndedToast\"),\n        }).catch(() => {});\n        chrome.storage.local.set({\n          recording: false,\n          restarting: false,\n          tabRecordedID: null,\n          lastTrackEndEvent: diagnosticInfo,\n        });\n        requestStop(\"helper-video-track-ended\");\n      };\n    }\n  }\n\n  async function warmUpStream(liveStream) {\n    debug(\"warmUpStream() start\", {\n      hasVideo: liveStream.getVideoTracks().length,\n      hasAudio: liveStream.getAudioTracks().length,\n    });\n\n    const videoTrack = liveStream.getVideoTracks()[0];\n    const audioTrack = liveStream.getAudioTracks()[0];\n\n    await new Promise(async (resolve) => {\n      const proc = new MediaStreamTrackProcessor({ track: videoTrack });\n      const reader = proc.readable.getReader();\n\n      while (true) {\n        const { value: frame } = await reader.read();\n        if (frame) {\n          if (frame.codedWidth > 0 && frame.codedHeight > 0) {\n            debug(\"warmUpStream() video frame OK\", {\n              codedWidth: frame.codedWidth,\n              codedHeight: frame.codedHeight,\n            });\n            frame.close();\n            reader.releaseLock();\n            resolve();\n            break;\n          }\n          frame.close();\n        }\n      }\n    });\n\n    if (audioTrack) {\n      await new Promise(async (resolve) => {\n        const proc = new MediaStreamTrackProcessor({ track: audioTrack });\n        const reader = proc.readable.getReader();\n\n        while (true) {\n          const { value: audio } = await reader.read();\n          if (audio && audio.numberOfFrames > 0) {\n            debug(\"warmUpStream() audio OK\", {\n              numberOfFrames: audio.numberOfFrames,\n            });\n            audio.close?.();\n            reader.releaseLock();\n            resolve();\n            break;\n          }\n          audio?.close?.();\n        }\n      });\n    }\n\n    debug(\"warmUpStream() done\");\n  }\n\n  const rebuildBlobFromChunks = async () => {\n    const items = [];\n    await chunksStore.ready();\n    await chunksStore.iterate((value) => (items.push(value), undefined));\n    items.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));\n    const parts = items.map((c) =>\n      c.chunk instanceof Blob ? c.chunk : new Blob([c.chunk]),\n    );\n    if (!parts.length) return null;\n    const first = parts[0];\n    const inferredType = first?.type || \"video/mp4\";\n    return new Blob(parts, { type: inferredType });\n  };\n\n  const updateFreeFinalizeStatus = async (stage, percent = 0, error = null) => {\n    try {\n      const { fastRecorderActiveRecordingId } = await chrome.storage.local.get([\n        \"fastRecorderActiveRecordingId\",\n      ]);\n      if (!fastRecorderActiveRecordingId) return;\n      const key = `freeFinalizeStatus:${fastRecorderActiveRecordingId}`;\n      const existing = await chrome.storage.local.get([key]);\n      const current = existing[key];\n      if (\n        current &&\n        (current.stage === \"ready\" || current.stage === \"chunks_ready\") &&\n        (stage === \"stopping\" || stage === \"finalizing\")\n      ) {\n        return;\n      }\n      debugRecordingEvent(recdbgSessionRef, \"free-finalize-status\", {\n        recordingId: fastRecorderActiveRecordingId,\n        stage,\n        percent,\n        error: error || undefined,\n      });\n      await chrome.storage.local.set({\n        [key]: {\n          recordingId: fastRecorderActiveRecordingId,\n          stage,\n          percent,\n          updatedAt: Date.now(),\n          error: error || undefined,\n        },\n      });\n    } catch {}\n  };\n\n  async function stopRecording() {\n    if (isFinishing.current) {\n      debugWarn(\"stopRecording() called while already finishing\");\n      return;\n    }\n    debug(\"stopRecording()\");\n    // Stop takes priority over any pending start gate.\n    resetGateState();\n    isFinishing.current = true;\n    isRecording.current = false;\n    await updateFreeFinalizeStatus(\"stopping\", 0);\n\n    // Stop the session heartbeat and persist final state\n    stopSessionHeartbeat();\n    stopRecordingTick();\n    persistSessionState(\"stopping\");\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n\n    try {\n      if (\n        useWebCodecs.current &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        debug(\"Stopping WebCodecsRecorder\");\n        await updateFreeFinalizeStatus(\"finalizing\", 20);\n        await recorder.current.stop();\n      } else if (recorder.current instanceof MediaRecorder) {\n        debug(\"Stopping MediaRecorder\");\n        await updateFreeFinalizeStatus(\"finalizing\", 20);\n        try {\n          recorder.current.requestData();\n        } catch {}\n        if (recorder.current.state !== \"inactive\") {\n          recorder.current.stop();\n        }\n      }\n    } catch (err) {\n      debugError(\"stopRecording() error while stopping recorder\", err);\n    }\n\n    await waitForDrain();\n    if (!useWebCodecs.current) {\n      await updateFreeFinalizeStatus(\"chunks_ready\", 100);\n    }\n    recorder.current = null;\n\n    // Stop the silent audio keep-alive\n    stopTabKeepAlive();\n\n    // Clear session state after successful stop\n    persistSessionState(\"completed\");\n\n    if (!isRestarting.current) {\n      debug(\"Stopping tracks and clearing streams\");\n      slLog(\"helperVideoStream-nulling\", { path: \"stopRecording\" });\n      liveStream.current?.getTracks().forEach((t) => t.stop());\n      helperVideoStream.current?.getTracks().forEach((t) => t.stop());\n      helperAudioStream.current?.getTracks().forEach((t) => t.stop());\n\n      liveStream.current = null;\n      helperVideoStream.current = null;\n      helperAudioStream.current = null;\n    }\n    if (!useWebCodecs.current) {\n      await updateFreeFinalizeStatus(\"ready\", 100);\n    }\n  }\n\n  const dismissRecording = async () => {\n    debug(\"dismissRecording()\");\n    resetGateState();\n    uiClosing.current = true;\n    isRecording.current = false;\n\n    // Clean up keep-alive and session tracking\n    stopTabKeepAlive();\n    stopSessionHeartbeat();\n    persistSessionState(\"dismissed\");\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n\n    window.close();\n  };\n\n  const restartRecording = async () => {\n    if (isRestarting.current) {\n      debugWarn(\"restartRecording() called while already restarting\");\n      return false;\n    }\n\n    debug(\"restartRecording()\");\n    recordingGeneration.current += 1;\n\n    // Kill any stale start-gate timeout from the previous session.\n    resetGateState();\n\n    isRestarting.current = true;\n    isRecording.current = false;\n    sentLast.current = false;\n    isFinishing.current = false;\n    stopSignalSent.current = false;\n    pausedStateRef.current = false;\n\n    try {\n      if (\n        useWebCodecs.current &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        debug(\"Cleaning up WebCodecsRecorder for restart\");\n        recorder.current.running = false;\n        recorder.current.paused = false;\n        await recorder.current.cleanup();\n      } else if (recorder.current instanceof MediaRecorder) {\n        debug(\"Stopping MediaRecorder for restart\");\n        const mediaRecorder = recorder.current;\n        mediaRecorder.ondataavailable = null;\n        mediaRecorder.onstop = null;\n        mediaRecorder.onerror = null;\n        try {\n          mediaRecorder.requestData();\n        } catch {}\n        if (mediaRecorder.state !== \"inactive\") {\n          await new Promise((resolve) => {\n            let done = false;\n            const finish = () => {\n              if (done) return;\n              done = true;\n              clearTimeout(timeoutId);\n              resolve();\n            };\n            const timeoutId = setTimeout(finish, 1600);\n            try {\n              mediaRecorder.addEventListener(\"stop\", finish, { once: true });\n            } catch {}\n            try {\n              mediaRecorder.stop();\n            } catch {\n              finish();\n            }\n          });\n        }\n      }\n    } catch (err) {\n      debugError(\"Error while restarting recorder\", err);\n      isRestarting.current = false;\n      return false;\n    }\n\n    recorder.current = null;\n\n    pending.current = [];\n    pendingBytes.current = 0;\n    draining.current = false;\n    hasChunks.current = false;\n    savedCount.current = 0;\n    lastSize.current = 0;\n    lastTimecode.current = 0;\n    await chunksStore.clear();\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n\n    useWebCodecs.current = false;\n\n    isRestarting.current = false;\n    debug(\"restartRecording() done, ready to start again\");\n    slLog(\"restart-done\", {\n      hasStream: !!helperVideoStream.current,\n      hasPendingStart: pendingStartAfterRestart.current,\n    });\n    if (pendingStartAfterRestart.current) {\n      pendingStartAfterRestart.current = false;\n      debug(\"Processing queued start after restart\");\n      requestStart();\n    }\n    return true;\n  };\n\n  async function startAudioStream(id) {\n    debug(\"startAudioStream()\", { id });\n    const useExact = id && id !== \"none\";\n    const audioStreamOptions = {\n      mimeType: \"video/webm;codecs=vp8,opus\",\n      audio: useExact\n        ? {\n            deviceId: {\n              exact: id,\n            },\n          }\n        : true,\n    };\n\n    const { defaultAudioInputLabel, audioinput } =\n      await chrome.storage.local.get([\"defaultAudioInputLabel\", \"audioinput\"]);\n    const desiredLabel =\n      defaultAudioInputLabel ||\n      audioinput?.find((device) => device.deviceId === id)?.label ||\n      \"\";\n\n    const result = await getUserMediaWithFallback({\n      constraints: audioStreamOptions,\n      fallbacks:\n        useExact && desiredLabel\n          ? [\n              {\n                kind: \"audioinput\",\n                desiredDeviceId: id,\n                desiredLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultAudioInput: resolvedId,\n                    defaultAudioInputLabel: desiredLabel,\n                  });\n                },\n              },\n            ]\n          : [],\n    })\n      .then((stream) => {\n        debug(\"startAudioStream() got stream with exact device\", {\n          hasAudio: stream.getAudioTracks().length,\n        });\n        return stream;\n      })\n      .catch((err) => {\n        debugWarn(\n          \"startAudioStream() exact device failed, retrying generic\",\n          err,\n        );\n        const audioStreamOptions = {\n          mimeType: \"video/webm;codecs=vp8,opus\",\n          audio: true,\n        };\n\n        return navigator.mediaDevices\n          .getUserMedia(audioStreamOptions)\n          .then((stream) => {\n            debug(\"startAudioStream() got generic stream\", {\n              hasAudio: stream.getAudioTracks().length,\n            });\n            return stream;\n          })\n          .catch((err2) => {\n            debugError(\"startAudioStream() failed completely\", err2);\n            return null;\n          });\n      });\n\n    return result;\n  }\n\n  function setAudioInputVolume(volume) {\n    if (!audioInputGain.current) {\n      debugWarn(\"setAudioInputVolume() called but audioInputGain is null\");\n      return;\n    }\n    debug(\"setAudioInputVolume()\", volume);\n    audioInputGain.current.gain.value = volume;\n  }\n\n  function setAudioOutputVolume(volume) {\n    if (!audioOutputGain.current) {\n      debugWarn(\"setAudioOutputVolume() called but audioOutputGain is null\");\n      return;\n    }\n    debug(\"setAudioOutputVolume()\", volume);\n    audioOutputGain.current.gain.value = volume;\n  }\n\n  const setMic = async (result) => {\n    debug(\"setMic()\", result);\n    if (helperAudioStream.current != null) {\n      if (result.active) {\n        setAudioInputVolume(1);\n      } else {\n        setAudioInputVolume(0);\n      }\n    }\n  };\n\n  async function startStream(data, id, options, permissions, permissions2) {\n    slLog(\"startStream-enter\", {\n      recordingType: data.recordingType,\n      hasId: !!id,\n      isTab: isTab.current,\n    });\n    debug(\"startStream()\", {\n      recordingType: data.recordingType,\n      id,\n      isTab: isTab.current,\n      options,\n      permissions: permissions?.state,\n      micPermissions: permissions2?.state,\n    });\n\n    const { qualityValue } = await chrome.storage.local.get([\"qualityValue\"]);\n    const { isPro, maxQuality, maxFps } = await getFreeCaptureCaps();\n    const effectiveQualityValue = isPro\n      ? qualityValue\n      : clampQualityValue(qualityValue, maxQuality);\n    const { width, height } = getResolutionForQuality(effectiveQualityValue);\n\n    const { fpsValue } = await chrome.storage.local.get([\"fpsValue\"]);\n    let fps = parseInt(fpsValue);\n\n    if (isNaN(fps)) {\n      fps = 30;\n    }\n    if (!isPro) {\n      fps = Math.min(fps, maxFps);\n    }\n\n    let userConstraints = {\n      audio: {\n        deviceId: data.defaultAudioInput,\n      },\n      video: {\n        deviceId: data.defaultVideoInput,\n        width: {\n          ideal: width,\n        },\n        height: {\n          ideal: height,\n        },\n        frameRate: {\n          ideal: fps,\n        },\n      },\n    };\n    if (!isPro && userConstraints.video) {\n      userConstraints.video = {\n        ...userConstraints.video,\n        width: {\n          ideal: width,\n          max: width,\n        },\n        height: {\n          ideal: height,\n          max: height,\n        },\n      };\n    }\n    if (permissions.state === \"denied\") {\n      userConstraints.video = false;\n    }\n    if (permissions2.state === \"denied\") {\n      userConstraints.audio = false;\n    }\n\n    debug(\"User media constraints\", {\n      userConstraints,\n      qualityValue: effectiveQualityValue,\n      fps,\n    });\n\n    let userStream;\n    if (\n      permissions.state != \"denied\" &&\n      permissions2.state != \"denied\" &&\n      data.recordingType === \"camera\"\n    ) {\n      debug(\"Requesting camera userStream\");\n      const {\n        defaultAudioInputLabel,\n        defaultVideoInputLabel,\n        audioinput,\n        videoinput,\n      } = await chrome.storage.local.get([\n        \"defaultAudioInputLabel\",\n        \"defaultVideoInputLabel\",\n        \"audioinput\",\n        \"videoinput\",\n      ]);\n      const desiredAudioLabel =\n        defaultAudioInputLabel ||\n        audioinput?.find((device) => device.deviceId === data.defaultAudioInput)\n          ?.label ||\n        \"\";\n      const desiredVideoLabel =\n        defaultVideoInputLabel ||\n        videoinput?.find((device) => device.deviceId === data.defaultVideoInput)\n          ?.label ||\n        \"\";\n\n      const hasAudioDevice =\n        data.defaultAudioInput && data.defaultAudioInput !== \"none\";\n      const hasVideoDevice =\n        data.defaultVideoInput && data.defaultVideoInput !== \"none\";\n      const cameraConstraints = {\n        ...userConstraints,\n        audio:\n          userConstraints.audio && hasAudioDevice\n            ? {\n                ...userConstraints.audio,\n                deviceId: { exact: data.defaultAudioInput },\n              }\n            : userConstraints.audio,\n        video:\n          userConstraints.video && hasVideoDevice\n            ? {\n                ...userConstraints.video,\n                deviceId: { exact: data.defaultVideoInput },\n              }\n            : userConstraints.video,\n      };\n\n      userStream = await getUserMediaWithFallback({\n        constraints: cameraConstraints,\n        fallbacks: [\n          hasVideoDevice && desiredVideoLabel\n            ? {\n                kind: \"videoinput\",\n                desiredDeviceId: data.defaultVideoInput,\n                desiredLabel: desiredVideoLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultVideoInput: resolvedId,\n                    defaultVideoInputLabel: desiredVideoLabel,\n                  });\n                },\n              }\n            : null,\n          hasAudioDevice && desiredAudioLabel\n            ? {\n                kind: \"audioinput\",\n                desiredDeviceId: data.defaultAudioInput,\n                desiredLabel: desiredAudioLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultAudioInput: resolvedId,\n                    defaultAudioInputLabel: desiredAudioLabel,\n                  });\n                },\n              }\n            : null,\n        ].filter(Boolean),\n      });\n      debug(\"Camera userStream acquired\", {\n        videoTracks: userStream.getVideoTracks().length,\n        audioTracks: userStream.getAudioTracks().length,\n      });\n    }\n\n    if (data.recordingType === \"camera\") {\n      if (!userStream || typeof userStream.getVideoTracks !== \"function\") {\n        debugWarn(\"Camera stream unavailable\");\n        resetGateState();\n        sendRecordingError(\"Camera stream unavailable\");\n        return;\n      }\n      if (!userStream.getVideoTracks().length) {\n        debugWarn(\"Camera stream has no video track\");\n        resetGateState();\n        sendRecordingError(\"Camera stream has no video track\");\n        return;\n      }\n      helperVideoStream.current = userStream;\n      slLog(\"helperVideoStream-assigned\", { path: \"camera\", streamId: userStream.id });\n    } else {\n      const constraints = {\n        audio: {\n          mandatory: {\n            chromeMediaSource: isTab.current ? \"tab\" : \"desktop\",\n            chromeMediaSourceId: id,\n          },\n        },\n        video: {\n          mandatory: {\n            chromeMediaSource: isTab.current ? \"tab\" : \"desktop\",\n            chromeMediaSourceId: id,\n            maxWidth: width,\n            maxHeight: height,\n            width: { ideal: width, max: width },\n            height: { ideal: height, max: height },\n            maxFrameRate: fps,\n          },\n        },\n      };\n\n      debug(\"desktopCapture getUserMedia constraints\", constraints);\n      slLog(\"getUserMedia-start\", { isTab: isTab.current });\n\n      let stream;\n\n      try {\n        stream = await navigator.mediaDevices.getUserMedia(constraints);\n\n        if (stream.getVideoTracks().length === 0) {\n          debugError(\"No video tracks returned from getUserMedia\");\n          resetGateState();\n          sendRecordingError(\"No video tracks available\");\n          return;\n        }\n      } catch (err) {\n        debugError(\"Failed to get user media for desktop/tab capture\", err);\n        resetGateState();\n        sendRecordingError(\"Failed to get user media: \" + String(err));\n        return;\n      }\n\n      debug(\"desktop/tab stream acquired\", {\n        videoTracks: stream.getVideoTracks().length,\n        audioTracks: stream.getAudioTracks().length,\n      });\n      slLog(\"getUserMedia-resolved\", {\n        videoTracks: stream.getVideoTracks().length,\n        audioTracks: stream.getAudioTracks().length,\n        streamId: stream.id,\n        trackState: stream.getVideoTracks()[0]?.readyState,\n      });\n\n      if (isTab.current) {\n        const output = new AudioContext();\n        const source = output.createMediaStreamSource(stream);\n        source.connect(output.destination);\n        debug(\"Created playback AudioContext for tab preview\");\n      }\n\n      helperVideoStream.current = stream;\n      slLog(\"helperVideoStream-assigned\", { path: \"desktop/tab\", streamId: stream.id });\n\n      const surface = stream.getVideoTracks()[0].getSettings().displaySurface;\n      debug(\"Display surface\", surface);\n      chrome.runtime.sendMessage({ type: \"set-surface\", surface: surface });\n    }\n\n    aCtx.current = new AudioContext();\n    destination.current = aCtx.current.createMediaStreamDestination();\n    liveStream.current = new MediaStream();\n\n    const micstream = await startAudioStream(data.defaultAudioInput);\n    helperAudioStream.current = micstream;\n\n    if (helperAudioStream.current != null && !data.micActive) {\n      setAudioInputVolume(0);\n    }\n\n    // System/Tab audio\n    const sysTracks = helperVideoStream.current.getAudioTracks();\n    debug(\"System/tab audio tracks\", sysTracks.length);\n    if (sysTracks.length > 0) {\n      const sysSource = aCtx.current.createMediaStreamSource(\n        new MediaStream([sysTracks[0]]),\n      );\n      audioOutputGain.current = aCtx.current.createGain();\n      sysSource.connect(audioOutputGain.current).connect(destination.current);\n    }\n\n    // Mic audio\n    const micTracks = helperAudioStream.current?.getAudioTracks() ?? [];\n    debug(\"Mic audio tracks\", micTracks.length);\n    if (micTracks.length > 0) {\n      const micSource = aCtx.current.createMediaStreamSource(\n        new MediaStream([micTracks[0]]),\n      );\n      audioInputGain.current = aCtx.current.createGain();\n      micSource.connect(audioInputGain.current).connect(destination.current);\n\n      if (!data.micActive) {\n        audioInputGain.current.gain.value = 0;\n      }\n    }\n\n    const helperVideoTrack = helperVideoStream.current.getVideoTracks()[0];\n    if (!helperVideoTrack) {\n      resetGateState();\n      sendRecordingError(\"Display stream missing video track\");\n      return;\n    }\n    liveStream.current.addTrack(helperVideoTrack);\n\n    const mainVideoTrack = liveStream.current?.getVideoTracks()[0];\n    if (mainVideoTrack) {\n      mainVideoTrack.onmute = () => {\n        debugWarn(\"mainVideoTrack muted\");\n      };\n      mainVideoTrack.oninactive = () => {\n        if (isFinishing.current || !isRecording.current) return;\n        debugWarn(\"mainVideoTrack inactive → stopping recording\");\n        requestStop(\"main-video-track-inactive\");\n        stopRecording();\n      };\n    }\n    if (\n      (helperAudioStream.current != null &&\n        helperAudioStream.current.getAudioTracks().length > 0) ||\n      helperVideoStream.current.getAudioTracks().length > 0\n    ) {\n      const mixedAudioTrack = destination.current.stream.getAudioTracks()[0];\n      if (mixedAudioTrack) {\n        liveStream.current.addTrack(mixedAudioTrack);\n      }\n    }\n\n    debug(\"liveStream ready\", {\n      videoTracks: liveStream.current.getVideoTracks().length,\n      audioTracks: liveStream.current.getAudioTracks().length,\n    });\n\n    setStarted(true);\n    streamReadyAt.current = Date.now();\n    slLog(\"stream-ready\", {\n      streamId: helperVideoStream.current?.id,\n      videoTracks: helperVideoStream.current?.getVideoTracks().length,\n      trackState: helperVideoStream.current?.getVideoTracks()[0]?.readyState,\n      startRequested: startRequested.current,\n    });\n\n    await warmUpStream(liveStream.current);\n\n    slLog(\"warmUp-done\");\n    chrome.runtime.sendMessage({ type: \"reset-active-tab\" });\n    slLog(\"reset-active-tab-sent\");\n\n    // If a start was already requested, honour it now that the stream is live.\n    tryStartIfReady();\n  }\n\n  async function startStreaming(data) {\n    debug(\"startStreaming()\", { data });\n    slLog(\"startStreaming-enter\", { recordingType: data?.recordingType });\n    const permissions = await navigator.permissions.query({\n      name: \"camera\",\n    });\n    const permissions2 = await navigator.permissions.query({\n      name: \"microphone\",\n    });\n\n    debug(\"Permissions\", {\n      camera: permissions.state,\n      microphone: permissions2.state,\n    });\n\n    try {\n      if (data.recordingType === \"camera\") {\n        debug(\"Streaming camera recording\");\n        startStream(data, null, null, permissions, permissions2);\n      } else if (!isTab.current) {\n        let captureTypes = [\"screen\", \"window\", \"tab\", \"audio\"];\n        if (tabPreferred.current) {\n          captureTypes = [\"tab\", \"screen\", \"window\", \"audio\"];\n        }\n        debug(\"desktopCapture.chooseDesktopMedia\", {\n          captureTypes,\n          tabPreferred: tabPreferred.current,\n        });\n        slLog(\"chooseDesktopMedia-show\");\n        chrome.desktopCapture.chooseDesktopMedia(\n          captureTypes,\n          null,\n          (streamId, options) => {\n            slLog(\"chooseDesktopMedia-picked\", { hasStreamId: !!streamId });\n            debug(\"chooseDesktopMedia callback\", { streamId, options });\n            if (\n              streamId === undefined ||\n              streamId === null ||\n              streamId === \"\"\n            ) {\n              debugWarn(\"User cancelled the desktop capture modal\");\n              resetGateState();\n              sendRecordingError(\"User cancelled the modal\", true);\n              return;\n            } else {\n              startStream(data, streamId, options, permissions, permissions2);\n            }\n          },\n        );\n      } else {\n        const tabStreamId = await waitForTabStreamId();\n        debug(\"Streaming with pre-resolved tabID\", tabStreamId);\n        if (!tabStreamId) {\n          resetGateState();\n          sendRecordingError(\"Unable to resolve tab stream id\", true);\n          return;\n        }\n        startStream(data, tabStreamId, null, permissions, permissions2);\n      }\n    } catch (err) {\n      debugError(\"startStreaming() error\", err);\n      resetGateState();\n      sendRecordingError(\"Failed to start streaming: \" + String(err), true);\n    }\n  }\n\n  useEffect(() => {\n    chrome.storage.local.get([\"tabPreferred\"], (result) => {\n      tabPreferred.current = result.tabPreferred;\n      debug(\"Loaded tabPreferred\", tabPreferred.current);\n    });\n  }, []);\n\n  const getStreamID = async (id) => {\n    debug(\"getStreamID()\", id);\n    const streamId = await chrome.tabCapture.getMediaStreamId({\n      targetTabId: id,\n    });\n    debug(\"Resolved tabCapture streamId\", streamId);\n    tabID.current = streamId;\n  };\n\n  const waitForTabStreamId = async () => {\n    if (tabID.current) return tabID.current;\n    for (let i = 0; i < 30; i += 1) {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n      if (tabID.current) return tabID.current;\n    }\n    return null;\n  };\n\n  // Cleanup on component unmount\n  useEffect(() => {\n    return () => {\n      debug(\"Component unmounting - cleaning up\");\n      slLog(\"component-unmount\", {\n        hadStream: !!helperVideoStream.current,\n        started,\n        startRequested: startRequested.current,\n      });\n      resetGateState();\n      stopTabKeepAlive();\n      stopSessionHeartbeat();\n    };\n  }, []);\n\n  useEffect(() => {\n    const handleClose = (e) => {\n      debug(\"beforeunload event\", {\n        uiClosing: uiClosing.current,\n        isRecording: isRecording.current,\n      });\n      if (uiClosing.current || !isRecording.current) return;\n\n      // Stop recording - note: beforeunload can't reliably wait for async\n      // The keep-alive and session state will help with recovery if needed\n      stopRecording();\n\n      e.preventDefault();\n      e.returnValue = \"\";\n    };\n\n    window.addEventListener(\"beforeunload\", handleClose);\n    return () => {\n      debug(\"Removing beforeunload handler\");\n      window.removeEventListener(\"beforeunload\", handleClose);\n    };\n  }, []);\n\n  // Message handler — registered once on mount; uses refs so closure stays fresh.\n  const onMessageRef = useRef(null);\n  onMessageRef.current = (request, sender, sendResponse) => {\n    if (DEBUG_RECORDER) {\n      debug(\"onMessage()\", request.type, { request, sender });\n    }\n\n    if (request.type === \"loaded\") {\n      slLog(\"msg-loaded\");\n      backupRef.current = request.backup;\n      if (!tabPreferred.current) {\n        isTab.current = request.isTab;\n        if (request.isTab) {\n          getStreamID(request.tabID);\n        }\n      } else {\n        isTab.current = false;\n      }\n      chrome.runtime.sendMessage({ type: \"get-streaming-data\" });\n    } else if (request.type === \"streaming-data\") {\n      slLog(\"msg-streaming-data\");\n      startStreaming(JSON.parse(request.data));\n    } else if (request.type === \"start-recording-tab\") {\n      slLog(\"msg-start-recording-tab\", {\n        hasStream: !!helperVideoStream.current,\n        streamReady: getStreamReadiness(),\n        isRestarting: isRestarting.current,\n        docHidden: document.hidden,\n        docVisibility: document.visibilityState,\n      });\n      // Request start via the readiness gate.\n      requestStart();\n    } else if (request.type === \"restart-recording-tab\") {\n      if (isRestarting.current) {\n        sendResponse?.({ ok: false, error: \"restart-in-progress\" });\n        return true;\n      }\n      pendingStartAfterRestart.current = false;\n      Promise.resolve(restartRecording())\n        .then((restarted) => {\n          if (!restarted) {\n            sendResponse?.({ ok: false, error: \"restart-teardown-failed\" });\n            return;\n          }\n          sendResponse?.({ ok: true, restarted: true });\n        })\n        .catch((error) => {\n          sendResponse?.({\n            ok: false,\n            error: error?.message || String(error),\n          });\n        });\n      return true;\n    } else if (request.type === \"stop-recording-tab\") {\n      stopRecording();\n      sendResponse?.({ ok: true });\n      return true;\n    } else if (request.type === \"set-mic-active-tab\") {\n      setMic(request);\n    } else if (request.type === \"set-audio-output-volume\") {\n      setAudioOutputVolume(request.volume);\n    } else if (request.type === \"pause-recording-tab\") {\n      if (!recorder.current) return;\n      if (pausedStateRef.current) return;\n      if (\n        useWebCodecs.current &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        debug(\"Pausing WebCodecsRecorder\");\n        recorder.current.pause();\n      } else if (recorder.current instanceof MediaRecorder) {\n        debug(\"Pausing MediaRecorder\");\n        recorder.current.pause();\n      }\n      const now = Date.now();\n      pausedStateRef.current = true;\n      void setRecordingTimingState({\n        paused: true,\n        pausedAt: now,\n      });\n    } else if (request.type === \"resume-recording-tab\") {\n      if (!recorder.current) return;\n      if (!pausedStateRef.current) return;\n      if (\n        useWebCodecs.current &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        debug(\"Resuming WebCodecsRecorder\");\n        recorder.current.resume();\n      } else if (recorder.current instanceof MediaRecorder) {\n        debug(\"Resuming MediaRecorder\");\n        recorder.current.resume();\n      }\n      const now = Date.now();\n      pausedStateRef.current = false;\n      void (async () => {\n        try {\n          const { pausedAt, totalPausedMs } = await chrome.storage.local.get([\n            \"pausedAt\",\n            \"totalPausedMs\",\n          ]);\n          const additional = pausedAt ? Math.max(0, now - pausedAt) : 0;\n          await setRecordingTimingState({\n            paused: false,\n            pausedAt: null,\n            totalPausedMs: (totalPausedMs || 0) + additional,\n          });\n        } catch (err) {\n          debugWarn(\"Failed to update resume timing state\", err);\n        }\n      })();\n    } else if (request.type === \"dismiss-recording\") {\n      dismissRecording();\n    }\n  };\n\n  // Stable wrapper — delegates to the ref so the listener never changes.\n  useEffect(() => {\n    const stableHandler = (request, sender, sendResponse) => {\n      return onMessageRef.current?.(request, sender, sendResponse);\n    };\n    debug(\"Adding chrome.runtime.onMessage listener (stable)\");\n    slLog(\"listener-add\");\n    chrome.runtime.onMessage.addListener(stableHandler);\n\n    return () => {\n      debug(\"Removing chrome.runtime.onMessage listener (stable)\");\n      slLog(\"listener-remove\");\n      chrome.runtime.onMessage.removeListener(stableHandler);\n    };\n  }, []);\n\n  return (\n    <>\n      <RecorderUI started={started} isTab={isTab.current} />\n      {process.env.SCREENITY_DEV_MODE === \"true\" && (\n        <div\n          style={{\n            position: \"fixed\",\n            top: 4,\n            right: 4,\n            zIndex: 2147483647,\n            background: \"rgba(0,0,0,0.65)\",\n            color: \"#0f0\",\n            fontSize: \"10px\",\n            padding: \"3px 7px\",\n            borderRadius: \"4px\",\n            fontFamily: \"monospace\",\n            pointerEvents: \"none\",\n          }}\n        >\n          DEV\n        </div>\n      )}\n    </>\n  );\n};\n\nexport default Recorder;\n"
  },
  {
    "path": "src/pages/Recorder/RecorderUI.jsx",
    "content": "// RecorderUI.jsx\nimport React from \"react\";\nimport Warning from \"./warning/Warning\";\n\nconst RecorderUI = ({ started, isTab }) => {\n  return (\n    <div className=\"wrap\">\n      <img\n        className=\"logo\"\n        src={chrome.runtime.getURL(\"assets/logo-text.svg\")}\n        alt=\"Screenity logo\"\n      />\n      <div className=\"middle-area\">\n        <img\n          src={chrome.runtime.getURL(\"assets/record-tab-active.svg\")}\n          alt=\"Recording icon\"\n        />\n        <div className=\"title\">\n          {!started\n            ? chrome.i18n.getMessage(\"recorderSelectTitle\")\n            : chrome.i18n.getMessage(\"recorderSelectProgressTitle\")}\n        </div>\n        <div className=\"subtitle\">\n          {chrome.i18n.getMessage(\"recorderSelectDescription\")}\n        </div>\n      </div>\n\n      {!isTab && !started && <Warning />}\n\n      <div className=\"setupBackgroundSVG\"></div>\n\n      <style>\n        {`\n          body {\n            overflow: hidden;\n          }\n          .button-stop {\n            padding: 10px 20px;\n            background: #FFF;\n            border-radius: 30px;\n            color: #29292F;\n            font-size: 14px;\n            font-weight: 500;\n            cursor: pointer;\n            margin-top: 0px;\n            border: 1px solid #E8E8E8;\n            margin-left: auto;\n            margin-right: auto;\n            z-index: 999999;\n          }\n          .setupBackgroundSVG {\n            position: absolute;\n            top: 0px;\n            left: 0px;\n            width: 100%;\n            height:100%;\n            background: url('${chrome.runtime.getURL(\n              \"assets/helper/pattern-svg.svg\"\n            )}') repeat;\n            background-size: 62px 23.5px;\n            animation: moveBackground 138s linear infinite;\n            transform: rotate(0deg);\n          }\n          @keyframes moveBackground {\n            0% {\n              background-position: 0 0;\n            }\n            100% {\n              background-position: 100% 0;\n            }\n          }\n          .logo {\n            position: absolute;\n            bottom: 30px;\n            left: 0px;\n            right: 0px;\n            margin: auto;\n            width: 120px;\n          }\n          .wrap {\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background-color: #F6F7FB;\n          }\n          .middle-area {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            height: 100%;\n            font-family: \"Satoshi Medium\", sans-serif;\n          }\n          .middle-area img {\n            width: 40px;\n            margin-bottom: 20px;\n          }\n          .title {\n            font-size: 24px;\n            font-weight: 700;\n            color: #1A1A1A;\n            margin-bottom: 14px;\n            font-family: Satoshi-Medium, sans-serif;\n            text-align: center;\n          }\n          .subtitle {\n            font-size: 14px;\n            font-weight: 400;\n            color: #6E7684;\n            margin-bottom: 24px;\n            font-family: Satoshi-Medium, sans-serif;\n            text-align: center;\n          }\n        `}\n      </style>\n    </div>\n  );\n};\n\nexport default RecorderUI;\n"
  },
  {
    "path": "src/pages/Recorder/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n    <title>Screenity - Recorder</title>\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n      body {\n        font-family: Satoshi-Medium;\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Recorder/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Recorder from \"./Recorder\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Recorder />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Recorder/mediaRecorderUtils.js",
    "content": "import { MIME_TYPES } from \"./recorderConfig\";\n\nexport function createMediaRecorder(\n  stream,\n  { audioBitsPerSecond, videoBitsPerSecond }\n) {\n  const mimeType = MIME_TYPES.find((type) =>\n    MediaRecorder.isTypeSupported(type)\n  );\n\n  if (!mimeType) {\n    throw new Error(\"❌ No supported MIME types found\");\n  }\n\n  return new MediaRecorder(stream, {\n    mimeType,\n    audioBitsPerSecond,\n    videoBitsPerSecond,\n  });\n}\n"
  },
  {
    "path": "src/pages/Recorder/messaging.js",
    "content": "import { classifyError } from \"../utils/errorCodes\";\n\nexport function sendRecordingError(why, cancel = false) {\n  const errorType = !cancel ? \"stream-error\" : \"cancel-modal\";\n  const whyStr = typeof why === \"string\" ? why : JSON.stringify(why);\n  const errorCode = classifyError(whyStr, errorType);\n\n  chrome.runtime.sendMessage({\n    type: \"recording-error\",\n    error: errorType,\n    why: whyStr,\n    errorCode,\n  });\n}\n\nexport function sendStopRecording(reason = \"generic\", extra = {}) {\n  chrome.runtime.sendMessage({\n    type: \"stop-recording-tab\",\n    reason,\n    ...extra,\n  });\n}\n"
  },
  {
    "path": "src/pages/Recorder/recorderConfig.js",
    "content": "export const MIME_TYPES = [\n  \"video/webm;codecs=vp9,opus\",\n  \"video/webm;codecs=vp8,opus\",\n  \"video/webm;codecs=h264\",\n  \"video/webm\",\n  \"video/mp4\",\n  \"video/webm;codecs=avc1\",\n];\n\nexport function getBitrates(quality) {\n  switch (quality) {\n    case \"4k\":\n      return { audio: 192000, video: 20000000 };\n    case \"1080p\":\n      return { audio: 128000, video: 8000000 };\n    case \"720p\":\n      return { audio: 128000, video: 5000000 };\n    case \"480p\":\n      return { audio: 96000, video: 3000000 };\n    case \"360p\":\n      return { audio: 96000, video: 1500000 };\n    case \"240p\":\n      return { audio: 64000, video: 1000000 };\n    default:\n      return { audio: 128000, video: 8000000 };\n  }\n}\nexport const VIDEO_QUALITIES = {\n  \"4k\": { width: 4096, height: 2160 },\n  \"1080p\": { width: 1920, height: 1080 },\n  \"720p\": { width: 1280, height: 720 },\n  \"480p\": { width: 854, height: 480 },\n  \"360p\": { width: 640, height: 360 },\n  \"240p\": { width: 426, height: 240 },\n  default: { width: 1920, height: 1080 },\n};\n\nexport function getResolutionForQuality(qualityValue = \"default\") {\n  return VIDEO_QUALITIES[qualityValue] || VIDEO_QUALITIES.default;\n}\n"
  },
  {
    "path": "src/pages/Recorder/warning/Warning.jsx",
    "content": "import React, {\n  useState,\n  useEffect,\n  useContext,\n  useCallback,\n  useRef,\n} from \"react\";\n\nimport { ReactSVG } from \"react-svg\";\n\nimport * as ToastEl from \"@radix-ui/react-toast\";\n\nconst Warning = () => {\n  const [open, setOpen] = useState(false);\n  const [title, setTitle] = useState(\"Record computer audio\");\n  const [description, setDescription] = useState(\"\");\n  const [duration, setDuration] = useState(10000);\n\n  const openWarning = useCallback((title, description, duration) => {\n    setTitle(title);\n    setDescription(description);\n    setDuration(duration);\n    setOpen(true);\n  }, []);\n\n  useEffect(() => {\n    // Check if macOS\n    const isMac = navigator.userAgent.indexOf(\"Mac\") !== -1;\n    if (isMac) {\n      openWarning(\n        chrome.i18n.getMessage(\"recordAudioWarningMacTitle\"),\n        chrome.i18n.getMessage(\"recordAudioWarningMacDescription\"),\n        10000\n      );\n    } else {\n      openWarning(\n        chrome.i18n.getMessage(\"recordAudioWarningOtherTitle\"),\n        chrome.i18n.getMessage(\"recordAudioWarningOtherDescription\"),\n        10000\n      );\n    }\n  }, []);\n\n  return (\n    <ToastEl.Provider swipeDirection=\"down\" duration={duration}>\n      <ToastEl.Root\n        className=\"warning-root\"\n        open={open}\n        onOpenChange={setOpen}\n        onSwipeEnd={() => {\n          setOpen(false);\n        }}\n      >\n        <div className=\"warning-icon\">\n          <ReactSVG\n            src={chrome.runtime.getURL(\"assets/tool-icons/audio-icon.svg\")}\n            width={20}\n            height={20}\n          />\n        </div>\n        <div className=\"warning-content\">\n          <ToastEl.Title className=\"warning-title\">{title}</ToastEl.Title>\n          <ToastEl.Description className=\"warning-description\">\n            {description}\n          </ToastEl.Description>\n        </div>\n        <ToastEl.Close\n          className=\"warning-close\"\n          onClick={() => {\n            setOpen(false);\n          }}\n        >\n          <ReactSVG\n            src={chrome.runtime.getURL(\"assets/camera-icons/close.svg\")}\n            width={20}\n            height={20}\n          />\n        </ToastEl.Close>\n      </ToastEl.Root>\n      <ToastEl.Viewport className=\"WarningViewport\" />\n      <style>\n        {`\n\n\t\t\t\tbutton {\n\t\t\t\t\tall: unset;\n\t\t\t\t}\n\t\t\t\t.WarningViewport {\n\t\t\t\t\t--viewport-padding: 25px;\n\t\t\t\t\tposition: fixed;\n\t\t\t\t\tbottom: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\tmargin: auto !important;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tpadding: var(--viewport-padding);\n\t\t\t\t\tgap: 14px;\n\t\t\t\t\tmax-width: 100vw;\n\t\t\t\t\twidth: fit-content;\n\t\t\t\t\tlist-style: none;\n\t\t\t\t\tz-index: 2147483647;\n\t\t\t\t\toutline: none;\n\t\t\t\t\tpointer-events: none;\n\t\t\t\t}\n\t\t\t\t\t.WarningViewport:has(.warning-root[data-state=\"open\"]) {\n  pointer-events: all;\n}\n\t\t\t\t\n\t\t\t\t.warning-root {\n\t\t\t\t\tbackground-color: #29292F;\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tbox-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n\t\t\t\t\t\thsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n\t\t\t\t\tpadding: 14px 20px;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: row;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tgap: 8px;\n\t\t\t\t\tfont-size: 15px;\n\t\t\t\t\tline-height: 1.5;\n\t\t\t\t\tmax-width: 500px;\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\ttext-align: left;\n\t\t\t\t\talign-items: flex-start;\n\t\t\t\t}\n\t\t\t\t.warning-content {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: flex-start;\n\t\t\t\t\tgap: 8px;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-state=\"open\"] {\n\t\t\t\t\tanimation: slideIn2 150ms cubic-bezier(0.16, 1, 0.3, 1);\n\t\t\t\t}\n\t\t\t\t.warning-root[data-state=\"closed\"] {\n\t\t\t\t\tanimation: hide 100ms ease-in;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"move\"] {\n\t\t\t\t\ttransform: translateY(var(--radix-toast-swipe-move-y));\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"cancel\"] {\n\t\t\t\t\ttransform: translateY(0);\n\t\t\t\t\ttransition: transform 200ms ease-out;\n\t\t\t\t}\n\t\t\t\t.warning-root[data-swipe=\"end\"] {\n\t\t\t\t\tanimation: swipeOut2 100ms ease-out;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes hide {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\topacity: 1;\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\topacity: 0 !important;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes slideIn2 {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\ttransform: translateY(calc(100% + var(--viewport-padding)));\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\ttransform: translateY(0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes swipeOut2 {\n\t\t\t\t\tfrom {\n\t\t\t\t\t\ttransform: translateY(var(--radix-toast-swipe-end-y));\n\t\t\t\t\t}\n\t\t\t\t\tto {\n\t\t\t\t\t\ttransform: translateY(calc(100% + var(--viewport-padding)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.warning-title {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.warning-description {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\topacity: 0.8;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t.ToastAction {\n\t\t\t\t\tcolor: #FFF;\n\t\t\t\t\tfont-family: \"Satoshi-Medium\", sans-serif;\n\t\t\t\t\ttext-align: right;\n\t\t\t\t\tbackground-color: #51515f;\n\t\t\t\t\tpadding: 0px 12px !important;\n\t\t\t\t\theight: 24px !important;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t\t.warning-close {\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t.warning-close:hover {\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t}\n\t\t\t\t`}\n      </style>\n    </ToastEl.Provider>\n  );\n};\n\nexport default Warning;\n"
  },
  {
    "path": "src/pages/Recorder/webcodecs/Mp4MuxerWrapper.ts",
    "content": "/*!\n * Screenity WebCodecs Recorder - MP4 Muxer Wrapper\n * Licensed under the GNU GPLv3.\n */\n// @ts-nocheck\n\nimport {\n  Output,\n  Mp4OutputFormat,\n  BufferTarget,\n  EncodedVideoPacketSource,\n  EncodedAudioPacketSource,\n  EncodedPacket,\n} from \"mediabunny\";\n\ninterface Mp4MuxerWrapperOptions {\n  width: number;\n  height: number;\n  fps: number;\n  videoBitrate: number;\n  audioBitrate: number;\n  videoCodec?: string;\n  audioCodec?: string;\n  onChunk: (chunk: Uint8Array, timestampUs: number | null) => void | Promise<void>;\n  debug?: boolean;\n}\n\nexport class Mp4MuxerWrapper {\n  private output: Output;\n  private target: BufferTarget;\n\n  private videoSource: EncodedVideoPacketSource;\n  private audioSource: EncodedAudioPacketSource | null = null;\n\n  private videoCodec: string;\n  private audioCodec: string;\n  private pausedOffsetUs = 0;\n  private started = false;\n\n  private videoTimestampOffsetUs: number | null = null;\n  private audioTimestampOffsetUs: number | null = null;\n  private lastVideoTimestampUs = 0;\n  private lastAudioTimestampUs = 0;\n\n  private _lastDecoderConfig: any = null;\n\n  private debug: boolean;\n  private log: (...args: any[]) => void;\n  private warn: (...args: any[]) => void;\n  private err: (...args: any[]) => void;\n\n  constructor(private options: Mp4MuxerWrapperOptions) {\n    this.debug = options.debug ?? false;\n\n    this.log = (...args) => this.debug && console.log(...args);\n    this.warn = (...args) => this.debug && console.warn(...args);\n    this.err = (...args) => this.debug && console.error(...args);\n\n    this.videoCodec = options.videoCodec || \"avc\";\n    this.audioCodec = options.audioCodec || \"aac\";\n\n    this.target = new BufferTarget();\n    this.output = new Output({\n      format: new Mp4OutputFormat({\n        fastStart: false, // metadata at end, more compatible\n      }),\n      target: this.target,\n    });\n\n    this.videoSource = new EncodedVideoPacketSource(this.videoCodec);\n    this.output.addVideoTrack(this.videoSource, {\n      frameRate: this.options.fps,\n    });\n  }\n\n  enableAudio() {\n    if (!this.audioSource) {\n      this.log(\"[MUXER] enableAudio\");\n      this.audioSource = new EncodedAudioPacketSource(this.audioCodec);\n      this.output.addAudioTrack(this.audioSource);\n    }\n  }\n\n  addVideoChunk(chunk: EncodedVideoChunk, meta: any) {\n    if (meta?.decoderConfig) {\n      this._lastDecoderConfig = meta.decoderConfig;\n    }\n\n    // Meta is read-only — supply cached decoderConfig when the chunk lacks one.\n    const effectiveMeta =\n      !meta?.decoderConfig && this._lastDecoderConfig\n        ? { ...meta, decoderConfig: this._lastDecoderConfig }\n        : meta;\n\n    const packet = this.buildPacket(chunk, effectiveMeta, \"video\");\n    return this.videoSource.add(packet, effectiveMeta);\n  }\n\n  addAudioChunk(chunk: EncodedAudioChunk, meta: any) {\n    if (!this.audioSource) return;\n\n    const packet = this.buildPacket(chunk, meta, \"audio\");\n    return this.audioSource.add(packet, meta);\n  }\n\n  async start() {\n    if (this.started) return;\n    this.log(\"[MUXER] start\");\n    await this.output.start();\n    this.started = true;\n  }\n\n  async finalize() {\n    this.log(\"[MUXER] finalize\");\n    await this.output.finalize();\n\n    const data = this.target.buffer;\n    if (data?.byteLength) {\n      this.log(\"[MUXER] emitting final file\", data.byteLength, \"bytes\");\n      this.emitChunk(new Uint8Array(data), null);\n    }\n  }\n\n  // No-op: WebCodecsRecorder already subtracts paused time from timestamps,\n  // so a second subtraction here would break monotonicity. Kept for API compat.\n  setPausedOffset(_offsetUs: number) {}\n\n  setAudioOffset(offsetUs: number) {\n    this.audioTimestampOffsetUs = offsetUs;\n  }\n\n  private emitChunk(chunk: Uint8Array, timestampUs: number | null) {\n    try {\n      const res = this.options.onChunk?.(chunk, timestampUs ?? null);\n      if (res instanceof Promise) {\n        res.catch((err) => this.err(\"[MUXER] onChunk rejected\", err));\n      }\n    } catch (err) {\n      this.err(\"[MUXER] onChunk error\", err);\n    }\n  }\n\n  private buildPacket(\n    chunk: EncodedVideoChunk | EncodedAudioChunk,\n    meta: any,\n    type: \"video\" | \"audio\"\n  ) {\n    const data = new Uint8Array(chunk.byteLength);\n    chunk.copyTo(data);\n\n    const tsUs = this.normalizeTimestamp(type, chunk.timestamp, chunk.duration || 0);\n    const durSec = (chunk.duration || 0) / 1e6;\n\n    return new EncodedPacket(data, chunk.type, tsUs / 1e6, durSec);\n  }\n\n  private normalizeTimestamp(\n    type: \"video\" | \"audio\",\n    timestampUs?: number,\n    durationUs = 0\n  ) {\n    const key = type === \"video\" ? \"lastVideoTimestampUs\" : \"lastAudioTimestampUs\";\n    const last = (this as any)[key] ?? 0;\n\n    if (type === \"audio\") {\n      // Audio ts is already monotonic from WebCodecsRecorder's sample counter.\n      // Just accumulate for logging.\n      const next = last + durationUs;\n      (this as any)[key] = next;\n      this.log(`[MUX] audio ts +${durationUs} => ${next}`);\n      return next;\n    }\n\n    if (typeof timestampUs !== \"number\") {\n      const fallback = last + durationUs;\n      (this as any)[key] = fallback;\n      this.warn(\"[MUX] video missing ts, fallback:\", fallback);\n      return fallback;\n    }\n\n    // Pin offset to the first frame so output starts at 0.\n    // Don't subtract pausedOffsetUs — WebCodecsRecorder already excludes\n    // paused time, and double-subtracting breaks monotonicity.\n    if (this.videoTimestampOffsetUs == null) {\n      this.videoTimestampOffsetUs = timestampUs;\n    }\n\n    const relative = Math.max(\n      0,\n      timestampUs - this.videoTimestampOffsetUs\n    );\n\n    (this as any)[key] = relative;\n    this.log(`[MUX] video ts => ${relative}`);\n\n    return relative;\n  }\n}"
  },
  {
    "path": "src/pages/Recorder/webcodecs/WebCodecsRecorder.js",
    "content": "/*!\n * Screenity WebCodecs Recorder\n * Copyright (c) 2025 Serial Labs Ltd.\n *\n * Licensed under the GNU GPLv3.\n * See the LICENSE file for full details.\n *\n * MP4 recording using WebCodecs + MediaBunny.\n * Handles H.264 video, AAC audio, timestamp safety,\n * canvas resizing, pause/resume, and chunked output.\n */\nimport { Mp4MuxerWrapper } from \"./Mp4MuxerWrapper\";\n\nexport class WebCodecsRecorder {\n  constructor(stream, options) {\n    this.stream = stream;\n    this.options = options;\n\n    this.debug = options.debug ?? false;\n    this.log = (...args) => this.debug && console.log(...args);\n    this.warn = (...args) => this.debug && console.warn(...args);\n    this.err = (...args) => this.debug && console.error(...args);\n\n    this.videoTrack = null;\n    this.audioTrack = null;\n\n    this.videoProcessor = null;\n    this.audioProcessor = null;\n\n    this.videoReader = null;\n    this.audioReader = null;\n\n    this.videoEncoder = null;\n    this.audioEncoder = null;\n\n    this.muxer = null;\n\n    this.running = false;\n\n    this.actualWidth = 0;\n    this.actualHeight = 0;\n\n    this.targetWidth = 0;\n    this.targetHeight = 0;\n\n    this.videoTimestampOffsetUs = null;\n    this.videoFallbackStartMs = null;\n    this.selectedVideoCodec = null;\n    this.firstVideoFrame = undefined;\n    this.frameCount = 0;\n    // Monotonic video frame index for safe timestamps\n    this._videoFrameIndex = 0;\n    this._videoStartUs = null;\n    this._frameDurationUs = null;\n\n    this.audioSamplesWritten = 0;\n    this.audioSampleRate = null;\n\n    this.resizeCanvas = null;\n    this.resizeCtx = null;\n\n    this.justResumed = false;\n    this.paused = false;\n    this.pauseStartUs = null;\n    this.totalPausedDurationUs = 0;\n\n    this._startPromise = null;\n    this._startResolve = null;\n\n    this._prebufferedAudio = [];\n    this._audioReady = false;\n\n    this._stopping = false;\n    this._videoLoopPromise = null;\n    this._audioLoopPromise = null;\n  }\n\n  async _probeRealResolution() {\n    const processor = new MediaStreamTrackProcessor({ track: this.videoTrack });\n    const reader = processor.readable.getReader();\n\n    const { value: frame } = await reader.read();\n    if (!frame) throw new Error(\"Cannot probe frame resolution\");\n\n    const width = frame.codedWidth;\n    const height = frame.codedHeight;\n\n    this.log(\"[WCR] Probed resolution:\", width, \"x\", height);\n    frame.close();\n    reader.releaseLock();\n\n    return { width, height };\n  }\n\n  async start() {\n    this.videoTimestampOffsetUs = null;\n    this.firstVideoFrame = undefined;\n    this._stopping = false;\n    this._videoFrameIndex = 0;\n    this._videoStartUs = null;\n    this._frameDurationUs = null;\n    this.audioSamplesWritten = 0;\n\n    if (this.running) return this._startPromise;\n\n    this._startPromise = new Promise((resolve) => {\n      this._startResolve = resolve;\n    });\n\n    this.running = true;\n    try {\n      this.videoTrack = this.stream.getVideoTracks()[0] || null;\n      this.audioTrack = this.stream.getAudioTracks()[0] || null;\n\n      this.log(\n        \"[WCR] videoTrack\",\n        this.videoTrack,\n        \"state=\",\n        this.videoTrack?.readyState\n      );\n      this.log(\n        \"[WCR] audioTrack\",\n        this.audioTrack,\n        \"state=\",\n        this.audioTrack?.readyState\n      );\n\n      if (!this.videoTrack) {\n        const err = new Error(\"WebCodecsRecorder: No video track\");\n        this.options.onError?.(err);\n        this.running = false;\n\n        if (this._startResolve) {\n          this._startResolve(false);\n          this._startResolve = null;\n        }\n\n        this._startPromise = null;\n        return false;\n      }\n\n      try {\n        const { width, height } = await this._probeRealResolution();\n        this.actualWidth = width;\n        this.actualHeight = height;\n\n        let targetWidth = Math.min(width, 1920);\n        let targetHeight = Math.round((height / width) * targetWidth);\n        if (targetHeight > 1080) {\n          targetHeight = 1080;\n          targetWidth = Math.round((width / height) * targetHeight);\n        }\n        this.targetWidth = targetWidth;\n        this.targetHeight = targetHeight;\n\n        this.log(\"[WCR] Target:\", this.targetWidth, \"x\", this.targetHeight);\n\n        const fps = this.options.fps || 30;\n        this._frameDurationUs = Math.round(1_000_000 / fps);\n        const safeBitrate = this.options.videoBitrate || 10_000_000;\n\n        const overrideConfig =\n          this.options.videoEncoderConfig && typeof this.options.videoEncoderConfig === \"object\"\n            ? this.options.videoEncoderConfig\n            : null;\n\n        let videoConfig = null;\n        if (overrideConfig) {\n          const config = {\n            ...overrideConfig,\n            width: this.targetWidth,\n            height: this.targetHeight,\n          };\n          this.log(\"[WCR] Using override encoder config\", config);\n          try {\n            // Verify override is still supported at runtime\n            const support = await VideoEncoder.isConfigSupported(config);\n            if (support?.supported) {\n              videoConfig = {\n                config: support.config || config,\n                codec: (support.config || config).codec || overrideConfig.codec,\n                containerCodec: \"avc\",\n              };\n            }\n          } catch (err) {\n            this.warn(\"[WCR] Override config unsupported\", err);\n          }\n        }\n\n        if (!videoConfig) {\n          videoConfig = await this.chooseVideoEncoderConfig({\n            width: this.targetWidth,\n            height: this.targetHeight,\n            fps,\n            bitrate: safeBitrate,\n          });\n        }\n        this.selectedVideoCodec = videoConfig.codec;\n\n        const audioConfig = await this.prepareAudioEncoderConfig();\n        this._pendingAudioConfig = audioConfig;\n\n        this.log(\"[WCR] Audio config:\", audioConfig);\n\n        if (!audioConfig) {\n          this.warn(\"[WCR] No audio available\");\n          this.options.audioBitrate = undefined;\n          this.options.enableAudio = false;\n          this._audioReady = false;\n        }\n\n        if (audioConfig && this.audioTrack) {\n          this.audioProcessor = new MediaStreamTrackProcessor({\n            track: this.audioTrack,\n          });\n          this.audioReader = this.audioProcessor.readable.getReader();\n        }\n\n        // Init audio encoder first so _audioReady=true when both loops\n        // launch together — avoids audio prebuffering before video starts.\n        if (this._pendingAudioConfig) {\n          await this.initAudioEncoder(this._pendingAudioConfig);\n          this._audioReady = true;\n        }\n\n        this.videoProcessor = new MediaStreamTrackProcessor({\n          track: this.videoTrack,\n        });\n        this.videoReader = this.videoProcessor.readable.getReader();\n\n        this.log(\"[WCR] creating muxer...\");\n        this.muxer = new Mp4MuxerWrapper({\n          width: this.targetWidth,\n          height: this.targetHeight,\n          fps,\n          videoBitrate: this.options.videoBitrate,\n          audioBitrate: this.options.audioBitrate,\n          videoCodec: videoConfig.containerCodec,\n          audioCodec: this.options.enableAudio ? \"aac\" : undefined,\n          onChunk: this.options.onChunk,\n        });\n\n        if (this.options.enableAudio) {\n          this.muxer.enableAudio();\n        }\n\n        await this.muxer.start();\n        this.log(\"[WCR] muxer started\");\n\n        await this.initVideoEncoder(videoConfig.config, videoConfig.codec);\n\n        await Promise.resolve();\n\n        setTimeout(() => {\n          // Start both loops together so t=0 is aligned.\n          this._videoLoopPromise = this.readVideoLoop();\n          if (this.audioReader && this.options.enableAudio) {\n            this.log(\"[WCR] start audio loop (aligned with video)\");\n            this._audioLoopPromise = this.readAudioLoop();\n          }\n\n          if (this._startResolve) {\n            this._startResolve(true);\n            this._startResolve = null;\n          }\n        }, 30);\n\n        this.log(\"[WCR] start() done\");\n      } catch (err) {\n        this.err(\"[WCR] start() failed:\", err);\n        this.options.onError?.(err);\n        this.running = false;\n        this.cleanup();\n\n        if (this._startResolve) {\n          this._startResolve(false);\n          this._startResolve = null;\n        }\n      }\n    } catch (err) {\n      this.err(\"[WCR] start() outer error:\", err);\n      this.options.onError?.(err);\n      this.running = false;\n      this.cleanup();\n\n      if (this._startResolve) {\n        this._startResolve(false);\n        this._startResolve = null;\n      }\n    }\n\n    return this._startPromise;\n  }\n\n  async stop() {\n    this.log(\"[WCR] stop()\", this.frameCount);\n\n    if (this._stopping) {\n      this.log(\"[WCR] stop() already in progress\");\n      return;\n    }\n    this._stopping = true;\n\n    if (!this.running) {\n      this.log(\"[WCR] stop() called but recorder not running\");\n      this._stopping = false;\n      return;\n    }\n\n    // Soft stop: let loops finish current work\n    this.running = false;\n    this.paused = true;\n\n    try {\n      const loopPromises = [];\n      if (this._videoLoopPromise) loopPromises.push(this._videoLoopPromise);\n      if (this._audioLoopPromise) loopPromises.push(this._audioLoopPromise);\n\n      if (loopPromises.length) {\n        this.log(\"[WCR] Waiting for read loops to finish before flush...\");\n        await Promise.race([\n          Promise.allSettled(loopPromises),\n          new Promise((res) => setTimeout(res, 500)), // safety timeout\n        ]);\n      }\n    } catch (err) {\n      this.err(\"[WCR] Error while waiting for loops:\", err);\n    }\n\n    try {\n      if (this.videoEncoder && this.videoEncoder.state !== \"closed\") {\n        await this.videoEncoder.flush();\n      }\n      if (this.audioEncoder && this.audioEncoder.state !== \"closed\") {\n        await this.audioEncoder.flush();\n      }\n    } catch (err) {\n      this.err(\"[WCR] flush error:\", err);\n      this.options.onError?.(err);\n    }\n\n    try {\n      if (this.muxer) {\n        await this.muxer.finalize();\n        if (this.options.onFinalized) {\n          await this.options.onFinalized();\n        }\n      }\n    } catch (err) {\n      this.err(\"[WCR] muxer.finalize:\", err);\n      this.options.onError?.(err);\n    }\n\n    this.cleanup();\n\n    if (this.options.onStop) {\n      await this.options.onStop();\n    }\n\n    this._stopping = false;\n  }\n\n  pause() {\n    if (!this.running || this.paused) return;\n    this.paused = true;\n    this.pauseStartUs = performance.now() * 1000;\n  }\n\n  resume() {\n    if (!this.running || !this.paused) return;\n\n    const nowUs = performance.now() * 1000;\n    this.totalPausedDurationUs += nowUs - this.pauseStartUs;\n\n    this.paused = false;\n    this.justResumed = true;\n    this.muxer?.setPausedOffset(this.totalPausedDurationUs);\n  }\n\n  cleanup() {\n    this.log(\"[WCR] cleanup\");\n\n    try {\n      this.videoReader?.releaseLock();\n    } catch {}\n    try {\n      this.audioReader?.releaseLock();\n    } catch {}\n\n    try {\n      this.videoEncoder?.close();\n    } catch {}\n    try {\n      this.audioEncoder?.close();\n    } catch {}\n\n    // Release any AudioData objects that were queued before the audio encoder\n    // was ready.  If encoder init failed, these would otherwise be held in\n    // memory indefinitely because readAudioLoop never drained the buffer.\n    for (const item of this._prebufferedAudio) {\n      try { item.close?.(); } catch {}\n    }\n    this._prebufferedAudio = [];\n\n    this.videoProcessor = null;\n    this.audioProcessor = null;\n    this.videoReader = null;\n    this.audioReader = null;\n    this.videoEncoder = null;\n    this.audioEncoder = null;\n    this.videoTrack = null;\n    this.audioTrack = null;\n\n    this.videoTimestampOffsetUs = null;\n    this.videoFallbackStartMs = null;\n    this.firstVideoFrame = undefined;\n    this.selectedVideoCodec = null;\n    this._videoFrameIndex = 0;\n    this._videoStartUs = null;\n    this._frameDurationUs = null;\n    this.audioSamplesWritten = 0;\n    this.audioSampleRate = null;\n\n    this.resizeCanvas = null;\n    this.resizeCtx = null;\n    this._startPromise = null;\n    this._startResolve = null;\n\n    this._videoLoopPromise = null;\n    this._audioLoopPromise = null;\n  }\n\n  async initVideoEncoder(config, codecLabel) {\n    this.log(\"[WCR] initVideoEncoder()\", codecLabel);\n\n    this.videoEncoder = new VideoEncoder({\n      output: (chunk, meta) => {\n        this.muxer.addVideoChunk(chunk, meta);\n      },\n      error: (err) => {\n        this.err(\"[WCR] VideoEncoder error:\", err);\n        this.options.onError?.(err);\n      },\n    });\n\n    this.videoEncoder.configure(config);\n    this.log(\"[WCR] VideoEncoder configured\");\n  }\n\n  async initAudioEncoder(config) {\n    if (!config) return;\n\n    this.audioEncoder = new AudioEncoder({\n      output: (chunk, meta) => {\n        if (this.debug) this.log(\"[WCR] AUDIO\");\n        this.muxer.addAudioChunk(chunk, meta);\n      },\n      error: (err) => {\n        this.err(\"[WCR] AudioEncoder error:\", err);\n        this.options.onError?.(err);\n      },\n    });\n\n    this.audioEncoder.configure(config);\n  }\n\n  async prepareAudioEncoderConfig() {\n    if (!this.audioTrack) return null;\n\n    const settings = this.audioTrack.getSettings();\n    const sampleRate = settings.sampleRate || 48000;\n    const numberOfChannels = settings.channelCount || 2;\n\n    const candidateConfig = {\n      codec: \"mp4a.40.2\",\n      sampleRate,\n      numberOfChannels,\n      bitrate: this.options.audioBitrate || 128000,\n    };\n\n    try {\n      const support = await AudioEncoder.isConfigSupported(candidateConfig);\n      if (!support.supported) {\n        this.warn(\"[WCR] AAC unsupported\");\n        return null;\n      }\n      this.audioSampleRate = support.config?.sampleRate || candidateConfig.sampleRate;\n      return support.config || candidateConfig;\n    } catch {\n      this.warn(\"[WCR] AAC probe failed\");\n      return null;\n    }\n  }\n\n  async chooseVideoEncoderConfig({ width, height, fps, bitrate }) {\n    const base = {\n      width,\n      height,\n      framerate: fps,\n      bitrate,\n      bitrateMode: \"constant\",\n      latencyMode: \"realtime\",\n    };\n\n    const candidates = [\n      { codec: \"avc1.64002A\", containerCodec: \"avc\", hw: \"prefer-hardware\" },\n      { codec: \"avc1.4D401F\", containerCodec: \"avc\", hw: \"prefer-hardware\" },\n      { codec: \"avc1.42E01E\", containerCodec: \"avc\", hw: \"prefer-hardware\" },\n      { codec: \"avc1.64002A\", containerCodec: \"avc\", hw: \"prefer-software\" },\n      { codec: \"avc1.4D401F\", containerCodec: \"avc\", hw: \"prefer-software\" },\n      { codec: \"avc1.42E01E\", containerCodec: \"avc\", hw: \"prefer-software\" },\n    ];\n\n    for (const c of candidates) {\n      const config = { ...base, codec: c.codec, hardwareAcceleration: c.hw };\n      try {\n        const test = new VideoEncoder({\n          output() {},\n          error() {},\n        });\n        test.configure(config);\n        test.close();\n        this.log(\"[WCR] Selected encoder:\", c.codec, c.hw);\n        return {\n          config,\n          codec: c.codec,\n          containerCodec: c.containerCodec,\n        };\n      } catch (e) {}\n    }\n    throw new Error(\"WebCodecsRecorder: No supported H.264 encoder\");\n  }\n\n  async readVideoLoop() {\n    this.log(\"[WCR] video loop start\");\n    if (!this.videoReader || !this.videoEncoder) return;\n\n    const ensureResizeCanvas = () => {\n      if (!this.resizeCanvas) {\n        this.resizeCanvas = document.createElement(\"canvas\");\n        this.resizeCanvas.width = this.targetWidth;\n        this.resizeCanvas.height = this.targetHeight;\n        this.resizeCtx = this.resizeCanvas.getContext(\"2d\");\n        this.log(\"[WCR] resize canvas created\");\n      }\n    };\n\n    try {\n      while (this.running) {\n        if (this.paused) {\n          await new Promise((r) => setTimeout(r, 10));\n          continue;\n        }\n\n        const { value: frame, done } = await this.videoReader\n          .read()\n          .catch(() => ({ done: true }));\n\n        if (done || !frame) break;\n\n        if (frame.codedWidth === 0 || frame.codedHeight === 0) {\n          this.warn(\"[WCR] zero-size frame\");\n          this.options.onError?.({ type: \"video-lost\" });\n        }\n\n        ensureResizeCanvas();\n\n        this.resizeCtx.drawImage(\n          frame,\n          0,\n          0,\n          this.targetWidth,\n          this.targetHeight\n        );\n\n        if (!this._videoStartUs) this._videoStartUs = performance.now() * 1000;\n\n        const nowUs = performance.now() * 1000;\n        const elapsedUs = Math.max(\n          0,\n          nowUs - this._videoStartUs - (this.totalPausedDurationUs || 0)\n        );\n        const frameDurationUs = this._frameDurationUs || Math.round(1_000_000 / 30);\n        const targetIndex = Math.max(0, Math.floor(elapsedUs / frameDurationUs));\n\n        // If we're ahead of wall-clock, drop this frame.\n        if (targetIndex < this._videoFrameIndex) {\n          frame.close();\n          continue;\n        }\n\n        // If the tab was hidden or the system was heavily loaded, we can end\n        // up many frames behind wall-clock.  Filling the entire gap in one\n        // iteration would create hundreds of VideoFrames synchronously and\n        // overflow the encoder's internal queue.  Instead, skip ahead to at\n        // most MAX_GAP_FRAMES behind the target so the burst is bounded.\n        // The skipped indices advance _videoFrameIndex, preventing repeated\n        // catch-up attempts on the next iteration.\n        const MAX_GAP_FRAMES = 8; // ≈ 267 ms at 30 fps\n        const gap = targetIndex - this._videoFrameIndex;\n        if (gap > MAX_GAP_FRAMES) {\n          this.warn(`[WCR] skipping ${gap - MAX_GAP_FRAMES} frames (tab gap)`);\n          this._videoFrameIndex = targetIndex - MAX_GAP_FRAMES;\n        }\n\n        const startIndex = this._videoFrameIndex;\n        const endIndex = targetIndex;\n        for (let i = startIndex; i <= endIndex; i += 1) {\n          const tsUs = i * frameDurationUs;\n          const resized = new VideoFrame(this.resizeCanvas, {\n            timestamp: tsUs,\n            duration: frameDurationUs,\n          });\n\n          const keyFrame = i === 0 || (this.justResumed && i === startIndex);\n          if (this.justResumed && i === startIndex) {\n            this.justResumed = false;\n          }\n\n          this.videoEncoder.encode(resized, {\n            timestamp: tsUs,\n            keyFrame,\n          });\n          if (this.debug && (this.frameCount < 5 || this.frameCount % 300 === 0)) {\n            this.log(\"[WCR] video pts\", {\n              frame: this.frameCount,\n              tsUs,\n              durUs: frameDurationUs,\n              targetIndex,\n            });\n          }\n          this.frameCount++;\n          this._videoFrameIndex = i + 1;\n          resized.close();\n        }\n\n        frame.close();\n      }\n    } catch (err) {\n      this.err(\"[WCR] video loop error:\", err);\n    }\n\n    this.log(\"[WCR] video loop exit\", this.frameCount);\n  }\n\n  async readAudioLoop() {\n    while (this.paused && this.running) {\n      await new Promise((r) => setTimeout(r, 10));\n    }\n    if (!this.audioReader) return;\n    this.log(\"[WCR] audio loop start\");\n\n    const encodeAudioData = (audioData) => {\n      const sampleRate =\n        audioData.sampleRate || this.audioSampleRate || 48000;\n      const frames =\n        typeof audioData.numberOfFrames === \"number\"\n          ? audioData.numberOfFrames\n          : 0;\n      const tsUs = Math.round(\n        (this.audioSamplesWritten * 1_000_000) / sampleRate\n      );\n      const durUs = Math.round((frames * 1_000_000) / sampleRate);\n\n      this.audioEncoder.encode(audioData, {\n        timestamp: tsUs,\n      });\n      this.audioSamplesWritten += frames;\n\n      if (this.debug && (this.audioSamplesWritten === frames || this.audioSamplesWritten % (sampleRate * 10) < frames)) {\n        this.log(\"[WCR] audio pts\", {\n          samples: this.audioSamplesWritten,\n          tsUs,\n          durUs,\n          sampleRate,\n        });\n      }\n    };\n\n    try {\n      while (this.running) {\n        const { value: audioData, done } = await this.audioReader\n          .read()\n          .catch(() => ({ done: true }));\n\n        if (done || !audioData) break;\n        if (!this.audioTrack || this.audioTrack.readyState === \"ended\") {\n          this.warn(\"[WCR] audio lost\");\n          this.options.onError?.({ type: \"audio-lost\" });\n          break;\n        }\n\n        if (!this._audioReady) {\n          this._prebufferedAudio.push(audioData);\n          continue;\n        }\n\n        // Drop audio data while the recording is paused.  The sample counter\n        // is not advanced, so audio timestamps resume from the correct\n        // active-recording position when resume() is called — keeping audio\n        // and video aligned across any number of pause/resume cycles.\n        if (this.paused) {\n          audioData.close?.();\n          continue;\n        }\n\n        try {\n          if (this._prebufferedAudio.length) {\n            while (this._prebufferedAudio.length) {\n              const buffered = this._prebufferedAudio.shift();\n              try {\n                encodeAudioData(buffered);\n              } finally {\n                buffered.close?.();\n              }\n            }\n          }\n          encodeAudioData(audioData);\n        } catch (err) {\n          audioData.close?.();\n          this.options.onError?.(err);\n          break;\n        }\n\n        audioData.close?.();\n      }\n    } catch (err) {\n      this.err(\"[WCR] audio loop error:\", err);\n    }\n\n    this.log(\"[WCR] audio loop exit\");\n  }\n}\n"
  },
  {
    "path": "src/pages/RecorderOffscreen/RecorderOffscreen.jsx",
    "content": "import React, { useEffect, useState, useRef, useCallback } from \"react\";\nimport localforage from \"localforage\";\nimport { getUserMediaWithFallback } from \"../utils/mediaDeviceFallback\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\n// Get chunks store\nconst chunksStore = localforage.createInstance({\n  name: \"chunks\",\n});\n\nconst RecorderOffscreen = () => {\n  const isRestarting = useRef(false);\n  const isFinishing = useRef(false);\n  const sentLast = useRef(false);\n  const lastSize = useRef(0);\n  const index = useRef(0);\n  const lastTimecode = useRef(0);\n  const hasChunks = useRef(false);\n  const pausedStateRef = useRef(false);\n\n  const [started, setStarted] = useState(false);\n\n  // Main stream (recording)\n  const liveStream = useRef(null);\n\n  // Helper streams\n  const helperVideoStream = useRef(null);\n  const helperAudioStream = useRef(null);\n\n  // Audio controls, with refs to persist across renders\n  const aCtx = useRef(null);\n  const destination = useRef(null);\n  const audioInputSource = useRef(null);\n  const audioOutputSource = useRef(null);\n  const audioInputGain = useRef(null);\n  const audioOutputGain = useRef(null);\n\n  const recorder = useRef(null);\n\n  const isTab = useRef(false);\n  const tabID = useRef(null);\n  const quality = useRef(\"1080p\");\n  const fps = useRef(30);\n  const backupRef = useRef(false);\n\n  const pending = useRef([]);\n  const draining = useRef(false);\n  const lowStorageAbort = useRef(false);\n  const savedCount = useRef(0);\n\n  const lastEstimateAt = useRef(0);\n  const ESTIMATE_INTERVAL_MS = 5000;\n  const MIN_HEADROOM = 25 * 1024 * 1024;\n  const MAX_PENDING_BYTES = 8 * 1024 * 1024;\n  const pendingBytes = useRef(0);\n\n  const setRecordingTimingState = async (nextState) => {\n    try {\n      await chrome.storage.local.set(nextState);\n    } catch (err) {\n      console.warn(\n        \"[RecorderOffscreen] Failed to persist recording timing state\",\n        err,\n      );\n    }\n  };\n\n  async function canFitChunk(byteLength) {\n    const now = performance.now();\n    if (now - lastEstimateAt.current < ESTIMATE_INTERVAL_MS) {\n      return !lowStorageAbort.current;\n    }\n    lastEstimateAt.current = now;\n\n    try {\n      const { usage = 0, quota = 0 } = await navigator.storage.estimate();\n      const remaining = quota - usage;\n      return remaining > MIN_HEADROOM + (byteLength || 0);\n    } catch {\n      return !lowStorageAbort.current;\n    }\n  }\n\n  async function saveChunk(e, i) {\n    const ts = e.timecode ?? 0;\n\n    if (\n      savedCount.current > 0 &&\n      ts === lastTimecode.current &&\n      e.data.size === lastSize.current\n    ) {\n      return false;\n    }\n\n    if (!(await canFitChunk(e.data.size))) {\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n      });\n      chrome.runtime.sendMessage({ type: \"stop-recording-tab\" });\n      return false;\n    }\n\n    try {\n      await chunksStore.setItem(`chunk_${i}`, {\n        index: i,\n        chunk: e.data,\n        timestamp: ts,\n      });\n    } catch (err) {\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n      });\n      chrome.runtime.sendMessage({ type: \"stop-recording-tab\" });\n      return false;\n    }\n\n    lastTimecode.current = ts;\n    lastSize.current = e.data.size;\n    savedCount.current += 1;\n\n    if (backupRef.current) {\n      chrome.runtime.sendMessage({ type: \"write-file\", index: i });\n    }\n    return true;\n  }\n\n  async function drainQueue() {\n    if (draining.current) return;\n    draining.current = true;\n\n    try {\n      while (pending.current.length) {\n        if (lowStorageAbort.current) {\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          break;\n        }\n\n        const e = pending.current.shift();\n        pendingBytes.current -= e.data.size;\n\n        if (!(await canFitChunk(e.data.size))) {\n          if (!lowStorageAbort.current) {\n            chrome.runtime.sendMessage({\n              type: \"show-toast\",\n              message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n              timeout: 8000,\n            });\n          }\n          lowStorageAbort.current = true;\n          chrome.storage.local.set({\n            recording: false,\n            restarting: false,\n            tabRecordedID: null,\n            memoryError: true,\n          });\n          chrome.runtime.sendMessage({ type: \"stop-recording-tab\" });\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          break;\n        }\n\n        const i = index.current;\n        const saved = await saveChunk(e, i);\n        if (saved) index.current = i + 1;\n      }\n    } finally {\n      draining.current = false;\n    }\n  }\n\n  async function waitForDrain() {\n    while (draining.current || pending.current.length) {\n      await new Promise((r) => setTimeout(r, 10));\n    }\n  }\n  // End FLAG\n\n  async function startRecording() {\n    // Check that a recording is not already in progress\n    if (recorder.current !== null) return;\n    navigator.storage.persist();\n    // Check if the stream actually has data in it\n    if (helperVideoStream.current.getVideoTracks().length === 0) {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: \"No video tracks available\",\n      });\n      return;\n    }\n\n    chunksStore.clear();\n\n    lastTimecode.current = 0;\n    lastSize.current = 0;\n    hasChunks.current = false;\n    savedCount.current = 0;\n    pending.current = [];\n    draining.current = false;\n    lowStorageAbort.current = false;\n    pendingBytes.current = 0;\n\n    try {\n      const qualityValue = quality.current;\n\n      let audioBitsPerSecond = 128000;\n      let videoBitsPerSecond = 5000000;\n\n      if (qualityValue === \"4k\") {\n        audioBitsPerSecond = 192000;\n        videoBitsPerSecond = 40000000;\n      } else if (qualityValue === \"1080p\") {\n        audioBitsPerSecond = 192000;\n        videoBitsPerSecond = 8000000;\n      } else if (qualityValue === \"720p\") {\n        audioBitsPerSecond = 128000;\n        videoBitsPerSecond = 5000000;\n      } else if (qualityValue === \"480p\") {\n        audioBitsPerSecond = 96000;\n        videoBitsPerSecond = 2500000;\n      } else if (qualityValue === \"360p\") {\n        audioBitsPerSecond = 96000;\n        videoBitsPerSecond = 1000000;\n      } else if (qualityValue === \"240p\") {\n        audioBitsPerSecond = 64000;\n        videoBitsPerSecond = 500000;\n      }\n\n      // List all mimeTypes\n      const mimeTypes = [\n        \"video/webm;codecs=avc1\",\n        \"video/webm;codecs=vp8,opus\",\n        \"video/webm;codecs=vp9,opus\",\n        \"video/webm;codecs=vp9\",\n        \"video/webm;codecs=vp8\",\n        \"video/webm;codecs=h264\",\n        \"video/webm\",\n      ];\n\n      // Check if the browser supports any of the mimeTypes, make sure to select the first one that is supported from the list\n      let mimeType = mimeTypes.find((mimeType) =>\n        MediaRecorder.isTypeSupported(mimeType),\n      );\n\n      // If no mimeType is supported, throw an error\n      if (!mimeType) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-error\",\n          why: \"No supported mimeTypes available\",\n        });\n        return;\n      }\n\n      recorder.current = new MediaRecorder(liveStream.current, {\n        mimeType: mimeType,\n        audioBitsPerSecond: audioBitsPerSecond,\n        videoBitsPerSecond: videoBitsPerSecond,\n      });\n    } catch (err) {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: String(err),\n      });\n      return;\n    }\n\n    isRestarting.current = false;\n    index.current = 0;\n\n    try {\n      recorder.current.start(1000);\n    } catch (err) {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: String(err),\n      });\n      return;\n    }\n\n    const recordingStartTime = Date.now();\n    await setRecordingTimingState({\n      recording: true,\n      paused: false,\n      recordingStartTime,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    chrome.storage.local.set({\n      recording: true,\n      restarting: false,\n    });\n\n    recorder.current.onerror = (ev) => {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"mediarecorder\",\n        why: String(ev?.error || \"unknown\"),\n      });\n    };\n\n    recorder.current.onstop = async () => {\n      if (isRestarting.current) return;\n      await waitForDrain();\n      if (!sentLast.current) {\n        sentLast.current = true;\n        isFinishing.current = false;\n        chrome.runtime.sendMessage({ type: \"video-ready\" });\n      }\n    };\n\n    recorder.current.ondataavailable = (e) => {\n      if (!e || !e.data || !e.data.size) {\n        if (recorder.current && recorder.current.state === \"inactive\") {\n          chrome.storage.local.set({\n            recording: false,\n            restarting: false,\n            tabRecordedID: null,\n          });\n          chrome.runtime.sendMessage({ type: \"stop-recording-tab\" });\n        }\n        return;\n      }\n\n      if (lowStorageAbort.current) {\n        return;\n      }\n\n      if (!hasChunks.current) {\n        hasChunks.current = true;\n        lastTimecode.current = e.timecode ?? 0;\n        lastSize.current = e.data.size;\n      }\n\n      pending.current.push(e);\n      pendingBytes.current += e.data.size;\n      void drainQueue();\n    };\n\n    recorder.current.onpause = () => {\n      lastTimecode.current = 0;\n      lastSize.current = 0;\n    };\n\n    recorder.current.onresume = () => {\n      lastTimecode.current = 0;\n      lastSize.current = 0;\n    };\n\n    liveStream.current.getVideoTracks()[0].onended = () => {\n      const track = liveStream.current?.getVideoTracks()[0];\n      // Log detailed diagnostics for debugging\n      const diagnosticInfo = {\n        reason: \"liveStream-video-track-ended-offscreen\",\n        savedChunks: savedCount.current,\n        lastTimecode: lastTimecode.current,\n        trackLabel: track?.label || null,\n        trackReadyState: track?.readyState || null,\n      };\n      console.warn(\n        \"[RecorderOffscreen] liveStream video track ended\",\n        diagnosticInfo,\n      );\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        lastTrackEndEvent: diagnosticInfo,\n      });\n      chrome.runtime.sendMessage({\n        type: \"stop-recording-tab\",\n        reason: \"video-track-ended\",\n      });\n    };\n\n    helperVideoStream.current.getVideoTracks()[0].onended = () => {\n      const track = helperVideoStream.current?.getVideoTracks()[0];\n      // Log detailed diagnostics for debugging\n      const diagnosticInfo = {\n        reason: \"helperVideoStream-video-track-ended-offscreen\",\n        savedChunks: savedCount.current,\n        lastTimecode: lastTimecode.current,\n        trackLabel: track?.label || null,\n        trackReadyState: track?.readyState || null,\n      };\n      console.warn(\n        \"[RecorderOffscreen] helperVideoStream video track ended\",\n        diagnosticInfo,\n      );\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        lastTrackEndEvent: diagnosticInfo,\n      });\n      chrome.runtime.sendMessage({\n        type: \"stop-recording-tab\",\n        reason: \"video-track-ended\",\n      });\n    };\n  }\n\n  async function stopRecording() {\n    isFinishing.current = true;\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    if (recorder.current) {\n      try {\n        recorder.current.requestData();\n      } catch {}\n      recorder.current.stop();\n      await waitForDrain();\n      recorder.current = null;\n    }\n\n    if (liveStream.current) {\n      liveStream.current.getTracks().forEach((t) => t.stop());\n      liveStream.current = null;\n    }\n\n    if (helperVideoStream.current) {\n      helperVideoStream.current.getTracks().forEach((t) => t.stop());\n      helperVideoStream.current = null;\n    }\n\n    if (helperAudioStream.current) {\n      helperAudioStream.current.getTracks().forEach((t) => t.stop());\n      helperAudioStream.current = null;\n    }\n  }\n\n  const dismissRecording = async () => {\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    isRestarting.current = true;\n    if (recorder.current !== null) {\n      recorder.current.stop();\n      recorder.current = null;\n    }\n    window.close();\n  };\n\n  const restartRecording = async () => {\n    if (isRestarting.current) return false;\n    isRestarting.current = true;\n    pausedStateRef.current = false;\n    if (recorder.current !== null && recorder.current.state !== \"inactive\") {\n      await new Promise((resolve) => {\n        let done = false;\n        const finish = () => {\n          if (done) return;\n          done = true;\n          clearTimeout(timeoutId);\n          resolve();\n        };\n        const timeoutId = setTimeout(finish, 1600);\n        try {\n          recorder.current.addEventListener(\"stop\", finish, { once: true });\n        } catch {}\n        try {\n          recorder.current.stop();\n        } catch {\n          finish();\n        }\n      });\n    }\n    recorder.current = null;\n    pending.current = [];\n    pendingBytes.current = 0;\n    await chunksStore.clear();\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    isRestarting.current = false;\n    return true;\n  };\n\n  async function startAudioStream(id) {\n    const useExact = id && id !== \"none\";\n    const audioStreamOptions = {\n      mimeType: \"video/webm;codecs=vp8,opus\",\n      audio: useExact\n        ? {\n            deviceId: {\n              exact: id,\n            },\n          }\n        : true,\n    };\n\n    const { defaultAudioInputLabel, audioinput } =\n      await chrome.storage.local.get([\"defaultAudioInputLabel\", \"audioinput\"]);\n    const desiredLabel =\n      defaultAudioInputLabel ||\n      audioinput?.find((device) => device.deviceId === id)?.label ||\n      \"\";\n\n    const result = await getUserMediaWithFallback({\n      constraints: audioStreamOptions,\n      fallbacks:\n        useExact && desiredLabel\n          ? [\n              {\n                kind: \"audioinput\",\n                desiredDeviceId: id,\n                desiredLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultAudioInput: resolvedId,\n                    defaultAudioInputLabel: desiredLabel,\n                  });\n                },\n              },\n            ]\n          : [],\n    })\n      .then((stream) => {\n        return stream;\n      })\n      .catch((err) => {\n        // Try again without the device ID\n        const audioStreamOptions = {\n          mimeType: \"video/webm;codecs=vp8,opus\",\n          audio: true,\n        };\n\n        return navigator.mediaDevices\n          .getUserMedia(audioStreamOptions)\n          .then((stream) => {\n            return stream;\n          })\n          .catch((err) => {\n            return null;\n          });\n      });\n\n    return result;\n  }\n  // Set audio input volume\n  function setAudioInputVolume(volume) {\n    audioInputGain.current.gain.value = volume;\n  }\n\n  // Set audio output volume\n  function setAudioOutputVolume(volume) {\n    audioOutputGain.current.gain.value = volume;\n  }\n\n  async function startStreaming(data) {\n    // Get quality value\n    const qualityValue = quality.current;\n\n    let width = 1920;\n    let height = 1080;\n\n    if (qualityValue === \"4k\") {\n      width = 4096;\n      height = 2160;\n    } else if (qualityValue === \"1080p\") {\n      width = 1920;\n      height = 1080;\n    } else if (qualityValue === \"720p\") {\n      width = 1280;\n      height = 720;\n    } else if (qualityValue === \"480p\") {\n      width = 854;\n      height = 480;\n    } else if (qualityValue === \"360p\") {\n      width = 640;\n      height = 360;\n    } else if (qualityValue === \"240p\") {\n      width = 426;\n      height = 240;\n    }\n\n    const fpsValue = fps.current;\n    let fpsVal = parseInt(fpsValue);\n\n    // Check if fps is a number\n    if (isNaN(fpsVal)) {\n      fpsVal = 30;\n    }\n    // Check user permissions for camera and microphone individually\n    const permissions = await navigator.permissions.query({\n      name: \"camera\",\n    });\n    const permissions2 = await navigator.permissions.query({\n      name: \"microphone\",\n    });\n\n    try {\n      let userConstraints = {\n        audio: {\n          deviceId: data.defaultAudioInput,\n        },\n        video: {\n          deviceId: data.defaultVideoInput,\n          width: {\n            ideal: width,\n          },\n          height: {\n            ideal: height,\n          },\n          frameRate: {\n            ideal: fpsVal,\n          },\n        },\n      };\n      if (permissions.state === \"denied\") {\n        userConstraints.video = false;\n      }\n      if (permissions2.state === \"denied\") {\n        userConstraints.audio = false;\n      }\n\n      let userStream;\n\n      if (\n        permissions.state != \"denied\" &&\n        permissions2.state != \"denied\" &&\n        data.recordingType === \"camera\"\n      ) {\n        const {\n          defaultAudioInputLabel,\n          defaultVideoInputLabel,\n          audioinput,\n          videoinput,\n        } = await chrome.storage.local.get([\n          \"defaultAudioInputLabel\",\n          \"defaultVideoInputLabel\",\n          \"audioinput\",\n          \"videoinput\",\n        ]);\n        const desiredAudioLabel =\n          defaultAudioInputLabel ||\n          audioinput?.find(\n            (device) => device.deviceId === data.defaultAudioInput,\n          )?.label ||\n          \"\";\n        const desiredVideoLabel =\n          defaultVideoInputLabel ||\n          videoinput?.find(\n            (device) => device.deviceId === data.defaultVideoInput,\n          )?.label ||\n          \"\";\n\n        const hasAudioDevice =\n          data.defaultAudioInput && data.defaultAudioInput !== \"none\";\n        const hasVideoDevice =\n          data.defaultVideoInput && data.defaultVideoInput !== \"none\";\n        const cameraConstraints = {\n          ...userConstraints,\n          audio:\n            userConstraints.audio && hasAudioDevice\n              ? {\n                  ...userConstraints.audio,\n                  deviceId: { exact: data.defaultAudioInput },\n                }\n              : userConstraints.audio,\n          video:\n            userConstraints.video && hasVideoDevice\n              ? {\n                  ...userConstraints.video,\n                  deviceId: { exact: data.defaultVideoInput },\n                }\n              : userConstraints.video,\n        };\n\n        userStream = await getUserMediaWithFallback({\n          constraints: cameraConstraints,\n          fallbacks: [\n            hasVideoDevice && desiredVideoLabel\n              ? {\n                  kind: \"videoinput\",\n                  desiredDeviceId: data.defaultVideoInput,\n                  desiredLabel: desiredVideoLabel,\n                  onResolved: (resolvedId) => {\n                    chrome.storage.local.set({\n                      defaultVideoInput: resolvedId,\n                      defaultVideoInputLabel: desiredVideoLabel,\n                    });\n                  },\n                }\n              : null,\n            hasAudioDevice && desiredAudioLabel\n              ? {\n                  kind: \"audioinput\",\n                  desiredDeviceId: data.defaultAudioInput,\n                  desiredLabel: desiredAudioLabel,\n                  onResolved: (resolvedId) => {\n                    chrome.storage.local.set({\n                      defaultAudioInput: resolvedId,\n                      defaultAudioInputLabel: desiredAudioLabel,\n                    });\n                  },\n                }\n              : null,\n          ].filter(Boolean),\n        });\n      }\n\n      // Create an audio context, destination, and stream\n      aCtx.current = new AudioContext();\n      destination.current = aCtx.current.createMediaStreamDestination();\n      liveStream.current = new MediaStream();\n\n      const micstream = await startAudioStream(data.defaultAudioInput);\n\n      // Save the helper streams\n      if (data.recordingType === \"camera\") {\n        helperVideoStream.current = userStream;\n      } else {\n        let stream;\n        if (isTab.current === true) {\n          stream = await navigator.mediaDevices.getUserMedia({\n            audio: {\n              mandatory: {\n                chromeMediaSource: \"tab\",\n                chromeMediaSourceId: tabID.current,\n              },\n            },\n            video: {\n              mandatory: {\n                chromeMediaSource: \"tab\",\n                chromeMediaSourceId: tabID.current,\n                maxWidth: width,\n                maxHeight: height,\n                maxFrameRate: fpsVal,\n              },\n            },\n          });\n\n          // Check if the stream actually has data in it\n          if (stream.getVideoTracks().length === 0) {\n            chrome.runtime.sendMessage({\n              type: \"recording-error\",\n              error: \"stream-error\",\n              why: \"No video tracks available\",\n            });\n            return;\n          }\n        } else {\n          stream = await navigator.mediaDevices.getDisplayMedia({\n            audio: data.systemAudio,\n            video: {\n              frameRate: fpsVal,\n              displaySurface: \"monitor\",\n            },\n            selfBrowserSurface: \"exclude\",\n            systemAudio: \"include\",\n          });\n\n          // Check if the stream actually has data in it\n          if (stream.getVideoTracks().length === 0) {\n            chrome.runtime.sendMessage({\n              type: \"recording-error\",\n              error: \"stream-error\",\n              why: \"No video tracks available\",\n            });\n            return;\n          }\n        }\n        helperVideoStream.current = stream;\n\n        const surface = stream.getVideoTracks()[0].getSettings().displaySurface;\n        chrome.runtime.sendMessage({ type: \"set-surface\", surface: surface });\n      }\n      helperAudioStream.current = micstream;\n\n      // Check if micstream has an audio track\n      if (\n        helperAudioStream.current != null &&\n        helperAudioStream.current.getAudioTracks().length > 0\n      ) {\n        audioInputGain.current = aCtx.current.createGain();\n        audioInputSource.current = aCtx.current.createMediaStreamSource(\n          helperAudioStream.current,\n        );\n        audioInputSource.current\n          .connect(audioInputGain.current)\n          .connect(destination.current);\n      } else {\n        // No microphone available\n      }\n\n      if (helperAudioStream.current != null && !data.micActive) {\n        setAudioInputVolume(0);\n      }\n\n      // Check if stream has an audio track\n      if (helperVideoStream.current.getAudioTracks().length > 0) {\n        audioOutputGain.current = aCtx.current.createGain();\n        audioOutputSource.current = aCtx.current.createMediaStreamSource(\n          helperVideoStream.current,\n        );\n        audioOutputSource.current\n          .connect(audioOutputGain.current)\n          .connect(destination.current);\n      } else {\n        // No system audio available\n      }\n\n      // Add the tracks to the stream\n      liveStream.current.addTrack(\n        helperVideoStream.current.getVideoTracks()[0],\n      );\n      if (\n        (helperAudioStream.current != null &&\n          helperAudioStream.current.getAudioTracks().length > 0) ||\n        helperVideoStream.current.getAudioTracks().length > 0\n      ) {\n        liveStream.current.addTrack(\n          destination.current.stream.getAudioTracks()[0],\n        );\n      }\n\n      // Send message to go back to the previously active tab\n      setStarted(true);\n      chrome.runtime.sendMessage({ type: \"reset-active-tab\" });\n    } catch (err) {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"cancel-modal\",\n        why: String(err),\n      });\n    }\n  }\n\n  const setMic = async (result) => {\n    if (helperAudioStream.current != null) {\n      if (result.active) {\n        setAudioInputVolume(1);\n      } else {\n        setAudioInputVolume(0);\n      }\n    } else {\n      // No microphone available\n    }\n  };\n\n  const onMessage = useCallback(\n    (request, sender, sendResponse) => {\n      if (request.type === \"loaded\") {\n        backupRef.current = request.backup;\n        isTab.current = request.isTab;\n        quality.current = request.quality;\n        fps.current = request.fps;\n        if (request.isTab) {\n          tabID.current = request.tabID;\n        }\n        chrome.runtime.sendMessage({ type: \"get-streaming-data\" });\n      }\n      if (request.type === \"streaming-data\") {\n        startStreaming(JSON.parse(request.data));\n      } else if (request.type === \"start-recording-tab\") {\n        startRecording();\n      } else if (request.type === \"restart-recording-tab\") {\n        Promise.resolve(restartRecording())\n          .then((restarted) => {\n            if (!restarted) {\n              sendResponse?.({ ok: false, error: \"restart-teardown-failed\" });\n              return;\n            }\n            sendResponse?.({ ok: true, restarted: true });\n          })\n          .catch((error) => {\n            sendResponse?.({\n              ok: false,\n              error: error?.message || String(error),\n            });\n          });\n        return true;\n      } else if (request.type === \"stop-recording-tab\") {\n        stopRecording();\n        sendResponse?.({ ok: true });\n        return true;\n      } else if (request.type === \"set-mic-active-tab\") {\n        setMic(request);\n      } else if (request.type === \"set-audio-output-volume\") {\n        setAudioOutputVolume(request.volume);\n      } else if (request.type === \"pause-recording-tab\") {\n        if (!recorder.current) return;\n        if (pausedStateRef.current) return;\n        const now = Date.now();\n        recorder.current.pause();\n        pausedStateRef.current = true;\n        void setRecordingTimingState({\n          paused: true,\n          pausedAt: now,\n        });\n      } else if (request.type === \"resume-recording-tab\") {\n        if (!recorder.current) return;\n        if (!pausedStateRef.current) return;\n        recorder.current.resume();\n        const now = Date.now();\n        pausedStateRef.current = false;\n        void (async () => {\n          try {\n            const { pausedAt, totalPausedMs } = await chrome.storage.local.get([\n              \"pausedAt\",\n              \"totalPausedMs\",\n            ]);\n            const additional = pausedAt ? Math.max(0, now - pausedAt) : 0;\n            await setRecordingTimingState({\n              paused: false,\n              pausedAt: null,\n              totalPausedMs: (totalPausedMs || 0) + additional,\n            });\n          } catch (err) {\n            console.warn(\n              \"[RecorderOffscreen] Failed to update resume timing state\",\n              err,\n            );\n          }\n        })();\n      } else if (request.type === \"play-beep-offscreen\") {\n        // Reuse this offscreen document for beep sounds so Chrome doesn't\n        // need to create a second offscreen document (only one is allowed).\n        try {\n          const audioUrl = chrome.runtime.getURL(\"assets/sounds/beep.mp3\");\n          const audio = new Audio(audioUrl);\n          audio.volume = 0.5;\n          audio.play().catch(() => {});\n        } catch {}\n        sendResponse?.({ ok: true });\n        return true;\n      } else if (request.type === \"dismiss-recording\") {\n        dismissRecording();\n      }\n    },\n    [recorder.current],\n  );\n\n  useEffect(() => {\n    // Event listener (extension messaging)\n    chrome.runtime.onMessage.addListener(onMessage);\n\n    return () => {\n      chrome.runtime.onMessage.removeListener(onMessage);\n    };\n  }, []);\n\n  return <div className=\"wrap\"></div>;\n};\n\nexport default RecorderOffscreen;\n"
  },
  {
    "path": "src/pages/RecorderOffscreen/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/RecorderOffscreen/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport RecorderOffscreen from \"./RecorderOffscreen\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<RecorderOffscreen />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Region/Recorder.jsx",
    "content": "import React, { useEffect, useRef, useCallback } from \"react\";\nimport { getUserMediaWithFallback } from \"../utils/mediaDeviceFallback\";\nimport { WebCodecsRecorder } from \"../Recorder/webcodecs/WebCodecsRecorder\";\nimport {\n  debugRecordingEvent,\n  resetRecordingDebugSession,\n  isRecordingDebugEnabled,\n  hydrateRecordingDebugFlag,\n} from \"../utils/recordingDebug\";\nimport {\n  probeFastRecorderSupport,\n  shouldUseFastRecorder,\n  getFastRecorderStickyState,\n  markFastRecorderFailure,\n  validateFastRecorderOutputBlob,\n} from \"../../media/fastRecorderGate\";\n\nimport localforage from \"localforage\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\",\n  version: 1,\n});\n\n// Get chunks store\nconst chunksStore = localforage.createInstance({\n  name: \"chunks\",\n});\n\n// Debug flag for logging\n//   window.SCREENITY_DEBUG_RECORDER = true;\nconst DEBUG_RECORDER =\n  typeof window !== \"undefined\" ? !!window.SCREENITY_DEBUG_RECORDER : false;\nconst logPrefix = \"[Screenity Region Recorder]\";\n\nfunction debug(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.log(logPrefix, ...args);\n}\n\nfunction debugWarn(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.warn(logPrefix, ...args);\n}\n\nfunction debugError(...args) {\n  if (!DEBUG_RECORDER) return;\n  // eslint-disable-next-line no-console\n  console.error(logPrefix, ...args);\n}\n\nfunction buildTrackSnapshot(track) {\n  if (!track) return null;\n  const settings =\n    typeof track.getSettings === \"function\" ? track.getSettings() : {};\n  const constraints =\n    typeof track.getConstraints === \"function\" ? track.getConstraints() : {};\n  const capabilities =\n    typeof track.getCapabilities === \"function\" ? track.getCapabilities() : {};\n  return {\n    label: track.label,\n    settings,\n    constraints,\n    capabilities,\n  };\n}\n\nfunction logRecordingSnapshot(label, data) {\n  if (!DEBUG_RECORDER && !isRecordingDebugEnabled()) return;\n  debug(`Recording snapshot: ${label}`, data);\n}\n\nconst selectMimeType = (preferredCodec) => {\n  const preferred = (preferredCodec || \"\").toLowerCase();\n  const mimeTypes = [\n    \"video/webm;codecs=vp9,opus\",\n    \"video/webm;codecs=vp9\",\n    \"video/webm;codecs=vp8,opus\",\n    \"video/webm;codecs=vp8\",\n    \"video/webm;codecs=avc1\",\n    \"video/webm;codecs=h264\",\n    \"video/webm\",\n  ];\n  const ordered = preferred\n    ? mimeTypes\n        .filter((type) => type.includes(preferred))\n        .concat(mimeTypes.filter((type) => !type.includes(preferred)))\n    : mimeTypes;\n  return ordered.find((type) => MediaRecorder.isTypeSupported(type)) || null;\n};\n\nconst getCodecLabel = (mimeType) => {\n  if (!mimeType) return \"unknown\";\n  if (mimeType.includes(\"vp9\")) return \"vp9\";\n  if (mimeType.includes(\"vp8\")) return \"vp8\";\n  if (mimeType.includes(\"avc1\") || mimeType.includes(\"h264\")) return \"h264\";\n  return \"unknown\";\n};\n\nconst Recorder = () => {\n  const isRestarting = useRef(false);\n  const isDismissing = useRef(false);\n  const isFinishing = useRef(false);\n  const isFinished = useRef(false);\n  const sentLast = useRef(false);\n  const index = useRef(0);\n  const lastTimecode = useRef(0);\n  const hasChunks = useRef(false);\n  const lastSize = useRef(0);\n  const pausedStateRef = useRef(false);\n  const recordingStartTimeRef = useRef(null);\n  const recordingTick = useRef(null);\n\n  // Main stream (recording)\n  const liveStream = useRef(null);\n\n  // Helper streams\n  const helperVideoStream = useRef(null);\n  const helperAudioStream = useRef(null);\n  const startRetryTimer = useRef(null);\n  const startRetryAttempts = useRef(0);\n\n  // Audio controls, with refs to persist across renders\n  const aCtx = useRef(null);\n  const destination = useRef(null);\n  const audioInputSource = useRef(null);\n  const audioOutputSource = useRef(null);\n  const audioInputGain = useRef(null);\n  const audioOutputGain = useRef(null);\n\n  const recorder = useRef(null);\n  const useWebCodecs = useRef(false);\n  const recdbgSessionRef = useRef(null);\n\n  // Target\n  const target = useRef(null);\n\n  // Recording ref\n  const recordingRef = useRef();\n  const regionRef = useRef();\n  const backupRef = useRef(false);\n\n  const pending = useRef([]);\n  const draining = useRef(false);\n  const lowStorageAbort = useRef(false);\n  const savedCount = useRef(0);\n\n  const lastEstimateAt = useRef(0);\n  const ESTIMATE_INTERVAL_MS = 5000;\n  const MIN_HEADROOM = 25 * 1024 * 1024;\n  const MAX_PENDING_BYTES = 8 * 1024 * 1024;\n  const pendingBytes = useRef(0);\n  const recordingIdRef = useRef(null);\n\n  const clearStartRetry = () => {\n    if (startRetryTimer.current) {\n      clearTimeout(startRetryTimer.current);\n      startRetryTimer.current = null;\n    }\n    startRetryAttempts.current = 0;\n  };\n\n  const persistRegionStartDebug = (payload) => {\n    try {\n      chrome.storage.local.set({\n        lastRegionStartFailure: {\n          ts: Date.now(),\n          ...payload,\n        },\n      });\n    } catch {}\n  };\n\n  const setRegionRestartPhase = async (phase, details = {}) => {\n    try {\n      await chrome.storage.local.set({\n        lastRegionRestartPhase: {\n          phase,\n          ts: Date.now(),\n          ...details,\n        },\n      });\n    } catch {}\n  };\n\n  const sendStopWithReason = (reason) => {\n    chrome.runtime.sendMessage({ type: \"stop-recording-tab\", reason });\n  };\n\n  const setRecordingTimingState = async (nextState) => {\n    try {\n      await chrome.storage.local.set(nextState);\n    } catch (err) {\n      console.warn(\n        \"[Region Recorder] Failed to persist recording timing state\",\n        err,\n      );\n    }\n  };\n\n  const startRecordingTick = () => {\n    if (recordingTick.current) clearInterval(recordingTick.current);\n    recordingTick.current = setInterval(async () => {\n      if (!recordingStartTimeRef.current) return;\n      const { totalPausedMs, pausedAt, paused } =\n        await chrome.storage.local.get([\"totalPausedMs\", \"pausedAt\", \"paused\"]);\n      const now = Date.now();\n      const basePaused = Number(totalPausedMs) || 0;\n      const extraPaused =\n        paused && pausedAt ? Math.max(0, now - Number(pausedAt)) : 0;\n      const elapsed = Math.max(\n        0,\n        now - recordingStartTimeRef.current - basePaused - extraPaused,\n      );\n      chrome.storage.local.set({ recordingNow: now, recordingDuration: elapsed });\n    }, 1000);\n  };\n\n  const stopRecordingTick = () => {\n    if (recordingTick.current) {\n      clearInterval(recordingTick.current);\n      recordingTick.current = null;\n    }\n  };\n\n  async function canFitChunk(byteLength) {\n    const now = performance.now();\n    if (now - lastEstimateAt.current < ESTIMATE_INTERVAL_MS) {\n      return !lowStorageAbort.current;\n    }\n    lastEstimateAt.current = now;\n\n    try {\n      const { usage = 0, quota = 0 } = await navigator.storage.estimate();\n      const remaining = quota - usage;\n      return remaining > MIN_HEADROOM + (byteLength || 0);\n    } catch {\n      return !lowStorageAbort.current;\n    }\n  }\n\n  async function saveChunk(e, i) {\n    const ts = e.timecode ?? 0;\n\n    // FLAG: disabled duplicate chunk check due to issues with some devices\n    // if (\n    //   savedCount.current > 0 &&\n    //   ts === lastTimecode.current &&\n    //   e.data.size === lastSize.current\n    // ) {\n    //   return false;\n    // }\n\n    if (!(await canFitChunk(e.data.size))) {\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n      });\n      sendStopWithReason(\"region-low-storage-headroom\");\n      // Reload this iframe\n      window.location.reload();\n      return false;\n    }\n\n    try {\n      await chunksStore.setItem(`chunk_${i}`, {\n        index: i,\n        chunk: e.data,\n        timestamp: ts,\n      });\n    } catch (err) {\n      if (!lowStorageAbort.current) {\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n          timeout: 8000,\n        });\n      }\n      lowStorageAbort.current = true;\n      chrome.storage.local.set({\n        recording: false,\n        restarting: false,\n        tabRecordedID: null,\n        memoryError: true,\n      });\n      sendStopWithReason(\"region-save-chunk-failed\");\n      // Reload this iframe\n      window.location.reload();\n      return false;\n    }\n\n    lastTimecode.current = ts;\n    lastSize.current = e.data.size;\n    savedCount.current += 1;\n\n    if (backupRef.current) {\n      chrome.runtime.sendMessage({ type: \"write-file\", index: i });\n    }\n    return true;\n  }\n\n  const rebuildBlobFromChunks = async () => {\n    const items = [];\n    await chunksStore.ready();\n    await chunksStore.iterate((value) => (items.push(value), undefined));\n    items.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));\n    const parts = items.map((c) =>\n      c.chunk instanceof Blob ? c.chunk : new Blob([c.chunk]),\n    );\n    if (!parts.length) return null;\n    const first = parts[0];\n    const inferredType = first?.type || \"video/mp4\";\n    return new Blob(parts, { type: inferredType });\n  };\n\n  const updateFreeFinalizeStatus = async (stage, percent = 0, error = null) => {\n    try {\n      const recordingId =\n        recordingIdRef.current ||\n        (await chrome.storage.local.get([\"fastRecorderActiveRecordingId\"]))\n          .fastRecorderActiveRecordingId;\n      if (!recordingId) return;\n      const key = `freeFinalizeStatus:${recordingId}`;\n      const existing = await chrome.storage.local.get([key]);\n      const current = existing[key];\n      if (\n        current &&\n        (current.stage === \"ready\" || current.stage === \"chunks_ready\") &&\n        (stage === \"stopping\" || stage === \"finalizing\")\n      ) {\n        return;\n      }\n      await chrome.storage.local.set({\n        [key]: {\n          recordingId,\n          stage,\n          percent,\n          updatedAt: Date.now(),\n          error: error || undefined,\n        },\n      });\n    } catch {}\n  };\n\n  async function drainQueue() {\n    if (draining.current) return;\n    draining.current = true;\n\n    try {\n      while (pending.current.length) {\n        if (lowStorageAbort.current) {\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          break;\n        }\n\n        const e = pending.current.shift();\n        pendingBytes.current -= e.data.size;\n\n        if (!(await canFitChunk(e.data.size))) {\n          if (!lowStorageAbort.current) {\n            chrome.runtime.sendMessage({\n              type: \"show-toast\",\n              message: chrome.i18n.getMessage(\"toastStorageCritical\"),\n              timeout: 8000,\n            });\n          }\n          lowStorageAbort.current = true;\n          chrome.storage.local.set({\n            recording: false,\n            restarting: false,\n            tabRecordedID: null,\n            memoryError: true,\n          });\n          sendStopWithReason(\"region-drainqueue-low-storage\");\n          pending.current.length = 0;\n          pendingBytes.current = 0;\n          // Reload this iframe\n          window.location.reload();\n          break;\n        }\n\n        const i = index.current;\n        const saved = await saveChunk(e, i);\n        if (saved) index.current = i + 1;\n      }\n    } finally {\n      draining.current = false;\n    }\n  }\n\n  async function waitForDrain() {\n    while (draining.current || pending.current.length) {\n      await new Promise((r) => setTimeout(r, 10));\n    }\n  }\n\n  useEffect(() => {\n    window.parent.postMessage(\n      {\n        type: \"screenity-region-capture-loaded\",\n      },\n      \"*\",\n    );\n  }, []);\n\n  // Receive post message from parent (this is an iframe)\n  useEffect(() => {\n    const onMessage = (event) => {\n      if (event.data.type === \"crop-target\") {\n        target.current = event.data.target;\n        regionRef.current = true;\n      } else if (event.data.type === \"restart-recording\") {\n        restartRecording();\n      }\n    };\n    window.addEventListener(\"message\", onMessage);\n\n    return () => {\n      window.removeEventListener(\"message\", onMessage);\n    };\n  }, []);\n\n  async function startRecording() {\n    // Check that a recording is not already in progress\n    if (recorder.current !== null) return;\n    persistRegionStartDebug({ stage: \"start-recording-enter\" });\n\n    const hasHelperVideoTrack =\n      !!helperVideoStream.current &&\n      typeof helperVideoStream.current.getVideoTracks === \"function\" &&\n      helperVideoStream.current.getVideoTracks().length > 0;\n    const hasLiveVideoTrack =\n      !!liveStream.current &&\n      typeof liveStream.current.getVideoTracks === \"function\" &&\n      liveStream.current.getVideoTracks().length > 0;\n\n    if (!hasHelperVideoTrack || !hasLiveVideoTrack) {\n      if (startRetryAttempts.current < 50) {\n        startRetryAttempts.current += 1;\n        if (startRetryTimer.current) {\n          clearTimeout(startRetryTimer.current);\n        }\n        startRetryTimer.current = setTimeout(() => {\n          startRetryTimer.current = null;\n          startRecording();\n        }, 100);\n        return;\n      }\n\n      clearStartRetry();\n      persistRegionStartDebug({\n        stage: \"preflight-stream-ready-timeout\",\n        why: \"Capture stream is not ready yet\",\n        hasHelperVideoTrack,\n        hasLiveVideoTrack,\n      });\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: \"Capture stream is not ready yet\",\n      });\n      window.location.reload();\n      return;\n    }\n\n    clearStartRetry();\n    navigator.storage.persist();\n    isFinishing.current = false;\n    sentLast.current = false;\n    lastTimecode.current = 0;\n    lastSize.current = 0;\n    hasChunks.current = false;\n    isFinished.current = false;\n    savedCount.current = 0;\n    pending.current = [];\n    draining.current = false;\n    lowStorageAbort.current = false;\n    pendingBytes.current = 0;\n    // Check if the stream actually has data in it\n    try {\n      if (helperVideoStream.current.getVideoTracks().length === 0) {\n        persistRegionStartDebug({\n          stage: \"preflight-helper-video-empty\",\n          why: \"No video tracks available\",\n          helperVideo: buildTrackSnapshot(\n            helperVideoStream.current?.getVideoTracks?.()[0] || null,\n          ),\n          liveVideo: buildTrackSnapshot(\n            liveStream.current?.getVideoTracks?.()[0] || null,\n          ),\n        });\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-error\",\n          why: \"No video tracks available\",\n        });\n\n        // Reload this iframe\n        window.location.reload();\n        return;\n      }\n    } catch (err) {\n      persistRegionStartDebug({\n        stage: \"preflight-helper-video-throw\",\n        why: String(err),\n        helperVideo: buildTrackSnapshot(\n          helperVideoStream.current?.getVideoTracks?.()[0] || null,\n        ),\n        liveVideo: buildTrackSnapshot(\n          liveStream.current?.getVideoTracks?.()[0] || null,\n        ),\n      });\n      if (useWebCodecs.current) {\n        await chrome.storage.local.set({\n          useWebCodecsRecorder: false,\n          lastWebCodecsFailureAt: Date.now(),\n          lastWebCodecsFailureCode: \"start-exception\",\n        });\n        chrome.runtime.sendMessage({\n          type: \"show-toast\",\n          message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n        });\n      }\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: String(err),\n      });\n\n      // Reload this iframe\n      window.location.reload();\n      return;\n    }\n\n    await chunksStore.clear();\n\n    try {\n      const { qualityValue } = await chrome.storage.local.get([\"qualityValue\"]);\n      const { fpsValue } = await chrome.storage.local.get([\"fpsValue\"]);\n      const activeVideoTrack =\n        liveStream.current?.getVideoTracks?.()[0] ||\n        helperVideoStream.current?.getVideoTracks?.()[0] ||\n        null;\n      const trackSettings =\n        activeVideoTrack && typeof activeVideoTrack.getSettings === \"function\"\n          ? activeVideoTrack.getSettings()\n          : {};\n      const width =\n        Number.isFinite(trackSettings.width) && trackSettings.width > 0\n          ? Math.floor(trackSettings.width)\n          : 1920;\n      const height =\n        Number.isFinite(trackSettings.height) && trackSettings.height > 0\n          ? Math.floor(trackSettings.height)\n          : 1080;\n      let fps = Number.parseInt(fpsValue, 10);\n      if (!Number.isFinite(fps) || fps <= 0) {\n        fps =\n          Number.isFinite(trackSettings.frameRate) && trackSettings.frameRate > 0\n            ? Math.round(trackSettings.frameRate)\n            : 30;\n      }\n\n      let audioBitsPerSecond = 128000;\n      let videoBitsPerSecond = 5000000;\n\n      if (qualityValue === \"4k\") {\n        audioBitsPerSecond = 192000;\n        videoBitsPerSecond = 40000000;\n      } else if (qualityValue === \"1080p\") {\n        audioBitsPerSecond = 192000;\n        videoBitsPerSecond = 8000000;\n      } else if (qualityValue === \"720p\") {\n        audioBitsPerSecond = 128000;\n        videoBitsPerSecond = 5000000;\n      } else if (qualityValue === \"480p\") {\n        audioBitsPerSecond = 96000;\n        videoBitsPerSecond = 2500000;\n      } else if (qualityValue === \"360p\") {\n        audioBitsPerSecond = 96000;\n        videoBitsPerSecond = 1000000;\n      } else if (qualityValue === \"240p\") {\n        audioBitsPerSecond = 64000;\n        videoBitsPerSecond = 500000;\n      }\n\n      // List all mimeTypes\n      const mimeTypes = [\n        \"video/webm;codecs=avc1\",\n        \"video/webm;codecs=vp8,opus\",\n        \"video/webm;codecs=vp9,opus\",\n        \"video/webm;codecs=vp9\",\n        \"video/webm;codecs=vp8\",\n        \"video/webm;codecs=h264\",\n        \"video/webm\",\n      ];\n\n      // Check if the browser supports any of the mimeTypes, make sure to select the first one that is supported from the list\n      let mimeType = mimeTypes.find((mimeType) =>\n        MediaRecorder.isTypeSupported(mimeType),\n      );\n\n      const recordingId = `${Date.now()}-${Math.random()\n        .toString(16)\n        .slice(2, 8)}`;\n      recordingIdRef.current = recordingId;\n\n      await chrome.storage.local.set({\n        fastRecorderActiveRecordingId: recordingId,\n        fastRecorderInUse: false,\n        fastRecorderValidationFailed: false,\n        fastRecorderValidation: null,\n      });\n\n      const { useWebCodecsRecorder } = await chrome.storage.local.get([\n        \"useWebCodecsRecorder\",\n      ]);\n      const userSetting = useWebCodecsRecorder === true ? true : false;\n      const stickyState = await getFastRecorderStickyState();\n      const probeResult = await probeFastRecorderSupport();\n      const shouldUseFast = shouldUseFastRecorder(\n        userSetting,\n        probeResult,\n        stickyState,\n      );\n      const selectedVideoConfig =\n        probeResult?.details?.selectedVideoConfig || null;\n      let recorderToken = 0;\n      let codecFallbackTriggered = false;\n      let mediaRecorderStartAt = Date.now();\n      let recdbgChunkCount = 0;\n      let recdbgTotalBytes = 0;\n\n      await chrome.storage.local.set({\n        fastRecorderDecision: {\n          shouldUseFast,\n          reasons: probeResult.reasons,\n          stickyDisabled: stickyState.disabled,\n        },\n        fastRecorderStatus: {\n          userSetting,\n          probe: {\n            ok: probeResult.ok,\n            reasons: probeResult.reasons,\n            details: probeResult.details,\n            at: probeResult.at || Date.now(),\n          },\n          decision: {\n            useFast: shouldUseFast,\n            why:\n              userSetting === false\n                ? \"user_disabled\"\n                : stickyState?.disabled && userSetting !== true\n                ? \"sticky_disabled\"\n                : probeResult.ok\n                ? \"probe_ok\"\n                : \"probe_failed\",\n            at: Date.now(),\n          },\n          disabled: stickyState.disabled,\n          disabledReason: stickyState.reason || null,\n          disabledDetails: stickyState.details || null,\n          disabledAt: stickyState.disabledAt || null,\n          updatedAt: Date.now(),\n        },\n      });\n\n      const resetChunkState = async () => {\n        await chunksStore.clear();\n        index.current = 0;\n        pending.current = [];\n        pendingBytes.current = 0;\n        savedCount.current = 0;\n        hasChunks.current = false;\n        lastTimecode.current = 0;\n        lastSize.current = 0;\n        sentLast.current = false;\n      };\n\n      const waitForSavedChunks = async (timeoutMs = 4000) => {\n        if (savedCount.current > 0) return true;\n        const deadline = Date.now() + timeoutMs;\n        while (Date.now() < deadline) {\n          await new Promise((resolve) => setTimeout(resolve, 100));\n          if (savedCount.current > 0) return true;\n        }\n        return savedCount.current > 0;\n      };\n\n      const waitForTrailingChunks = async (timeoutMs = 1600) => {\n        const deadline = Date.now() + timeoutMs;\n        let stableRounds = 0;\n        let lastSaved = savedCount.current;\n        while (Date.now() < deadline) {\n          await waitForDrain();\n          await new Promise((resolve) => setTimeout(resolve, 120));\n          await waitForDrain();\n          const currentSaved = savedCount.current;\n          if (currentSaved === lastSaved) {\n            stableRounds += 1;\n            if (stableRounds >= 2) break;\n          } else {\n            stableRounds = 0;\n            lastSaved = currentSaved;\n          }\n        }\n      };\n\n      const handleWebCodecsChunk = async (blob, timestampUs) => {\n        if (lowStorageAbort.current) return;\n        const timecodeMs = timestampUs ? Math.floor(timestampUs / 1000) : 0;\n        recdbgChunkCount += 1;\n        recdbgTotalBytes += blob.size || 0;\n        if (recdbgChunkCount <= 3) {\n          debugRecordingEvent(recdbgSessionRef, \"chunk\", {\n            index: recdbgChunkCount,\n            size: blob.size,\n            timecode: timecodeMs,\n          });\n        } else if (recdbgChunkCount % 10 === 0) {\n          debugRecordingEvent(recdbgSessionRef, \"chunk-progress\", {\n            count: recdbgChunkCount,\n            totalBytes: recdbgTotalBytes,\n          });\n        }\n\n        pending.current.push({ data: blob, timecode: timecodeMs });\n        pendingBytes.current += blob.size;\n\n        if (pendingBytes.current > MAX_PENDING_BYTES) {\n          try {\n            await drainQueue();\n          } catch {}\n        }\n        void drainQueue();\n      };\n\n      const startWebCodecsRecorder = async () => {\n        const hasAudioTrack =\n          !!liveStream.current &&\n          typeof liveStream.current.getAudioTracks === \"function\" &&\n          liveStream.current.getAudioTracks().length > 0;\n\n        useWebCodecs.current = true;\n        await chrome.storage.local.set({ fastRecorderInUse: true });\n\n        recorder.current = new WebCodecsRecorder(liveStream.current, {\n          width,\n          height,\n          fps,\n          videoBitrate: videoBitsPerSecond,\n          audioBitrate: hasAudioTrack ? audioBitsPerSecond : undefined,\n          enableAudio: hasAudioTrack,\n          videoEncoderConfig: selectedVideoConfig,\n          debug: DEBUG_RECORDER,\n          onFinalized: async () => {\n            await waitForDrain();\n            const hasSavedChunks = await waitForSavedChunks();\n            if (!hasSavedChunks) {\n              persistRegionStartDebug({\n                stage: \"finalize-no-chunks\",\n                recorderType: \"WebCodecsRecorder\",\n                savedCount: savedCount.current,\n                pending: pending.current.length,\n              });\n              chrome.runtime.sendMessage({\n                type: \"recording-error\",\n                error: \"stream-error\",\n                why: \"No recording data was generated\",\n              });\n              return;\n            }\n            await updateFreeFinalizeStatus(\"chunks_ready\", 95);\n            let validation = null;\n            try {\n              const blob = await rebuildBlobFromChunks();\n              validation = await validateFastRecorderOutputBlob(blob, {\n                minBytes: 64 * 1024,\n                timeoutMs: 4000,\n                videoCodec: recorder.current?.selectedVideoCodec || undefined,\n                audioCodec: hasAudioTrack ? \"mp4a.40.2\" : null,\n                recordingId,\n              });\n              debugRecordingEvent(recdbgSessionRef, \"fast-recorder-validate\", {\n                validation,\n              });\n            } catch (err) {\n              validation = {\n                ok: false,\n                hardFail: true,\n                reasons: [\"validation-exception\"],\n                details: { error: String(err) },\n              };\n            }\n\n            if (validation && !validation.ok) {\n              await markFastRecorderFailure(\"validation-failed\", validation);\n              await chrome.storage.local.set({\n                useWebCodecsRecorder: false,\n                lastWebCodecsFailureAt: Date.now(),\n                lastWebCodecsFailureCode: \"validation-failed\",\n              });\n              const hardFail = Boolean(validation.hardFail);\n              await chrome.storage.local.set({\n                fastRecorderValidationFailed: hardFail,\n                fastRecorderValidation: validation,\n              });\n              await updateFreeFinalizeStatus(\n                \"failed\",\n                100,\n                validation.reasons || \"validation-failed\",\n              );\n              chrome.runtime.sendMessage({\n                type: \"show-toast\",\n                message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n              });\n              if (hardFail) {\n                chrome.runtime.sendMessage({\n                  type: \"fast-recorder-hard-fail\",\n                  recordingId,\n                });\n                if (!sentLast.current) {\n                  sentLast.current = true;\n                  isFinished.current = true;\n                  isFinishing.current = false;\n                  if (!isRestarting.current) {\n                    chrome.runtime.sendMessage({ type: \"video-ready\" });\n                  }\n                }\n                return;\n              }\n            } else {\n              await chrome.storage.local.set({\n                fastRecorderValidationFailed: false,\n                fastRecorderValidation: validation,\n              });\n            }\n\n            await updateFreeFinalizeStatus(\"ready\", 100);\n            if (!sentLast.current) {\n              sentLast.current = true;\n              isFinished.current = true;\n              isFinishing.current = false;\n              if (!isRestarting.current) {\n                chrome.runtime.sendMessage({ type: \"video-ready\" });\n              }\n            }\n          },\n          onChunk: (chunkData, timestampUs) => {\n            const blob = new Blob([chunkData], { type: \"video/mp4\" });\n            handleWebCodecsChunk(blob, timestampUs);\n          },\n          onError: (err) => {\n            markFastRecorderFailure(\"webcodecs-error\", {\n              error: String(err),\n            });\n            chrome.storage.local.set({\n              useWebCodecsRecorder: false,\n              lastWebCodecsFailureAt: Date.now(),\n              lastWebCodecsFailureCode: \"webcodecs-error\",\n            });\n            updateFreeFinalizeStatus(\"failed\", 100, String(err));\n            chrome.runtime.sendMessage({\n              type: \"show-toast\",\n              message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n            });\n            chrome.runtime.sendMessage({\n              type: \"recording-error\",\n              error: \"stream-error\",\n              why: \"Fast recorder failed. Please try compatibility mode.\",\n            });\n          },\n          onStop: async () => {\n            debugRecordingEvent(recdbgSessionRef, \"recorder-stop\", {\n              count: recdbgChunkCount,\n              totalBytes: recdbgTotalBytes,\n              encoder: \"webcodecs\",\n            });\n          },\n        });\n\n        const started = await recorder.current.start();\n        if (!started) {\n          useWebCodecs.current = false;\n          await chrome.storage.local.set({ fastRecorderInUse: false });\n          await chrome.storage.local.set({\n            useWebCodecsRecorder: false,\n            lastWebCodecsFailureAt: Date.now(),\n            lastWebCodecsFailureCode: \"start-failed\",\n          });\n          chrome.runtime.sendMessage({\n            type: \"show-toast\",\n            message: chrome.i18n.getMessage(\"webcodecsFailedOffToast\"),\n          });\n          return false;\n        }\n\n        debugRecordingEvent(recdbgSessionRef, \"recorder-start\", {\n          encoder: \"webcodecs\",\n          codec: \"webcodecs\",\n          width,\n          height,\n          fps,\n          timesliceMs: 1000,\n        });\n\n        return true;\n      };\n\n      const startMediaRecorderWithCodec = async (codec) => {\n        const mimeType = selectMimeType(codec);\n        if (!mimeType) {\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"stream-error\",\n            why: `No supported mimeTypes available for ${codec}`,\n          });\n\n          // Reload this iframe\n          window.location.reload();\n          return false;\n        }\n\n        recorderToken += 1;\n        const token = recorderToken;\n        recdbgChunkCount = 0;\n        recdbgTotalBytes = 0;\n        mediaRecorderStartAt = Date.now();\n\n        recorder.current = new MediaRecorder(liveStream.current, {\n          mimeType: mimeType,\n          audioBitsPerSecond: audioBitsPerSecond,\n          videoBitsPerSecond: videoBitsPerSecond,\n        });\n        debug(\"MediaRecorder config\", {\n          mimeType: recorder.current?.mimeType,\n          audioBitsPerSecond,\n          videoBitsPerSecond,\n        });\n        debugRecordingEvent(recdbgSessionRef, \"recorder-start\", {\n          encoder: \"mediarecorder\",\n          codec: getCodecLabel(mimeType),\n          mimeType: recorder.current?.mimeType,\n          audioBitsPerSecond,\n          videoBitsPerSecond,\n          width,\n          height,\n          fps,\n          timesliceMs: 1000,\n        });\n\n        setTimeout(() => {\n          if (token !== recorderToken) return;\n          const afterTrack = liveStream.current?.getVideoTracks?.()[0] ?? null;\n          logRecordingSnapshot(\"after-start-500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n          debugRecordingEvent(recdbgSessionRef, \"after-start-500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n        }, 500);\n\n        setTimeout(() => {\n          if (token !== recorderToken) return;\n          const afterTrack = liveStream.current?.getVideoTracks?.()[0] ?? null;\n          debugRecordingEvent(recdbgSessionRef, \"after-start-1500ms\", {\n            capture: buildTrackSnapshot(afterTrack),\n          });\n        }, 1500);\n\n        recorder.current.onerror = (ev) => {\n          if (token !== recorderToken) return;\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"mediarecorder\",\n            why: String(ev?.error || \"unknown\"),\n          });\n        };\n\n        recorder.current.onstop = async () => {\n          if (token !== recorderToken) return;\n          try {\n            recorder.current.requestData();\n          } catch {}\n\n          if (isRestarting.current) return;\n\n          regionRef.current = false;\n          recordingRef.current = false;\n\n          await waitForTrailingChunks();\n          try {\n            await chrome.storage.local.set({\n              lastRegionChunkStats: {\n                ts: Date.now(),\n                savedCount: savedCount.current,\n                pendingCount: pending.current.length,\n                pendingBytes: pendingBytes.current,\n                hasChunks: hasChunks.current,\n              },\n            });\n          } catch {}\n          const hasSavedChunks = await waitForSavedChunks();\n          if (!hasSavedChunks) {\n            persistRegionStartDebug({\n              stage: \"stop-no-chunks\",\n              recorderType: \"MediaRecorder\",\n              savedCount: savedCount.current,\n              pending: pending.current.length,\n            });\n            chrome.runtime.sendMessage({\n              type: \"recording-error\",\n              error: \"stream-error\",\n              why: \"No recording data was generated\",\n            });\n            return;\n          }\n\n          if (!sentLast.current) {\n            sentLast.current = true;\n            isFinished.current = true;\n            chrome.runtime.sendMessage({ type: \"video-ready\" });\n            isFinishing.current = false;\n          }\n        };\n\n        recorder.current.ondataavailable = async (e) => {\n          if (token !== recorderToken) return;\n          if (!e || !e.data || !e.data.size) {\n            if (recorder.current && recorder.current.state === \"inactive\") {\n              chrome.storage.local.set({\n                recording: false,\n                restarting: false,\n                tabRecordedID: null,\n              });\n              sendStopWithReason(\"region-empty-mediarecorder-data\");\n            }\n            return;\n          }\n\n          if (lowStorageAbort.current) {\n            return;\n          }\n\n          recdbgChunkCount += 1;\n          recdbgTotalBytes += e.data.size || 0;\n          const elapsedSec = Math.max(\n            0.001,\n            (Date.now() - mediaRecorderStartAt) / 1000,\n          );\n          const derivedMbps = (recdbgTotalBytes * 8) / elapsedSec / 1e6;\n\n          if (recdbgChunkCount <= 3) {\n            debugRecordingEvent(recdbgSessionRef, \"chunk\", {\n              index: recdbgChunkCount,\n              size: e.data.size,\n              timecode: e.timecode ?? 0,\n            });\n          }\n\n          if (recdbgChunkCount % 3 === 0) {\n            debugRecordingEvent(recdbgSessionRef, \"bitrate\", {\n              codec: getCodecLabel(mimeType),\n              elapsedSec,\n              derivedMbps,\n              bytes: recdbgTotalBytes,\n              targetVideoBps: videoBitsPerSecond,\n            });\n          }\n\n          if (\n            !codecFallbackTriggered &&\n            (recdbgChunkCount >= 3 || elapsedSec >= 3)\n          ) {\n            if (derivedMbps < 5 && getCodecLabel(mimeType) !== \"vp8\") {\n              codecFallbackTriggered = true;\n              debugRecordingEvent(recdbgSessionRef, \"codec-fallback\", {\n                from: getCodecLabel(mimeType),\n                to: \"vp8\",\n                derivedMbps,\n              });\n              recorderToken += 1;\n              await resetChunkState();\n              try {\n                recorder.current.stop();\n              } catch {}\n              const switched = await startMediaRecorderWithCodec(\"vp8\");\n              if (switched && recorder.current instanceof MediaRecorder) {\n                try {\n                  recorder.current.start(1000);\n                  setTimeout(() => {\n                    try { recorder.current?.requestData?.(); } catch (_) {}\n                  }, 250);\n                } catch (startErr) {\n                  persistRegionStartDebug({\n                    stage: \"codec-fallback-start-failed\",\n                    why: String(startErr),\n                  });\n                }\n              }\n              return;\n            }\n          }\n\n          if (!hasChunks.current) {\n            hasChunks.current = true;\n            lastTimecode.current = e.timecode ?? 0;\n            lastSize.current = e.data.size;\n          }\n\n          pending.current.push(e);\n          pendingBytes.current += e.data.size;\n\n          if (pendingBytes.current > MAX_PENDING_BYTES) {\n            try {\n              recorder.current.pause();\n              await drainQueue();\n              recorder.current.resume();\n            } catch {}\n          }\n\n          void drainQueue();\n        };\n\n        recorder.current.addEventListener(\"stop\", () => {\n          if (token !== recorderToken) return;\n          debugRecordingEvent(recdbgSessionRef, \"recorder-stop\", {\n            count: recdbgChunkCount,\n            totalBytes: recdbgTotalBytes,\n            encoder: \"mediarecorder\",\n            codec: getCodecLabel(mimeType),\n          });\n        });\n\n        return true;\n      };\n\n      if (shouldUseFast) {\n        const ok = await startWebCodecsRecorder();\n        if (!ok) {\n          useWebCodecs.current = false;\n          await chrome.storage.local.set({ fastRecorderInUse: false });\n          await startMediaRecorderWithCodec(\"vp9\");\n          persistRegionStartDebug({ stage: \"start-recorder-fallback-vp9\" });\n        } else {\n          persistRegionStartDebug({ stage: \"start-recorder-webcodecs-ok\" });\n        }\n      } else {\n        useWebCodecs.current = false;\n        await chrome.storage.local.set({ fastRecorderInUse: false });\n        await startMediaRecorderWithCodec(\"vp9\");\n        persistRegionStartDebug({ stage: \"start-recorder-mediarecorder-vp9\" });\n      }\n    } catch (err) {\n      persistRegionStartDebug({\n        stage: \"start-pipeline-exception\",\n        why: String(err),\n        helperVideo: buildTrackSnapshot(\n          helperVideoStream.current?.getVideoTracks?.()[0] || null,\n        ),\n        liveVideo: buildTrackSnapshot(\n          liveStream.current?.getVideoTracks?.()[0] || null,\n        ),\n      });\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"stream-error\",\n        why: String(err),\n      });\n\n      // Reload this iframe\n      window.location.reload();\n      return;\n    }\n\n    isRestarting.current = false;\n    index.current = 0;\n    recordingRef.current = true;\n    isDismissing.current = false;\n\n    const isMediaRecorder = recorder.current instanceof MediaRecorder;\n    if (isMediaRecorder) {\n      try {\n        recorder.current.start(1000);\n        // Force an early first chunk so even very short recordings have\n        // data in IndexedDB before the user can navigate away.\n        setTimeout(() => {\n          try { recorder.current?.requestData?.(); } catch (_) {}\n        }, 250);\n      } catch (err) {\n        persistRegionStartDebug({\n          stage: \"mediarecorder-start-throw\",\n          why: String(err),\n          recorderType: recorder.current?.constructor?.name || null,\n          helperVideo: buildTrackSnapshot(\n            helperVideoStream.current?.getVideoTracks?.()[0] || null,\n          ),\n          liveVideo: buildTrackSnapshot(\n            liveStream.current?.getVideoTracks?.()[0] || null,\n          ),\n        });\n        chrome.storage.local.set({\n          recording: false,\n          restarting: false,\n          tabRecordedID: null,\n          memoryError: true,\n        });\n        sendStopWithReason(\"region-mediarecorder-start-throw\");\n\n        // Reload this iframe\n        window.location.reload();\n        return;\n      }\n    }\n\n    const recordingStartTime = Date.now();\n    recordingStartTimeRef.current = recordingStartTime;\n    await setRecordingTimingState({\n      recording: true,\n      paused: false,\n      recordingStartTime,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    startRecordingTick();\n    chrome.storage.local.set({\n      recording: true,\n      restarting: false,\n    });\n    persistRegionStartDebug({\n      stage: \"recording-started\",\n      recorderType: recorder.current?.constructor?.name || null,\n    });\n\n    if (isMediaRecorder && recorder.current) {\n      recorder.current.onpause = () => {\n        lastTimecode.current = 0;\n      };\n\n      recorder.current.onresume = () => {\n        lastTimecode.current = 0;\n        lastSize.current = 0;\n      };\n    }\n\n    const liveVideoTrack = liveStream.current?.getVideoTracks?.()[0] || null;\n    if (liveVideoTrack) {\n      liveVideoTrack.onended = () => {\n        regionRef.current = false;\n        chrome.storage.local.set({\n          recording: false,\n          restarting: false,\n          tabRecordedID: null,\n        });\n        sendStopWithReason(\"region-live-video-ended\");\n      };\n    }\n\n    const vTrack = liveVideoTrack;\n    if (vTrack) {\n      vTrack.oninactive = () => {\n        persistRegionStartDebug({\n          stage: \"live-video-inactive\",\n          why: \"Video track became inactive\",\n          liveVideo: buildTrackSnapshot(vTrack),\n        });\n      };\n    }\n\n    const aTrack = helperAudioStream.current?.getAudioTracks()[0];\n    if (aTrack) {\n      aTrack.oninactive = () => {\n        persistRegionStartDebug({\n          stage: \"audio-inactive\",\n          why: \"Audio track became inactive\",\n        });\n      };\n    }\n\n    const helperVideoTrack =\n      helperVideoStream.current?.getVideoTracks?.()[0] || null;\n    if (helperVideoTrack) {\n      helperVideoTrack.onended = () => {\n        regionRef.current = false;\n        chrome.storage.local.set({\n          recording: false,\n          restarting: false,\n          tabRecordedID: null,\n        });\n        sendStopWithReason(\"region-helper-video-ended\");\n      };\n    }\n  }\n\n  async function stopRecording() {\n    clearStartRetry();\n    stopRecordingTick();\n    recordingStartTimeRef.current = null;\n    isFinishing.current = true;\n    regionRef.current = false;\n    await updateFreeFinalizeStatus(\"stopping\", 0);\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n\n    if (recorder.current) {\n      if (\n        useWebCodecs.current &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        try {\n          await updateFreeFinalizeStatus(\"finalizing\", 20);\n          await recorder.current.stop();\n        } catch {}\n        await waitForDrain();\n        recorder.current = null;\n      } else {\n        try {\n          recorder.current.requestData();\n        } catch {}\n\n        try {\n          if (recorder.current.state !== \"inactive\") {\n            recorder.current.stop();\n          }\n        } catch {}\n\n        await waitForDrain();\n        recorder.current = null;\n        await updateFreeFinalizeStatus(\"chunks_ready\", 100);\n      }\n    }\n\n    useWebCodecs.current = false;\n\n    if (liveStream.current !== null) {\n      liveStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      liveStream.current = null;\n    }\n\n    if (helperVideoStream.current !== null) {\n      helperVideoStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      helperVideoStream.current = null;\n    }\n\n    if (helperAudioStream.current !== null) {\n      helperAudioStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      helperAudioStream.current = null;\n    }\n  }\n\n  const dismissRecording = async () => {\n    clearStartRetry();\n    stopRecordingTick();\n    recordingStartTimeRef.current = null;\n    regionRef.current = false;\n    useWebCodecs.current = false;\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    pausedStateRef.current = false;\n    chrome.runtime.sendMessage({ type: \"handle-dismiss\" });\n    isRestarting.current = true;\n    isDismissing.current = true;\n    if (recorder.current !== null) {\n      recorder.current.stop();\n      recorder.current = null;\n    }\n    if (liveStream.current !== null) {\n      liveStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      liveStream.current = null;\n    }\n\n    if (helperVideoStream.current !== null) {\n      helperVideoStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      helperVideoStream.current = null;\n    }\n\n    if (helperAudioStream.current !== null) {\n      helperAudioStream.current.getTracks().forEach(function (track) {\n        track.stop();\n      });\n      helperAudioStream.current = null;\n    }\n  };\n\n  const restartRecording = async () => {\n    clearStartRetry();\n    stopRecordingTick();\n    recordingStartTimeRef.current = null;\n    if (isRestarting.current) {\n      return false;\n    }\n    await setRegionRestartPhase(\"restart-requested\");\n    const wasRegionMode = Boolean(regionRef.current || target.current);\n    isRestarting.current = true;\n    isDismissing.current = false;\n    pausedStateRef.current = false;\n    const wasUsingWebCodecs = useWebCodecs.current;\n    try {\n      if (\n        wasUsingWebCodecs &&\n        recorder.current instanceof WebCodecsRecorder\n      ) {\n        recorder.current.running = false;\n        recorder.current.paused = false;\n        await recorder.current.cleanup();\n      } else if (recorder.current && recorder.current.state !== \"inactive\") {\n        await new Promise((resolve) => {\n          let done = false;\n          const finish = () => {\n            if (done) return;\n            done = true;\n            clearTimeout(timeoutId);\n            resolve();\n          };\n          const timeoutId = setTimeout(finish, 1600);\n          try {\n            recorder.current.addEventListener(\"stop\", finish, { once: true });\n          } catch {}\n          try {\n            recorder.current.stop();\n          } catch {\n            finish();\n          }\n        });\n      }\n    } catch {\n      isRestarting.current = false;\n      await setRegionRestartPhase(\"restart-stop-failed\");\n      return false;\n    }\n    await setRegionRestartPhase(\"restart-stop-complete\");\n    recorder.current = null;\n    sentLast.current = false;\n    isFinished.current = false;\n    isFinishing.current = false;\n    hasChunks.current = false;\n    lastTimecode.current = 0;\n    lastSize.current = 0;\n    savedCount.current = 0;\n    pending.current = [];\n    pendingBytes.current = 0;\n    await chunksStore.clear();\n    await setRecordingTimingState({\n      recording: false,\n      paused: false,\n      recordingStartTime: null,\n      pausedAt: null,\n      totalPausedMs: 0,\n    });\n    useWebCodecs.current = false;\n    regionRef.current = wasRegionMode;\n    await setRegionRestartPhase(\"restart-state-restored\", {\n      regionMode: Boolean(regionRef.current),\n      hasTarget: Boolean(target.current),\n    });\n    isRestarting.current = false;\n    return true;\n  };\n\n  async function startAudioStream(id) {\n    const useExact = id && id !== \"none\";\n    const audioStreamOptions = {\n      mimeType: \"video/webm;codecs=vp8,opus\",\n      audio: useExact\n        ? {\n            deviceId: {\n              exact: id,\n            },\n          }\n        : true,\n    };\n\n    const { defaultAudioInputLabel, audioinput } =\n      await chrome.storage.local.get([\"defaultAudioInputLabel\", \"audioinput\"]);\n    const desiredLabel =\n      defaultAudioInputLabel ||\n      audioinput?.find((device) => device.deviceId === id)?.label ||\n      \"\";\n\n    const result = await getUserMediaWithFallback({\n      constraints: audioStreamOptions,\n      fallbacks:\n        useExact && desiredLabel\n          ? [\n              {\n                kind: \"audioinput\",\n                desiredDeviceId: id,\n                desiredLabel,\n                onResolved: (resolvedId) => {\n                  chrome.storage.local.set({\n                    defaultAudioInput: resolvedId,\n                    defaultAudioInputLabel: desiredLabel,\n                  });\n                },\n              },\n            ]\n          : [],\n    })\n      .then((stream) => {\n        return stream;\n      })\n      .catch((err) => {\n        // Try again without the device ID\n        const audioStreamOptions = {\n          mimeType: \"video/webm;codecs=vp8,opus\",\n          audio: true,\n        };\n\n        return navigator.mediaDevices\n          .getUserMedia(audioStreamOptions)\n          .then((stream) => {\n            return stream;\n          })\n          .catch((err) => {\n            return null;\n          });\n      });\n\n    return result;\n  }\n  // Set audio input volume\n  function setAudioInputVolume(volume) {\n    if (!audioInputGain.current) return;\n    audioInputGain.current.gain.value = volume;\n  }\n\n  // Set audio output volume\n  function setAudioOutputVolume(volume) {\n    if (!audioOutputGain.current) return;\n    audioOutputGain.current.gain.value = volume;\n  }\n\n  async function startStreaming(data) {\n    try {\n      // Get quality value\n      const { qualityValue } = await chrome.storage.local.get([\"qualityValue\"]);\n\n      let width = 1920;\n      let height = 1080;\n\n      if (qualityValue === \"4k\") {\n        width = 4096;\n        height = 2160;\n      } else if (qualityValue === \"1080p\") {\n        width = 1920;\n        height = 1080;\n      } else if (qualityValue === \"720p\") {\n        width = 1280;\n        height = 720;\n      } else if (qualityValue === \"480p\") {\n        width = 854;\n        height = 480;\n      } else if (qualityValue === \"360p\") {\n        width = 640;\n        height = 360;\n      } else if (qualityValue === \"240p\") {\n        width = 426;\n        height = 240;\n      }\n\n      const { fpsValue } = await chrome.storage.local.get([\"fpsValue\"]);\n      let fps = parseInt(fpsValue);\n\n      // Check if fps is a number\n      if (isNaN(fps)) {\n        fps = 30;\n      }\n\n      let stream;\n\n      const constraints = {\n        preferCurrentTab: true,\n        audio: data.systemAudio,\n        video: {\n          frameRate: fps,\n          width: {\n            ideal: width,\n          },\n          height: {\n            ideal: height,\n          },\n        },\n      };\n      stream = await navigator.mediaDevices.getDisplayMedia(constraints);\n      if (!stream || typeof stream.getVideoTracks !== \"function\") {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-error\",\n          why: \"Display stream is unavailable\",\n        });\n        window.location.reload();\n        return;\n      }\n      if (!stream.getVideoTracks().length) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-error\",\n          why: \"Display stream has no video track\",\n        });\n        window.location.reload();\n        return;\n      }\n\n      helperVideoStream.current = stream;\n\n      // Create an audio context, destination, and stream\n      aCtx.current = new AudioContext();\n      destination.current = aCtx.current.createMediaStreamDestination();\n      liveStream.current = new MediaStream();\n      const micstream = await startAudioStream(data.defaultAudioInput);\n\n      // Save the helper streams\n      helperAudioStream.current = micstream;\n\n      // Check if micstream has an audio track\n      if (\n        helperAudioStream.current != null &&\n        helperAudioStream.current.getAudioTracks().length > 0\n      ) {\n        audioInputGain.current = aCtx.current.createGain();\n        audioInputSource.current = aCtx.current.createMediaStreamSource(\n          helperAudioStream.current,\n        );\n        audioInputSource.current\n          .connect(audioInputGain.current)\n          .connect(destination.current);\n      } else {\n        // No microphone available\n      }\n\n      if (helperAudioStream.current != null && !data.micActive) {\n        setAudioInputVolume(0);\n      }\n\n      // Check if stream has an audio track\n      if (helperVideoStream.current.getAudioTracks().length > 0) {\n        audioOutputGain.current = aCtx.current.createGain();\n        audioOutputSource.current = aCtx.current.createMediaStreamSource(\n          helperVideoStream.current,\n        );\n        audioOutputSource.current\n          .connect(audioOutputGain.current)\n          .connect(destination.current);\n      } else {\n        // No system audio available\n      }\n\n      // Add the tracks to the stream\n      const helperVideoTrack = helperVideoStream.current.getVideoTracks()[0];\n      if (!helperVideoTrack) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"stream-error\",\n          why: \"Display stream missing video track\",\n        });\n        window.location.reload();\n        return;\n      }\n      liveStream.current.addTrack(helperVideoTrack);\n\n      if (\n        (helperAudioStream.current != null &&\n          helperAudioStream.current.getAudioTracks().length > 0) ||\n        helperVideoStream.current.getAudioTracks().length > 0\n      ) {\n        const mixedAudioTrack = destination.current.stream.getAudioTracks()[0];\n        if (mixedAudioTrack) {\n          liveStream.current.addTrack(mixedAudioTrack);\n        }\n      }\n\n      try {\n        if (target.current) {\n          const track = liveStream.current.getVideoTracks()[0];\n          if (!track || typeof track.cropTo !== \"function\") {\n            chrome.runtime.sendMessage({\n              type: \"recording-error\",\n              error: \"cancel-modal\",\n              why: \"Selected source does not support region cropping\",\n            });\n            window.location.reload();\n            return;\n          }\n          await track.cropTo(target.current);\n        } else {\n          // No target\n          chrome.runtime.sendMessage({\n            type: \"recording-error\",\n            error: \"cancel-modal\",\n            why: \"No target\",\n          });\n\n          // Reload this iframe\n          window.location.reload();\n        }\n      } catch (err) {\n        chrome.runtime.sendMessage({\n          type: \"recording-error\",\n          error: \"cancel-modal\",\n          why: String(err),\n        });\n\n        // Reload this iframe\n        window.location.reload();\n      }\n\n      // Send message to go back to the previously active tab\n      chrome.runtime.sendMessage({ type: \"reset-active-tab\" });\n    } catch (err) {\n      chrome.runtime.sendMessage({\n        type: \"recording-error\",\n        error: \"cancel-modal\",\n        why: String(err),\n      });\n\n      // Reload this iframe\n      window.location.reload();\n    }\n  }\n\n  useEffect(() => {\n    chrome.storage.local.get([\"backup\"], (result) => {\n      if (result.backup) {\n        backupRef.current = true;\n      } else {\n        backupRef.current = false;\n      }\n    });\n  }, []);\n\n  useEffect(() => {\n    const handleBeforeUnload = (e) => {\n      if (recordingRef.current && regionRef.current) {\n        // Flush any buffered data from the MediaRecorder into IndexedDB.\n        // The \"Leave page?\" dialog gives the ondataavailable handler enough\n        // time to persist the chunk before the frame is torn down.\n        try {\n          recorder.current?.requestData?.();\n        } catch (_) {}\n        e.preventDefault();\n        e.returnValue = \"\";\n      }\n    };\n    window.addEventListener(\"beforeunload\", handleBeforeUnload);\n\n    // When the iframe is actually being destroyed (user clicked \"Leave\" or\n    // the parent page navigated), fire a message to the background to force\n    // stop the recording. pagehide fires synchronously during teardown and\n    // chrome.runtime.sendMessage from an extension page is delivered via IPC\n    // before the frame is gone — no async race.\n    const handlePageHide = () => {\n      if (recordingRef.current && regionRef.current) {\n        try {\n          chrome.runtime.sendMessage({\n            type: \"region-iframe-destroyed\",\n          });\n        } catch (_) {\n          // Extension context may already be invalidated\n        }\n      }\n    };\n    window.addEventListener(\"pagehide\", handlePageHide);\n\n    return () => {\n      window.removeEventListener(\"beforeunload\", handleBeforeUnload);\n      window.removeEventListener(\"pagehide\", handlePageHide);\n    };\n  }, []);\n\n  const setMic = async (result) => {\n    if (helperAudioStream.current != null) {\n      if (result.active) {\n        setAudioInputVolume(1);\n      } else {\n        setAudioInputVolume(0);\n      }\n    } else {\n      // No microphone available\n    }\n  };\n\n  const onMessage = useCallback(\n    (request, sender, sendResponse) => {\n      if (request.type === \"loaded\") {\n        backupRef.current = request.backup;\n        if (request.region) {\n          chrome.runtime.sendMessage({ type: \"get-streaming-data\" });\n        }\n      }\n      if (request.type === \"streaming-data\") {\n        if (regionRef.current) {\n          startStreaming(JSON.parse(request.data));\n        }\n      } else if (request.type === \"start-recording-tab\") {\n        setRegionRestartPhase(\"restart-start-message-received\", {\n          regionMode: Boolean(regionRef.current),\n          hasTarget: Boolean(target.current),\n        });\n        if (regionRef.current || target.current) {\n          regionRef.current = true;\n          setRegionRestartPhase(\"restart-started\");\n          startRecording();\n        }\n      } else if (request.type === \"restart-recording-tab\") {\n        Promise.resolve(restartRecording())\n          .then((restarted) => {\n            if (!restarted) {\n              sendResponse?.({ ok: false, error: \"restart-teardown-failed\" });\n              return;\n            }\n            sendResponse?.({ ok: true, restarted: true });\n          })\n          .catch((error) => {\n            sendResponse?.({\n              ok: false,\n              error: error?.message || String(error),\n            });\n          });\n        return true;\n      } else if (request.type === \"stop-recording-tab\") {\n        if (isFinishing.current) return;\n        stopRecording();\n      } else if (request.type === \"set-mic-active-tab\") {\n        setMic(request);\n      } else if (request.type === \"set-audio-output-volume\") {\n        setAudioOutputVolume(request.volume);\n      } else if (request.type === \"pause-recording-tab\") {\n        if (!recorder.current) return;\n        if (pausedStateRef.current) return;\n        recorder.current.pause();\n        const now = Date.now();\n        pausedStateRef.current = true;\n        void setRecordingTimingState({\n          paused: true,\n          pausedAt: now,\n        });\n      } else if (request.type === \"resume-recording-tab\") {\n        if (!recorder.current) return;\n        if (!pausedStateRef.current) return;\n        recorder.current.resume();\n        const now = Date.now();\n        pausedStateRef.current = false;\n        void (async () => {\n          try {\n            const { pausedAt, totalPausedMs } = await chrome.storage.local.get([\n              \"pausedAt\",\n              \"totalPausedMs\",\n            ]);\n            const additional = pausedAt ? Math.max(0, now - pausedAt) : 0;\n            await setRecordingTimingState({\n              paused: false,\n              pausedAt: null,\n              totalPausedMs: (totalPausedMs || 0) + additional,\n            });\n          } catch (err) {\n            console.warn(\n              \"[Region Recorder] Failed to update resume timing state\",\n              err,\n            );\n          }\n        })();\n      } else if (request.type === \"dismiss-recording\") {\n        dismissRecording();\n      }\n    },\n    [regionRef.current, isFinishing.current, recorder.current],\n  );\n\n  useEffect(() => {\n    // Event listener (extension messaging)\n    chrome.runtime.onMessage.addListener(onMessage);\n\n    return () => {\n      clearStartRetry();\n      chrome.runtime.onMessage.removeListener(onMessage);\n    };\n  }, []);\n\n  return (\n    <div>\n      {process.env.SCREENITY_DEV_MODE === \"true\" && (\n        <div\n          style={{\n            position: \"fixed\",\n            top: 4,\n            right: 4,\n            zIndex: 2147483647,\n            background: \"rgba(0,0,0,0.65)\",\n            color: \"#0f0\",\n            fontSize: \"10px\",\n            padding: \"3px 7px\",\n            borderRadius: \"4px\",\n            fontFamily: \"monospace\",\n            pointerEvents: \"none\",\n          }}\n        >\n          DEV\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default Recorder;\n"
  },
  {
    "path": "src/pages/Region/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Region/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport Recorder from \"./Recorder\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Recorder />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Sandbox/DevHUD.jsx",
    "content": "import React, { useState } from \"react\";\n\nconst BTN = {\n  padding: \"4px 8px\",\n  fontSize: \"11px\",\n  border: \"1px solid rgba(255,255,255,0.2)\",\n  borderRadius: \"4px\",\n  background: \"rgba(0,0,0,0.6)\",\n  color: \"#fff\",\n  cursor: \"pointer\",\n  whiteSpace: \"nowrap\",\n};\nconst BTN_RESET = { ...BTN, background: \"rgba(200,50,50,0.7)\" };\nconst Z = 2147483647;\n\nconst DevHUD = ({ setContentState, contentStateRef }) => {\n  const [collapsed, setCollapsed] = useState(true);\n\n  if (collapsed) {\n    return (\n      <div\n        onClick={() => setCollapsed(false)}\n        style={{\n          position: \"fixed\",\n          top: 6,\n          right: 6,\n          zIndex: Z,\n          background: \"rgba(0,0,0,0.65)\",\n          color: \"#0f0\",\n          fontSize: \"10px\",\n          padding: \"3px 7px\",\n          borderRadius: \"4px\",\n          cursor: \"pointer\",\n          fontFamily: \"monospace\",\n        }}\n      >\n        DEV\n      </div>\n    );\n  }\n\n  const actions = [\n    {\n      label: \"Stuck processing\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          ready: false,\n          processingProgress: 0,\n        })),\n    },\n    {\n      label: \"Processing 50%\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          ready: false,\n          processingProgress: 50,\n        })),\n    },\n    {\n      label: \"Recovery mode\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          fallback: true,\n          noffmpeg: true,\n          editLimit: 3600,\n          ready: true,\n          editErrorType: null,\n          offline: false,\n          webm: p.webm || p.rawBlob || p.blob,\n        })),\n    },\n    {\n      label: \"Long recording\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          fallback: true,\n          noffmpeg: true,\n          editLimit: 0,\n          ready: true,\n          editErrorType: null,\n          offline: false,\n          webm: p.webm || p.rawBlob || p.blob,\n        })),\n    },\n    {\n      label: \"Encoding (FFmpeg)\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          ready: true,\n          isFfmpegRunning: true,\n          mp4ready: false,\n          noffmpeg: false,\n          fallback: false,\n          editErrorType: null,\n          offline: false,\n          duration: Math.min(p.duration || 10, p.editLimit || 3600),\n        })),\n    },\n    {\n      label: \"MP4 not ready\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          ready: true,\n          mp4ready: false,\n          isFfmpegRunning: false,\n          noffmpeg: false,\n          fallback: false,\n          editErrorType: null,\n          offline: false,\n          duration: Math.min(p.duration || 10, p.editLimit || 3600),\n        })),\n    },\n    {\n      label: \"Edit timeout\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          editErrorType: \"timeout\",\n          fallback: false,\n        })),\n    },\n    {\n      label: \"Edit failed\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          editErrorType: \"failed\",\n          fallback: false,\n        })),\n    },\n    {\n      label: \"Edit too long\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          editErrorType: \"too-long\",\n          fallback: false,\n        })),\n    },\n    {\n      label: \"Offline\",\n      fn: () =>\n        setContentState((p) => ({\n          ...p,\n          offline: true,\n          fallback: false,\n        })),\n    },\n    {\n      label: \"Memory limit modal\",\n      fn: () => {\n        contentStateRef.current?.openModal?.(\n          chrome.i18n.getMessage(\"memoryLimitTitle\"),\n          chrome.i18n.getMessage(\"memoryLimitDescription\"),\n          chrome.i18n.getMessage(\"understoodButton\"),\n          null,\n          () => {},\n          () => {}\n        );\n      },\n    },\n    {\n      label: \"Having issues modal\",\n      fn: () => {\n        contentStateRef.current?.openModal?.(\n          chrome.i18n.getMessage(\"havingIssuesModalTitle\"),\n          chrome.i18n.getMessage(\"havingIssuesModalDescription\"),\n          chrome.i18n.getMessage(\"restoreRecording\"),\n          chrome.i18n.getMessage(\"havingIssuesModalButton2\"),\n          () => {},\n          () => {}\n        );\n      },\n    },\n  ];\n\n  return (\n    <div\n      style={{\n        position: \"fixed\",\n        top: 6,\n        right: 6,\n        zIndex: Z,\n        background: \"rgba(20,20,20,0.9)\",\n        borderRadius: \"6px\",\n        padding: \"8px\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: \"4px\",\n        fontFamily: \"monospace\",\n        maxWidth: \"160px\",\n        maxHeight: \"90vh\",\n        overflowY: \"auto\",\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          alignItems: \"center\",\n          marginBottom: \"2px\",\n        }}\n      >\n        <span style={{ color: \"#0f0\", fontSize: \"10px\" }}>DEV HUD</span>\n        <span\n          onClick={() => setCollapsed(true)}\n          style={{ color: \"#888\", fontSize: \"12px\", cursor: \"pointer\" }}\n        >\n          x\n        </span>\n      </div>\n      {actions.map((a) => (\n        <button key={a.label} style={BTN} onClick={a.fn}>\n          {a.label}\n        </button>\n      ))}\n      <button\n        style={BTN_RESET}\n        onClick={() =>\n          setContentState((p) => ({\n            ...p,\n            ready: true,\n            fallback: false,\n            noffmpeg: false,\n            editErrorType: null,\n            isFfmpegRunning: false,\n            mp4ready: true,\n            offline: false,\n            processingProgress: 0,\n          }))\n        }\n      >\n        Reset\n      </button>\n    </div>\n  );\n};\n\nexport default DevHUD;\n"
  },
  {
    "path": "src/pages/Sandbox/Sandbox.jsx",
    "content": "import \"./styles/edit/_VideoPlayer.scss\";\nimport \"./styles/global/_app.scss\";\n\nimport React, { useEffect, useRef, useContext } from \"react\";\n// Layout\nimport Editor from \"./layout/editor/Editor\";\nimport Player from \"./layout/player/Player\";\nimport Modal from \"./components/global/Modal\";\n\nimport HelpButton from \"./components/player/HelpButton\";\n\n// Context\nimport { ContentStateContext } from \"./context/ContentState\"; // Import the ContentState context\n\nconst Sandbox = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const parentRef = useRef(null);\n  const progress = useRef(\"\");\n\n  const getChromeVersion = () => {\n    var raw = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n\n    return raw ? parseInt(raw[2], 10) : false;\n  };\n\n  useEffect(() => {\n    const MIN_CHROME_VERSION = 110;\n    const chromeVersion = getChromeVersion();\n\n    if (chromeVersion && chromeVersion > MIN_CHROME_VERSION) {\n      contentState.loadFFmpeg();\n    } else {\n      setContentState((prevState) => ({\n        ...prevState,\n        updateChrome: true,\n        ffmpeg: true,\n      }));\n    }\n  }, []);\n\n  useEffect(() => {\n    if (!contentState.blob || !contentState.ffmpeg) return;\n    if (contentState.frame) return;\n    // Frame extraction now works in fallback mode using Canvas API\n    contentState.getFrame();\n  }, [contentState.blob, contentState.ffmpeg]);\n\n  // Programmatically add custom scrollbars\n  useEffect(() => {\n    if (!parentRef) return;\n    if (!parentRef.current) return;\n\n    // Check if on mac\n    const isMac = navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0;\n    if (isMac) return;\n\n    const parentDiv = parentRef.current;\n\n    const elements = parentDiv.querySelectorAll(\"*\");\n    elements.forEach((element) => {\n      element.classList.add(\"screenity-scrollbar\");\n    });\n\n    const observer = new MutationObserver((mutationsList) => {\n      for (const mutation of mutationsList) {\n        if (mutation.type === \"childList\") {\n          const addedNodes = Array.from(mutation.addedNodes);\n          const removedNodes = Array.from(mutation.removedNodes);\n\n          addedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.add(\"screenity-scrollbar\");\n            }\n          });\n\n          removedNodes.forEach((node) => {\n            if (node.nodeType === Node.ELEMENT_NODE) {\n              node.classList.remove(\"screenity-scrollbar\");\n            }\n          });\n        }\n      }\n    });\n\n    observer.observe(parentDiv, { childList: true, subtree: true });\n\n    return () => {\n      observer.disconnect();\n    };\n  }, [parentRef.current]);\n\n  useEffect(() => {\n    if (contentState.chunkCount > 0) {\n      progress.current = `(${Math.min(\n        100,\n        Math.round((contentState.chunkIndex / contentState.chunkCount) * 100),\n      )}%)`;\n    }\n  }, [contentState.chunkIndex, contentState.chunkCount]);\n\n  useEffect(() => {\n    // Check if we need to show support banner\n    chrome.runtime.sendMessage({ type: \"check-banner-support\" }, (response) => {\n      if (response && response.bannerSupport) {\n        setContentState((prev) => ({\n          ...prev,\n          bannerSupport: true,\n        }));\n      }\n    });\n  }, []);\n\n  // If editor was opened manually and we don't yet have a blob/ready state,\n  // proactively ask the background to send chunks to this sandbox tab.\n  useEffect(() => {\n    let requested = false;\n    if (requested) return; // guard\n    requested = true;\n\n    const tryRequest = () => {\n      try {\n        if (!contentState.blob && !contentState.ready) {\n          console.debug(\n            \"[Screenity][Sandbox] requesting chunks from background (send-chunks-to-sandbox)\",\n          );\n          // Ask background to send chunks to this tab (background will\n          // determine the target tab or use this sender)\n          chrome.runtime.sendMessage(\n            { type: \"send-chunks-to-sandbox\" },\n            () => {},\n          );\n        }\n      } catch (err) {}\n    };\n\n    // Delay slightly to allow initial message listeners to be registered\n    const t = setTimeout(tryRequest, 400);\n    return () => clearTimeout(t);\n  }, []);\n\n  // Regenerate frame when entering crop mode to reflect current blob\n  useEffect(() => {\n    if (\n      contentState.mode === \"crop\" &&\n      contentState.getFrame &&\n      contentState.blob &&\n      contentState.ffmpeg\n    ) {\n      // Small delay to ensure state updates have propagated\n      setTimeout(() => {\n        contentState.getFrame();\n      }, 50);\n    }\n  }, [contentState.mode]);\n\n  return (\n    <div ref={parentRef}>\n      <Modal />\n      <video></video>\n      {/* Render the WaveformGenerator component and pass the ffmpeg instance as a prop */}\n      {contentState.ffmpeg &&\n        contentState.ready &&\n        contentState.mode === \"edit\" && <Editor />}\n      {contentState.mode != \"edit\" && contentState.ready && <Player />}\n      {!contentState.ready && (\n        <div className=\"wrap\">\n          <img className=\"logo\" src=\"/assets/logo-text.svg\" />\n          <div className=\"middle-area\">\n            <img src=\"/assets/record-tab-active.svg\" />\n            <div className=\"title\">\n              {chrome.i18n.getMessage(\"sandboxProgressTitle\") +\n                \" \" +\n                (contentState.processingProgress > 0\n                  ? `(${Math.round(contentState.processingProgress)}%)`\n                  : progress.current)}\n            </div>\n            <div className=\"subtitle\">\n              {chrome.i18n.getMessage(\"sandboxProgressDescription\")}\n            </div>\n            {typeof contentState.openModal === \"function\" && (\n              <div\n                className=\"button-stop\"\n                onClick={() => {\n                  contentState.openModal(\n                    chrome.i18n.getMessage(\"havingIssuesModalTitle\"),\n                    chrome.i18n.getMessage(\"havingIssuesModalDescription\"),\n                    chrome.i18n.getMessage(\"restoreRecording\"),\n                    chrome.i18n.getMessage(\"havingIssuesModalButton2\"),\n                    () => {\n                      chrome.runtime.sendMessage({ type: \"restore-recording\" });\n                    },\n                    () => {\n                      chrome.runtime.sendMessage({ type: \"report-bug\" });\n                    },\n                    null, // image\n                    null, // learnMore\n                    null, // learnMoreLink\n                    false, // colorSafe\n                    chrome.i18n.getMessage(\"getHelpButton\"),\n                    () => {\n                      chrome.runtime.sendMessage({\n                        type: \"report-error\",\n                        source: \"processing-stuck\",\n                      });\n                    },\n                  );\n                }}\n              >\n                {chrome.i18n.getMessage(\"havingIssuesButton\")}\n              </div>\n            )}\n          </div>\n          <HelpButton />\n          <div className=\"setupBackgroundSVG\"></div>\n        </div>\n      )}\n      <style>\n        {`\n\t\t\t\t\n\t\t\t\t.wrap {\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t}\n\t\t\t\t.setupBackgroundSVG {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground: url(/assets/helper/pattern-svg.svg) repeat;\n\t\t\t\t\tbackground-size: 62px 23.5px;\n\t\t\t\t\tanimation: moveBackground 138s linear infinite;\n\t\t\t\t}\n\t\t\t\t.button-stop {\n\t\t\t\t\tpadding: 10px 20px;\n\t\t\t\t\tbackground: #FFF;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tcolor: #29292F;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tmargin-top: 0px;\n\t\t\t\t\tborder: 1px solid #E8E8E8;\n\t\t\t\t\tmargin-left: auto;\n\t\t\t\t\tmargin-right: auto;\n\t\t\t\t\tz-index: 999999;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes moveBackground {\n\t\t\t\t\t0% {\n\t\t\t\t\t\tbackground-position: 0 0;\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\tbackground-position: 100% 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.logo {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tbottom: 30px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\twidth: 120px;\n\t\t\t\t}\n\t\t\t\t.wrap {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tbackground-color: #F6F7FB;\n\t\t\t\t}\n\t\t\t\t\t.middle-area {\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\tflex-direction: column;\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tjustify-content: center;\n\t\t\t\t\t\theight: 100%;\n\t\t\t\t\t\tfont-family: Satoshi Medium, sans-serif;\n\t\t\t\t\t}\n\t\t\t\t\t.middle-area img {\n\t\t\t\t\t\twidth: 40px;\n\t\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t\t}\n\t\t\t\t\t.title {\n\t\t\t\t\t\tfont-size: 24px;\n\t\t\t\t\t\tfont-weight: 700;\n\t\t\t\t\t\tcolor: #1A1A1A;\n\t\t\t\t\t\tmargin-bottom: 14px;\n\t\t\t\t\t\tfont-family: Satoshi-Medium, sans-serif;\n\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t}\n\t\t\t\t\t.subtitle {\n\t\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\t\tfont-weight: 400;\n\t\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\t\tmargin-bottom: 24px;\n\t\t\t\t\t\tfont-family: Satoshi-Medium, sans-serif;\n\t\t\t\t\t\ttext-align: center;\n\t\t\t\t\t}\n\n\n.screenity-scrollbar *::-webkit-scrollbar, .screenity-scrollbar::-webkit-scrollbar {\n  background-color: rgba(0,0,0,0);\n  width: 16px;\n  height: 16px;\n  z-index: 999999;\n}\n.screenity-scrollbar *::-webkit-scrollbar-track, .screenity-scrollbar::-webkit-scrollbar-track {\n  background-color: rgba(0,0,0,0);\n}\n.screenity-scrollbar *::-webkit-scrollbar-thumb, .screenity-scrollbar::-webkit-scrollbar-thumb {\n  background-color: rgba(0,0,0,0);\n  border-radius:16px;\n  border:0px solid #fff;\n}\n.screenity-scrollbar *::-webkit-scrollbar-button, .screenity-scrollbar::-webkit-scrollbar-button {\n  display:none;\n}\n.screenity-scrollbar *:hover::-webkit-scrollbar-thumb, .screenity-scrollbar:hover::-webkit-scrollbar-thumb {\n  background-color: #a0a0a5;\n  border:4px solid #fff;\n}\n::-webkit-scrollbar-thumb *:hover, ::-webkit-scrollbar-thumb:hover {\n    background-color:#a0a0a5;\n    border:4px solid #f4f4f4\n}\n.videoBanner {\n\theight: 40px!important;\n\twidth: 100%!important;\n\tposition: absolute!important;\n\ttop: 0px!important;\n\tleft: 0px!important;\n\tbackground-color: #3080F8!important;\n\tcolor: #FFF!important;\n\tfont-family: \"Satoshi-Medium\"!important;\n\tz-index: 99999999999!important;\n\ttext-align: center!important;\n\tdisplay: flex!important;\n\talign-items: center!important;\n\tjustify-content: center!important;\n\tflex-direction: row!important;\n\tgap: 6px!important;\n}\n\t\t\t\t\t\n\t\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default Sandbox;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/CropperWrap.jsx",
    "content": "import React, { useState, useEffect, useContext, useRef } from \"react\";\nimport { CropperRef, Cropper } from \"react-advanced-cropper\";\nimport \"react-advanced-cropper/dist/style.css\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst CropperWrap = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const [image, setImage] = useState(null);\n  const cropperRef = useRef(null);\n\n  useEffect(() => {\n    if (!cropperRef.current) return;\n    if (!cropperRef.current.getCoordinates()) return;\n    if (contentState.fromCropper) return;\n    cropperRef.current.setCoordinates({\n      top: contentState.top,\n      left: contentState.left,\n      width: contentState.width,\n      height: contentState.height,\n    });\n    if (contentState.top != cropperRef.current.getCoordinates().top) {\n      setContentState((prevState) => ({\n        ...prevState,\n        top: cropperRef.current.getCoordinates().top,\n      }));\n    }\n    if (contentState.left != cropperRef.current.getCoordinates().left) {\n      setContentState((prevState) => ({\n        ...prevState,\n        left: cropperRef.current.getCoordinates().left,\n      }));\n    }\n    if (contentState.width != cropperRef.current.getCoordinates().width) {\n      setContentState((prevState) => ({\n        ...prevState,\n        width: cropperRef.current.getCoordinates().width,\n      }));\n    }\n    if (contentState.height != cropperRef.current.getCoordinates().height) {\n      setContentState((prevState) => ({\n        ...prevState,\n        height: cropperRef.current.getCoordinates().height,\n      }));\n    }\n  }, [\n    contentState.width,\n    contentState.height,\n    contentState.top,\n    contentState.left,\n  ]);\n\n  const onChange = (cropper) => {\n    if (!cropper) return;\n    setContentState((prevState) => ({\n      ...prevState,\n      top: cropper.getCoordinates().top,\n      left: cropper.getCoordinates().left,\n      width: cropper.getCoordinates().width,\n      height: cropper.getCoordinates().height,\n      fromCropper: true,\n    }));\n  };\n\n  useEffect(() => {\n    if (!contentState.blob) return;\n\n    setImage(contentState.frame);\n  }, [contentState.frame]);\n\n  return (\n    <div>\n      <Cropper\n        src={image}\n        ref={cropperRef}\n        onChange={onChange}\n        className={\"cropper\"}\n        stencilProps={{\n          grid: true,\n        }}\n        defaultSize={{\n          width: contentState.width || undefined,\n          height: contentState.height || undefined,\n        }}\n        backgroundWrapperClassName=\"CropperBackgroundWrapper\"\n        width={contentState.width}\n        height={contentState.height}\n        transitions={false}\n        style={{ transition: \"none\" }}\n      />\n    </div>\n  );\n};\n\nexport default CropperWrap;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/Dropdown.jsx",
    "content": "import React, { useEffect, useState, useContext, useRef } from \"react\";\n\nimport * as Select from \"@radix-ui/react-select\";\n\nimport styles from \"../../styles/edit/_Dropdown.module.scss\";\n\n// Icons\n//import DropdownIcon from \"../../public/assets/icons/dropdown.svg\";\n//import CheckWhiteIcon from \"../../public/assets/icons/check-white.svg\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Dropdown = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const [label, setLabel] = useState(\"None\");\n  const [value, setValue] = useState(\"none\");\n\n  // Video presets for Youtube, Instagram, TikTok, etc.\n  const presets = [\n    {\n      name: \"none\",\n    },\n    {\n      name: \"Youtube\",\n      label: \"Youtube\",\n      width: 1920,\n      height: 1080,\n    },\n    {\n      name: \"YoutubeShorts\",\n      label: \"Youtube Shorts\",\n      width: 1920,\n      height: 1080,\n    },\n    {\n      name: \"InstagramPost\",\n      label: \"Instagram Post\",\n      width: 1080,\n      height: 1080,\n    },\n    {\n      name: \"InstagramStory\",\n      label: \"Instagram Story\",\n      width: 1080,\n      height: 1920,\n    },\n    {\n      name: \"TikTok\",\n      label: \"TikTok\",\n      width: 1080,\n      height: 1920,\n    },\n    {\n      name: \"Facebook\",\n      label: \"Facebook\",\n      width: 1080,\n      height: 1080,\n    },\n    {\n      name: \"Twitter\",\n      label: \"Twitter\",\n      width: 1080,\n      height: 1080,\n    },\n    {\n      name: \"Dribbble\",\n      label: \"Dribbble\",\n      width: 2800,\n      height: 2100,\n    },\n  ];\n\n  useEffect(() => {\n    // Update the value when the contentState changes\n    setValue(contentState.cropPreset);\n    setLabel(\n      presets.find((preset) => preset.name === contentState.cropPreset).label\n    );\n\n    if (contentState.cropPreset === \"none\") return;\n    const preset = presets.find(\n      (preset) => preset.name === contentState.cropPreset\n    );\n    const aspectRatio = preset.width / preset.height;\n    const maxWidth = contentState.prevWidth;\n    const maxHeight = contentState.prevHeight;\n\n    let width = Math.min(preset.width, maxWidth);\n    let height = Math.min(preset.height, maxHeight);\n\n    if (width > maxWidth || height > maxHeight) {\n      if (width / height > aspectRatio) {\n        width = Math.min(maxWidth, width);\n        height = width / aspectRatio;\n      } else {\n        height = Math.min(maxHeight, height);\n        width = height * aspectRatio;\n      }\n    }\n\n    if (width > maxWidth) {\n      width = maxWidth;\n      height = width / aspectRatio;\n    }\n\n    if (height > maxHeight) {\n      height = maxHeight;\n      width = height * aspectRatio;\n    }\n\n    const left = maxWidth / 2 - width / 2;\n    const top = maxHeight / 2 - height / 2;\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      fromCropper: false,\n      width: width,\n      height: height,\n      left: left,\n      top: top,\n    }));\n  }, [contentState.cropPreset]);\n\n  return (\n    <Select.Root\n      value={value}\n      onValueChange={(newValue) => {\n        setValue(newValue);\n        setLabel(presets.find((preset) => preset.name === newValue).label);\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          cropPreset: newValue,\n        }));\n      }}\n    >\n      <Select.Trigger className={styles.SelectTrigger} aria-label=\"Food\">\n        {props.icon && (\n          <Select.Icon className={styles.SelectIconType}></Select.Icon>\n        )}\n        <div className={styles.SelectValue}>\n          <Select.Value placeholder=\"Select a source\">{label}</Select.Value>\n        </div>\n        <Select.Icon className={styles.SelectIconDrop}>\n          <img src=\"/assets/icons/dropdown.svg\" />\n        </Select.Icon>\n      </Select.Trigger>\n      <Select.Portal className={styles.Portal}>\n        <Select.Content position=\"popper\" className={styles.SelectContent}>\n          <Select.ScrollUpButton\n            className={styles.SelectScrollButton}\n          ></Select.ScrollUpButton>\n          <Select.Viewport className={styles.SelectViewport}>\n            <Select.Group>\n              <SelectItem value=\"none\">None</SelectItem>\n            </Select.Group>\n\n            <Select.Separator className={styles.SelectSeparator} />\n            <Select.Group>\n              {presets.map(\n                (preset, index) =>\n                  preset.name !== \"none\" && (\n                    <SelectItem value={preset.name} key={index}>\n                      {preset.label}\n                    </SelectItem>\n                  )\n              )}\n            </Select.Group>\n          </Select.Viewport>\n          <Select.ScrollDownButton\n            className={styles.SelectScrollButton}\n          ></Select.ScrollDownButton>\n        </Select.Content>\n      </Select.Portal>\n    </Select.Root>\n  );\n};\n\nconst SelectItem = React.forwardRef(\n  ({ children, className, ...props }, forwardedRef) => {\n    return (\n      <Select.Item className={styles.SelectItem} {...props} ref={forwardedRef}>\n        <Select.ItemText>{children}</Select.ItemText>\n        <Select.ItemIndicator className={styles.SelectItemIndicator}>\n          <img src=\"/assets/icons/check-white.svg\" />\n        </Select.ItemIndicator>\n      </Select.Item>\n    );\n  }\n);\n\nexport default Dropdown;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/Switch.jsx",
    "content": "import React, { useContext, useEffect } from \"react\";\nimport * as S from \"@radix-ui/react-switch\";\n\n// Styles\nimport styles from \"../../styles/edit/_Switch.module.scss\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Switch = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext);\n\n  return (\n    <form>\n      <div className={styles.SwitchRow}>\n        <label\n          className={styles.Label}\n          htmlFor=\"replaceAudio\"\n          style={{ paddingRight: 15 }}\n        >\n          {chrome.i18n.getMessage(\"replaceAudioEditor\")}\n        </label>\n        <S.Root\n          className={styles.SwitchRoot}\n          checked={contentState.replaceAudio}\n          onCheckedChange={(checked) => {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              replaceAudio: checked,\n            }));\n          }}\n        >\n          <S.Thumb className={styles.SwitchThumb} />\n        </S.Root>\n      </div>\n    </form>\n  );\n};\n\nexport default Switch;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/Trimmer.jsx",
    "content": "import React, { useRef, useEffect, useState, useContext } from \"react\";\nimport styles from \"../../styles/edit/_Trimmer.module.scss\";\nimport WaveformGenerator from \"./Waveform\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Trimmer = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const trimmerRef = useRef(null);\n  const startHandleRef = useRef(null);\n  const endHandleRef = useRef(null);\n  const isDragging = useRef(false);\n  const activeHandle = useRef(null);\n\n  const handleMouseDown = (e, handle) => {\n    e.preventDefault();\n    isDragging.current = true;\n    activeHandle.current = handle;\n\n    document.addEventListener(\"mousemove\", handleMouseMove);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n  };\n\n  const handleMouseMove = (e) => {\n    if (!isDragging.current) return;\n\n    const trimmerRect = trimmerRef.current.getBoundingClientRect();\n    const trimmerWidth = trimmerRect.width;\n    const mouseX = e.clientX - trimmerRect.left;\n    let newPosition = mouseX / trimmerWidth;\n\n    if (activeHandle.current === \"start\") {\n      newPosition += 0;\n      const validPosition = Math.max(\n        Math.min(newPosition, contentState.end - 0.02),\n        0\n      );\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        start: validPosition,\n      }));\n    } else if (activeHandle.current === \"end\") {\n      newPosition -= 0;\n      const validPosition = Math.min(\n        Math.max(newPosition, contentState.start + 0.02),\n        1\n      );\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        end: validPosition,\n      }));\n    }\n  };\n\n  const handleMouseUp = () => {\n    isDragging.current = false;\n    activeHandle.current = null;\n\n    contentState.addToHistory();\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      dragInteracted: true,\n    }));\n\n    document.removeEventListener(\"mousemove\", handleMouseMove);\n    document.removeEventListener(\"mouseup\", handleMouseUp);\n  };\n\n  useEffect(() => {\n    // Update the handle positions when the start and end values change\n    startHandleRef.current.style.left = `calc(${contentState.start * 100}%)`;\n    endHandleRef.current.style.left = `${contentState.end * 100}%`;\n  }, [contentState.start, contentState.end]);\n\n  return (\n    <div>\n      <div className={styles.trimmerContainer} ref={trimmerRef}>\n        <div className={styles.trimWrap}>\n          <div\n            className={styles.leftOverlay}\n            style={{ width: `${contentState.start * 100}%` }}\n          />\n          <div\n            className={styles.rightOverlay}\n            style={{ width: `${(1 - contentState.end) * 100}%` }}\n          />\n          <div\n            className={styles.trimSection}\n            style={{\n              width: `${(contentState.end - contentState.start) * 100}%`,\n              left: `${contentState.start * 100}%`,\n            }}\n          />\n          <div className={styles.trimmer}>\n            <div\n              className={`${styles.handle} ${styles.startHandle}`}\n              onMouseDown={(e) => handleMouseDown(e, \"start\")}\n              ref={startHandleRef}\n            />\n            <div\n              className={`${styles.handle} ${styles.endHandle}`}\n              onMouseDown={(e) => handleMouseDown(e, \"end\")}\n              ref={endHandleRef}\n            />\n          </div>\n        </div>\n        <WaveformGenerator />\n      </div>\n    </div>\n  );\n};\n\nexport default Trimmer;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/VideoPlayer.jsx",
    "content": "import React, { useContext, useEffect, useState, useRef, useMemo } from \"react\";\nimport { default as Plyr } from \"plyr-react\";\nimport \"plyr-react/plyr.css\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst VideoPlayer = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const playerRef = useRef(null);\n  const [url, setUrl] = useState(null);\n  const [source, setSource] = useState(null);\n  const [isSet, setIsSet] = useState(false);\n\n  useEffect(() => {\n    if (\n      playerRef.current &&\n      playerRef.current.plyr &&\n      contentState.updatePlayerTime\n    ) {\n      playerRef.current.plyr.currentTime = contentState.time;\n    }\n  }, [contentState.time]);\n\n  const options = useMemo(\n    () => ({\n      controls: [\"play\", \"mute\", \"captions\", \"settings\", \"pip\", \"fullscreen\"],\n      ratio: \"16:9\",\n      blankVideo:\n        \"chrome-extension://\" +\n        chrome.i18n.getMessage(\"@@extension_id\") +\n        \"/assets/blank.mp4\",\n      keyboard: {\n        global: true,\n      },\n    }),\n    []\n  );\n\n  useEffect(() => {\n    if (contentState.blob) {\n      const objectURL = URL.createObjectURL(contentState.blob);\n      setSource({\n        type: \"video\",\n        sources: [\n          {\n            src: objectURL,\n            type: \"video/mp4\",\n          },\n        ],\n      });\n      setUrl(objectURL);\n\n      // if (playerRef.current && playerRef.current.plyr) {\n      //   // Check when the video is playing, update the time in real time\n      //   playerRef.current.plyr.on(\"timeupdate\", () => {\n      //     setContentState((prevContentState) => ({\n      //       ...prevContentState,\n      //       time: playerRef.current.plyr.currentTime,\n      //       updatePlayerTime: false,\n      //     }));\n      //   });\n      // }\n\n      return () => {\n        URL.revokeObjectURL(objectURL);\n\n        // if (playerRef.current && playerRef.current.plyr) {\n        //   playerRef.current.plyr.off(\"timeupdate\");\n        // }\n      };\n    }\n  }, [contentState.blob, playerRef]);\n\n  useEffect(() => {\n    if (playerRef.current && playerRef.current.plyr) {\n      // Check when the video is playing, update the time in real time\n      playerRef.current.plyr.on(\"timeupdate\", () => {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          time: playerRef.current.plyr.currentTime,\n          updatePlayerTime: false,\n        }));\n      });\n    }\n\n    return () => {\n      if (playerRef.current && playerRef.current.plyr) {\n        playerRef.current.plyr.off(\"timeupdate\");\n      }\n    };\n  }, [playerRef]);\n\n  const handleClick = () => {\n    if (isSet) return;\n    if (playerRef.current && playerRef.current.plyr) {\n      setIsSet(true);\n      playerRef.current.plyr.on(\"timeupdate\", () => {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          time: playerRef.current.plyr.currentTime,\n          updatePlayerTime: false,\n        }));\n      });\n    }\n  };\n\n  useEffect(() => {\n    if (isSet) return;\n    const handleKeyPress = (event) => {\n      if (playerRef.current && playerRef.current.plyr) {\n        setIsSet(true);\n        playerRef.current.plyr.on(\"timeupdate\", () => {\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            time: playerRef.current.plyr.currentTime,\n            updatePlayerTime: false,\n          }));\n        });\n      }\n    };\n    window.addEventListener(\"keydown\", handleKeyPress);\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyPress);\n    };\n  }, [isSet]);\n\n  return (\n    <div className=\"videoPlayer\">\n      <div className=\"playerWrap\" onClick={handleClick}>\n        {url && (\n          <Plyr\n            ref={playerRef}\n            id=\"plyr-player\"\n            source={source}\n            options={options}\n          />\n        )}\n      </div>\n      <style>\n        {`\n\t\t\t\t\t.plyr {\n\t\t\t\t\t\theight: 90%!important;\n\t\t\t\t\t}\n\t\t\t\t\t@media (max-width: 900px) {\n\t\t\t\t\t\t.videoPlayer {\n\t\t\t\t\t\t\theight: 100%!important;\n\t\t\t\t\t\t\ttop: 40px!important;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t.playerWrap {\n\t\t\t\t\t\t\theight: calc(100% - 300px)!important;\n\t\t\t\t\t\t}\n\t\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default VideoPlayer;\n"
  },
  {
    "path": "src/pages/Sandbox/components/editor/Waveform.jsx",
    "content": "import React, { useContext, useRef, useEffect, useState } from \"react\";\nimport WaveSurfer from \"wavesurfer.js\";\nimport styles from \"../../styles/edit/_Waveform.module.scss\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst WaveformGenerator = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const wavesurferRef = useRef(null);\n  const waveformContainerRef = useRef(null);\n  const customCursorRef = useRef(null);\n  const ghostCursorRef = useRef(null);\n  const [showGhost, setShowGhost] = useState(false);\n  const mouseDown = useRef(false);\n\n  async function blobToArrayBuffer(blob) {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.onloadend = () => {\n        if (reader.result instanceof ArrayBuffer) {\n          resolve(reader.result);\n        } else {\n          reject(new Error(\"Failed to convert Blob to ArrayBuffer\"));\n        }\n      };\n      reader.onerror = reject;\n      reader.readAsArrayBuffer(blob);\n    });\n  }\n\n  const loadWaveform = async (blob) => {\n    try {\n      wavesurferRef.current = WaveSurfer.create({\n        container: waveformContainerRef.current,\n        waveColor: \"#C4C5CE\",\n        progressColor: \"#9596A2\",\n        interpolation: \"cubic\",\n        height: \"auto\",\n        cursorWidth: 0,\n      });\n      const audioArrayBuffer = await blobToArrayBuffer(blob);\n\n      wavesurferRef.current.loadBlob(\n        new Blob([audioArrayBuffer], { type: \"audio/wav\" })\n      );\n\n      wavesurferRef.current.on(\"seeking\", (currentTime) => {\n        const containerRect =\n          waveformContainerRef.current.getBoundingClientRect();\n        const cursorX =\n          containerRect.width *\n          (currentTime / wavesurferRef.current.getDuration());\n        customCursorRef.current.style.left = `${cursorX}px`;\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          time: currentTime,\n          updatePlayerTime: true,\n        }));\n      });\n    } catch (error) {\n      console.error(\"Error loading waveform:\", error);\n    }\n  };\n\n  const handleMouseEnter = () => {\n    if (mouseDown.current) return;\n    setShowGhost(true);\n  };\n\n  const handleMouseMove = (e) => {\n    const containerRect = waveformContainerRef.current.getBoundingClientRect();\n    const cursorX = e.clientX - containerRect.left;\n    const minX = 0;\n    const maxX = containerRect.width;\n    const cursorStyle = ghostCursorRef.current.style;\n    cursorStyle.left = `${cursorX}px`;\n  };\n\n  const handleMouseLeave = () => {\n    setShowGhost(false);\n  };\n\n  const handleMouseDown = (e) => {\n    if (waveformContainerRef.current.contains(e.target)) return;\n    mouseDown.current = true;\n    setShowGhost(false);\n  };\n\n  const handleMouseUp = () => {\n    mouseDown.current = false;\n  };\n\n  useEffect(() => {\n    if (!contentState.blob) return;\n    loadWaveform(contentState.blob);\n    const container = waveformContainerRef.current;\n    container.addEventListener(\"mouseover\", handleMouseEnter);\n    container.addEventListener(\"mousemove\", handleMouseMove);\n    container.addEventListener(\"mouseleave\", handleMouseLeave);\n    document.addEventListener(\"mousedown\", handleMouseDown);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n\n    if (wavesurferRef.current) {\n      wavesurferRef.current.on(\"seek\", (position) => {\n        const containerRect =\n          waveformContainerRef.current.getBoundingClientRect();\n        const cursorX = containerRect.width * position;\n        customCursorRef.current.style.left = `${\n          cursorX + containerRect.left\n        }px`;\n      });\n    }\n\n    return () => {\n      if (wavesurferRef.current) {\n        wavesurferRef.current.destroy();\n      }\n      container.removeEventListener(\"mouseenter\", handleMouseEnter);\n      container.removeEventListener(\"mousemove\", handleMouseMove);\n      container.removeEventListener(\"mouseleave\", handleMouseLeave);\n      document.removeEventListener(\"mousedown\", handleMouseDown);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n  }, [contentState.blob]);\n\n  useEffect(() => {\n    if (!contentState.blob) return;\n    if (contentState.updatePlayerTime) return;\n    if (waveformContainerRef.current === null) return;\n\n    const video = document.createElement(\"video\");\n    video.preload = \"metadata\";\n    video.onloadedmetadata = async () => {\n      const containerRect =\n        waveformContainerRef.current.getBoundingClientRect();\n      const cursorX =\n        containerRect.width * (contentState.time / video.duration);\n      customCursorRef.current.style.left = `${cursorX}px`;\n\n      URL.revokeObjectURL(video.src);\n      video.remove();\n    };\n    video.src = URL.createObjectURL(contentState.blob);\n  }, [contentState.time, contentState.blob, waveformContainerRef.current]);\n\n  return (\n    <div style={{ height: \"100%\" }}>\n      <div className={styles.cursor} ref={customCursorRef}></div>\n      <div\n        className={styles.ghostCursor}\n        style={showGhost ? { opacity: 1 } : { opacity: 0 }}\n        ref={ghostCursorRef}\n      ></div>\n      <div className={styles.waveform} ref={waveformContainerRef}></div>\n    </div>\n  );\n};\n\nexport default WaveformGenerator;\n"
  },
  {
    "path": "src/pages/Sandbox/components/global/Modal.jsx",
    "content": "import React, { useState, useEffect, useContext, useCallback } from \"react\";\nimport * as AlertDialog from \"@radix-ui/react-alert-dialog\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\";\n\nconst Modal = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext);\n  const [title, setTitle] = useState(\"Test\");\n  const [description, setDescription] = useState(\"Description here\");\n  const [button1, setButton1] = useState(\"Submit\");\n  const [button2, setButton2] = useState(\"Cancel\");\n  const [trigger, setTrigger] = useState(() => {});\n  const [trigger2, setTrigger2] = useState(() => {});\n  const [showModal, setShowModal] = useState(false);\n  const [image, setImage] = useState(null);\n  const [learnmore, setLearnMore] = useState(null);\n  const [learnMoreLink, setLearnMoreLink] = useState(() => {});\n  const [colorSafe, setColorSafe] = useState(false);\n  const [sideButton, setSideButton] = useState(false);\n  const [sideButtonAction, setSideButtonAction] = useState(() => {});\n\n  const openModal = useCallback(\n    (\n      title,\n      description,\n      button1,\n      button2,\n      action,\n      action2,\n      image = null,\n      learnMore = null,\n      learnMoreLink = null,\n      colorSafe = false,\n      sideButton = false,\n      sideButtonAction = () => {}\n    ) => {\n      setTitle(title);\n      setDescription(description);\n      setButton1(button1);\n      setButton2(button2);\n      setShowModal(true);\n      setTrigger(() => action);\n      setTrigger2(() => action2);\n      setImage(image);\n      setLearnMore(learnMore);\n      setLearnMoreLink(() => learnMoreLink);\n      setColorSafe(colorSafe);\n      setSideButton(sideButton);\n      setSideButtonAction(() => sideButtonAction);\n    }\n  );\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      openModal: openModal,\n    }));\n\n    return () => {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        openModal: null,\n      }));\n    };\n  }, []);\n\n  return (\n    <AlertDialog.Root\n      open={showModal}\n      onOpenChange={(open) => {\n        setShowModal(open);\n      }}\n    >\n      <AlertDialog.Trigger asChild />\n      <AlertDialog.Portal>\n        <AlertDialog.Overlay className=\"AlertDialogOverlay\" />\n        <AlertDialog.Content className=\"AlertDialogContent\">\n          <AlertDialog.Title className=\"AlertDialogTitle\">\n            {title}\n          </AlertDialog.Title>\n          <AlertDialog.Description className=\"AlertDialogDescription\">\n            {description}\n            {learnmore && \" \"}\n            {learnmore && (\n              <a\n                href=\"#\"\n                onClick={(e) => {\n                  e.preventDefault();\n                  e.stopPropagation();\n                  learnMoreLink();\n                }}\n                target=\"_blank\"\n              >\n                {learnmore}\n              </a>\n            )}\n          </AlertDialog.Description>\n          {image && (\n            <img\n              src={image}\n              style={{\n                width: \"100%\",\n                marginBottom: 15,\n                marginTop: 5,\n                borderRadius: \"15px\",\n              }}\n            />\n          )}\n          <div style={{ display: \"flex\", gap: 12, justifyContent: \"flex-end\" }}>\n            {sideButton && (\n              <button\n                className=\"SideButtonModal\"\n                onClick={() => {\n                  sideButtonAction();\n                  setShowModal(false);\n                }}\n              >\n                {sideButton}\n              </button>\n            )}\n            {button2 && (\n              <AlertDialog.Cancel asChild>\n                <button className=\"Button grey\" onClick={() => trigger2()}>\n                  {button2}\n                </button>\n              </AlertDialog.Cancel>\n            )}\n            {button1 && (\n              <AlertDialog.Action asChild>\n                <button\n                  className={!colorSafe ? \"Button red\" : \"Button blue\"}\n                  onClick={() => trigger()}\n                >\n                  {button1}\n                </button>\n              </AlertDialog.Action>\n            )}\n          </div>\n        </AlertDialog.Content>\n      </AlertDialog.Portal>\n    </AlertDialog.Root>\n  );\n};\n\nexport default Modal;\n"
  },
  {
    "path": "src/pages/Sandbox/components/global/ProBanner.jsx",
    "content": "import React, { useContext, useState } from \"react\";\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst CLOUD_FEATURES_ENABLED =\n  process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\";\n\nconst ProBanner = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const titleKey = CLOUD_FEATURES_ENABLED\n    ? \"proBannerTitle\"\n    : \"hostBannerTitle\";\n\n  const descriptionKey = CLOUD_FEATURES_ENABLED\n    ? \"proBannerDescription\"\n    : \"hostBannerDescription\";\n\n  return (\n    <div className=\"pro-banner\">\n      <div className=\"pro-banner-content\">\n        <div className=\"pro-banner-left\">\n          <video\n            src={chrome.runtime.getURL(\"assets/videos/pro.mp4\")}\n            autoPlay\n            loop\n            muted\n            playsInline\n            style={{ width: \"100%\" }}\n          />\n        </div>\n        <div className=\"pro-banner-right\">\n          <div className=\"pro-banner-title\">\n            {chrome.i18n.getMessage(titleKey) ||\n              \"Unlock cloud sharing, multi-scene editing, auto zooms, captions & more\"}\n          </div>\n          <div className=\"pro-banner-description\">\n            {chrome.i18n.getMessage(descriptionKey) ||\n              \"Screenity will always be free, open-source, and ad-free. Pro helps cover cloud & infra costs, & supports development by a solo indie dev! ❤️\"}\n          </div>\n        </div>\n      </div>\n      <div className=\"pro-banner-cta\">\n        <button\n          className=\"button altButton\"\n          onClick={() => {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              bannerSupport: false,\n            }));\n            chrome.runtime.sendMessage({ type: \"hide-banner\" });\n          }}\n        >\n          {chrome.i18n.getMessage(\"proBannerAltButton\") || \"Don't show again\"}\n        </button>\n        <button\n          className=\"button primaryDarkButton\"\n          onClick={() => {\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              bannerSupport: false,\n            }));\n            chrome.runtime.sendMessage({ type: \"hide-banner\" });\n            chrome.runtime.sendMessage({ type: \"pricing\" });\n          }}\n        >\n          {chrome.i18n.getMessage(\"proBannerButton\") || \"Try it\"}\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default ProBanner;\n"
  },
  {
    "path": "src/pages/Sandbox/components/player/HelpButton.jsx",
    "content": "import React, { useContext, useState, useEffect } from \"react\";\nimport styles from \"../../styles/player/_HelpButton.module.scss\";\nimport { ReactSVG } from \"react-svg\";\nimport { ContentStateContext } from \"../../context/ContentState\";\n\nconst HelpButton = () => {\n  const [contentState] = useContext(ContentStateContext);\n  const [windowWidth, setWindowWidth] = useState(\n    typeof window !== \"undefined\" ? window.innerWidth : 1000\n  );\n\n  useEffect(() => {\n    const handleResize = () => setWindowWidth(window.innerWidth);\n    window.addEventListener(\"resize\", handleResize);\n\n    return () => window.removeEventListener(\"resize\", handleResize);\n  }, []);\n\n  const shouldOffset = contentState.bannerSupport && windowWidth > 900;\n\n  return (\n    <button\n      className={styles.HelpButton}\n      style={shouldOffset ? { bottom: \"110px\" } : {}}\n      aria-label=\"Help button\"\n      onClick={() => {\n        chrome.runtime.sendMessage({ type: \"open-help\" });\n      }}\n    >\n      <ReactSVG\n        src=\"/assets/editor/icons/help.svg\"\n        width=\"18px\"\n        height=\"18px\"\n      />\n    </button>\n  );\n};\n\nexport default HelpButton;\n"
  },
  {
    "path": "src/pages/Sandbox/components/player/ShareModal.jsx",
    "content": "import React, { useContext, useState } from \"react\";\n\nimport styles from \"../../styles/player/_ShareModal.module.scss\";\n\nimport { ReactSVG } from \"react-svg\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst URL = \"/assets/\";\n\nconst ShareModal = ({ showShare, setShowShare }) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  return (\n    <div className={styles.modalWrap}>\n      <div className={styles.modal}>\n        <div\n          className={styles.close}\n          onClick={() => {\n            setShowShare(false);\n          }}\n        >\n          <ReactSVG\n            src={URL + \"editor/icons/close-button.svg\"}\n            width=\"16px\"\n            height=\"16px\"\n          />\n        </div>\n        <div className={styles.emoji}>👋</div>\n        <div className={styles.title}>\n          {chrome.i18n.getMessage(\"shareModalSandboxTitle\")}\n        </div>\n        <div className={styles.subtitle}>\n          {chrome.i18n.getMessage(\"shareModalSandboxDescription\")}\n        </div>\n        <div\n          className={styles.button}\n          onClick={() => {\n            chrome.runtime.sendMessage({ type: \"join-waitlist\" });\n            setShowShare(false);\n          }}\n        >\n          {chrome.i18n.getMessage(\"shareModalSandboxButton\")}\n        </div>\n      </div>\n      <div\n        className={styles.modalBackground}\n        onClick={() => {\n          setShowShare(false);\n        }}\n      ></div>\n    </div>\n  );\n};\n\nexport default ShareModal;\n"
  },
  {
    "path": "src/pages/Sandbox/components/player/Title.jsx",
    "content": "import React, { useContext, useState, useEffect, useRef } from \"react\";\n\n// Styles\nimport styles from \"../../styles/player/_Title.module.scss\";\nconst URL = \"/assets/\";\n\n// Icon\nimport { ReactSVG } from \"react-svg\";\n\nimport ShareModal from \"./ShareModal\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Title = () => {\n  const [showShare, setShowShare] = useState(false);\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const inputRef = useRef(null);\n  // Show the video title, as a heading by default (multiline), on click show a text input to edit the title\n  const [showTitle, setShowTitle] = useState(true);\n  const [title, setTitle] = useState(contentState.title);\n  const [displayTitle, setDisplayTitle] = useState(contentState.title);\n\n  useEffect(() => {\n    setTitle(contentState.title);\n    if (contentState.title.length > 80) {\n      setDisplayTitle(contentState.title.slice(0, 80) + \"...\");\n    } else {\n      setDisplayTitle(contentState.title);\n    }\n  }, [contentState.title]);\n\n  const handleTitleChange = (e) => {\n    setTitle(e.target.value);\n  };\n\n  const handleTitleClick = () => {\n    setShowTitle(false);\n  };\n\n  const handleTitleBlur = () => {\n    setShowTitle(true);\n    setContentState((prevState) => ({\n      ...prevState,\n      title: title,\n    }));\n  };\n\n  useEffect(() => {\n    if (!showTitle) {\n      inputRef.current.focus();\n      inputRef.current.select();\n    }\n  }, [showTitle]);\n\n  useEffect(() => {\n    const handleKeyDown = (e) => {\n      if (e.key === \"Enter\") {\n        setShowTitle(true);\n        setContentState((prevState) => ({\n          ...prevState,\n          title: title,\n        }));\n      } else if (e.key === \"Escape\") {\n        setShowTitle(true);\n        setTitle(contentState.title);\n      }\n    };\n\n    document.addEventListener(\"keydown\", handleKeyDown);\n\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyDown);\n    };\n  }, [title]);\n\n  return (\n    <div className={styles.TitleParent}>\n      {showShare && (\n        <ShareModal showShare={showShare} setShowShare={setShowShare} />\n      )}\n      <div className={styles.TitleWrap}>\n        {showTitle ? (\n          <>\n            <h1 onClick={handleTitleClick}>\n              {displayTitle}{\" \"}\n              <ReactSVG\n                src={URL + \"editor/icons/pencil.svg\"}\n                className={styles.pencil}\n                styles={{ display: \"inline-block\" }}\n              />\n            </h1>\n            {/* <div\n              className={styles.shareButton}\n              onClick={() => {\n                chrome.runtime.sendMessage({ type: \"handle-login\" });\n              }}\n            >\n              <ReactSVG\n                src={URL + \"editor/icons/link.svg\"}\n                className={styles.shareIcon}\n              />\n              {chrome.i18n.getMessage(\"shareUnlockButton\") ||\n                \"Sign in to share (pro)\"}\n            </div> */}\n          </>\n        ) : (\n          <input\n            type=\"text\"\n            value={title}\n            onChange={handleTitleChange}\n            onBlur={handleTitleBlur}\n            ref={inputRef}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default Title;\n"
  },
  {
    "path": "src/pages/Sandbox/components/player/VideoPlayer.jsx",
    "content": "import React, { useContext, useEffect, useState, useRef, useMemo } from \"react\";\nimport Plyr from \"plyr-react\";\nimport \"../../styles/plyr.css\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\n// Components\nimport Title from \"./Title\";\n\nconst VideoPlayer = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const playerRef = useRef(null);\n  const [url, setUrl] = useState(null);\n  const [source, setSource] = useState(null);\n  const contentStateRef = useRef(contentState);\n  const bannerRef = useRef(null);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  const getProcessingBannerText = () => {\n    const base = chrome.i18n.getMessage(\"processingBannerEditor\");\n    const pct = Math.round(contentStateRef.current.processingProgress || 0);\n    if (pct > 0 && pct < 100) {\n      return `${base} (${pct}%)`;\n    }\n    return base;\n  };\n\n  useEffect(() => {\n    if (\n      playerRef.current &&\n      playerRef.current.plyr &&\n      contentState.updatePlayerTime\n    ) {\n      playerRef.current.plyr.currentTime = contentState.time;\n    }\n  }, [contentState.time]);\n\n  const options = useMemo(\n    () => ({\n      controls: [\n        \"play\",\n        \"rewind\",\n        \"fast-forward\",\n        \"progress\",\n        \"current-time\",\n        \"duration\",\n        \"mute\",\n        \"captions\",\n        \"settings\",\n        \"pip\",\n        \"fullscreen\",\n      ],\n      urls: null,\n      ratio: \"16:9\",\n      blankVideo:\n        \"chrome-extension://\" +\n        chrome.i18n.getMessage(\"@@extension_id\") +\n        \"/assets/blank.mp4\",\n      keyboard: {\n        global: true,\n      },\n    }),\n    []\n  );\n\n  /*\n  useEffect(() => {\n    if (contentState.blob) {\n      const objectURL = URL.createObjectURL(contentState.blob);\n      setSource({\n        type: \"video\",\n        sources: [\n          {\n            src: objectURL,\n            type: \"video/mp4\",\n          },\n        ],\n      });\n      setUrl(objectURL);\n\n      return () => {\n        URL.revokeObjectURL(objectURL);\n      };\n    }\n  }, [contentState.blob, playerRef]);\n\t*/\n\n  useEffect(() => {\n    if (contentState.webm || contentState.blob) {\n      let vid;\n      if (contentState.blob) {\n        if (bannerRef.current) {\n          bannerRef.current.style.display = \"none\";\n          bannerRef.current.remove();\n        }\n        vid = contentState.blob;\n      } else if (contentState.webm) {\n        vid = contentState.webm;\n      }\n      const objectURL = URL.createObjectURL(vid);\n      setSource({\n        type: \"video\",\n        sources: [\n          {\n            src: objectURL,\n            type: contentState.blob ? \"video/mp4\" : \"video/webm\",\n          },\n        ],\n      });\n      setUrl(objectURL);\n\n      return () => {\n        URL.revokeObjectURL(objectURL);\n      };\n    }\n  }, [\n    contentState.webm,\n    contentState.blob,\n    contentState.hasBeenEdited,\n    playerRef,\n  ]);\n\n  // Use a mutation observer to check if .plyr--video is added to the DOM\n  useEffect(() => {\n    if (contentStateRef.current.mp4ready || contentStateRef.current.blob)\n      return;\n    const config = { attributes: true, childList: true, subtree: true };\n\n    const callback = function (mutationsList, observer) {\n      for (let mutation of mutationsList) {\n        if (\n          document.querySelector(\".plyr--video\") &&\n          !contentStateRef.current.mp4ready &&\n          !contentStateRef.current.blob &&\n          !bannerRef.current &&\n          !contentStateRef.current.noffmpeg &&\n          !(\n            contentStateRef.current.duration >\n              contentStateRef.current.editLimit &&\n            !contentStateRef.current.override\n          )\n        ) {\n          bannerRef.current = document.createElement(\"div\");\n          bannerRef.current.classList.add(\"videoBanner\");\n          bannerRef.current.innerHTML =\n            \"<img src='\" +\n            chrome.runtime.getURL(\"assets/editor/icons/alert-white.svg\") +\n            \"'/> <span>\" +\n            getProcessingBannerText() +\n            \"</span>\";\n\n          document.querySelector(\".plyr--video\").appendChild(bannerRef.current);\n        }\n      }\n    };\n\n    const observer = new MutationObserver(callback);\n    observer.observe(document.body, config);\n\n    return () => {\n      observer.disconnect();\n\n      if (bannerRef.current) {\n        bannerRef.current.style.display = \"none\";\n        bannerRef.current.remove();\n        bannerRef.current = null;\n      }\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!bannerRef.current) return;\n    bannerRef.current.innerHTML =\n      \"<img src='\" +\n      chrome.runtime.getURL(\"assets/editor/icons/alert-white.svg\") +\n      \"'/> <span>\" +\n      getProcessingBannerText() +\n      \"</span>\";\n  }, [contentState.processingProgress]);\n\n  return (\n    <div className=\"videoPlayer\">\n      <div className=\"playerWrap\">\n        {url && (\n          <Plyr\n            ref={playerRef}\n            id=\"plyr-player\"\n            source={source}\n            options={options}\n          />\n        )}\n        {contentState.mode === \"player\" && <Title />}\n      </div>\n      <style>\n        {`\n\t\t\t\t\t@media (max-width: 900px) {\n\t\t\t\t\t\t.videoPlayer {\n\t\t\t\t\t\t\tposition: relative!important;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default VideoPlayer;\n"
  },
  {
    "path": "src/pages/Sandbox/context/ContentState.jsx",
    "content": "import React, {\n  createContext,\n  useContext,\n  useState,\n  useCallback,\n  useRef,\n} from \"react\";\nimport { useEffect } from \"react\";\n\nimport fixWebmDuration from \"fix-webm-duration\";\nimport { default as fixWebmDurationFallback } from \"webm-duration-fix\";\n\nimport localforage from \"localforage\";\nimport DevHUD from \"../DevHUD\";\nimport {\n  formatLocalTimestamp,\n  getHostnameFromUrl,\n  sanitizeFilenameBase,\n} from \"../../utils/filenameHelpers\";\nimport {\n  debugRecordingEventWithSession,\n  isRecordingDebugEnabled,\n} from \"../../utils/recordingDebug\";\n\nlocalforage.config({\n  driver: localforage.INDEXEDDB,\n  name: \"screenity\", // global DB group\n  version: 1,\n});\n\nconst chunksStore = localforage.createInstance({\n  name: \"chunks\", // the actual DB name\n  storeName: \"keyvaluepairs\",\n});\n\nexport const ContentStateContext = createContext();\n\nconst DEBUG_RECORDER =\n  typeof window !== \"undefined\" ? !!window.SCREENITY_DEBUG_RECORDER : false;\n// Enable post-stop debug logs for sandbox\nconst DEBUG_POSTSTOP = DEBUG_RECORDER;\n\nconst ContentState = (props) => {\n  const videoChunks = useRef([]);\n  const makeVideoCheck = useRef(false);\n  const chunkCount = useRef(0);\n  const recdbgSessionRef = useRef(null);\n  const tabIdRef = useRef(null);\n  // Stale-result guard + stuck-edit watchdog.\n  const opIdRef = useRef(0);\n  const editWatchdogRef = useRef(null);\n\n  useEffect(() => {\n    if (!DEBUG_RECORDER && !isRecordingDebugEnabled()) return;\n    chrome.storage.local.get(\n      [\"recordingDebugSessionId\", \"recordingDebugStartMs\"],\n      (res) => {\n        if (!res?.recordingDebugSessionId) return;\n        recdbgSessionRef.current = {\n          sessionId: res.recordingDebugSessionId,\n          startTimeMs: res.recordingDebugStartMs || null,\n          startPerfMs: null,\n        };\n      },\n    );\n  }, []);\n\n  useEffect(() => {\n    try {\n      chrome.tabs.getCurrent((tab) => {\n        tabIdRef.current = tab?.id || null;\n      });\n    } catch {}\n  }, []);\n\n  useEffect(() => {\n    if (!DEBUG_RECORDER && !isRecordingDebugEnabled()) return;\n    window.__screenityExportRecordingDebug = async () => {\n      const { recordingDebugSessionId } = await chrome.storage.local.get([\n        \"recordingDebugSessionId\",\n      ]);\n      if (!recordingDebugSessionId) {\n        // eslint-disable-next-line no-console\n        console.warn(\"[Sandbox] No recording debug session id found.\");\n        return;\n      }\n      chrome.runtime.sendMessage({\n        type: \"export-recording-debug\",\n        sessionId: recordingDebugSessionId,\n      });\n    };\n    window.__screenityPingRecdbg = () =>\n      chrome.runtime.sendMessage({ type: \"recdbg-ping\" });\n  }, []);\n\n  const defaultState = {\n    time: 0,\n    editLimit: 420,\n    blob: null,\n    webm: null,\n    originalBlob: null,\n    updatePlayerTime: false,\n    start: 0, // Add missing properties here\n    end: 1,\n    trimming: false,\n    cutting: false,\n    muting: false,\n    editErrorType: null, // null | \"too-long\" | \"timeout\" | \"failed\"\n    history: [{}], // Initialize history with a default state\n    redoHistory: [],\n    undoDisabled: true,\n    redoDisabled: true,\n    duration: 0,\n    mode: \"player\",\n    ffmpegLoaded: false,\n    frame: null,\n    getFrame: null,\n    isFfmpegRunning: false,\n    reencoding: false,\n    prevWidth: 0,\n    width: 0,\n    prevHeight: 0,\n    height: 0,\n    top: 0,\n    left: 0,\n    fromCropper: false,\n    base64: null,\n    saveDrive: false,\n    downloading: false,\n    downloadingWEBM: false,\n    downloadingGIF: false,\n    volume: 1,\n    cropPreset: \"none\",\n    replaceAudio: false,\n    title: null,\n    ready: false,\n    mp4ready: false,\n    saved: false,\n    iframeRef: null,\n    offline: false,\n    updateChrome: false,\n    driveEnabled: false,\n    hasBeenEdited: false,\n    dragInteracted: false,\n    noffmpeg: false,\n    processingProgress: 0, // Progress percentage (0-100) for current operation\n    openModal: null,\n    rawBlob: null,\n    override: false,\n    fallback: false,\n    chunkCount: 0,\n    chunkIndex: 0,\n    bannerSupport: false,\n    backupBlob: null,\n    recordingMeta: null,\n  };\n\n  const [contentState, _setContentState] = useState(defaultState);\n  const contentStateRef = useRef(contentState);\n  const launchModeRef = useRef(\"normal\");\n  const launchRecordingIdRef = useRef(null);\n  const pseudoProgressTimerRef = useRef(null);\n  const pseudoProgressStartRef = useRef(null);\n  const pseudoProgressStartAtRef = useRef(null);\n\n  const setContentState = useCallback((updater) => {\n    _setContentState((prev) => {\n      const next = typeof updater === \"function\" ? updater(prev) : updater;\n      contentStateRef.current = next;\n      return next;\n    });\n  }, []);\n\n  useEffect(() => {\n    try {\n      const params = new URLSearchParams(window.location.search);\n      launchModeRef.current = params.get(\"mode\") || \"normal\";\n      launchRecordingIdRef.current = params.get(\"recordingId\") || null;\n    } catch {}\n  }, []);\n\n  useEffect(() => {\n    if (launchModeRef.current !== \"postStop\") return;\n    if (contentState.ready) {\n      if (pseudoProgressTimerRef.current) {\n        clearInterval(pseudoProgressTimerRef.current);\n        pseudoProgressTimerRef.current = null;\n      }\n      return;\n    }\n\n    if (pseudoProgressTimerRef.current || pseudoProgressStartRef.current)\n      return;\n    pseudoProgressStartRef.current = setTimeout(() => {\n      pseudoProgressStartRef.current = null;\n      if (contentStateRef.current?.ready) return;\n      if ((contentStateRef.current?.processingProgress || 0) > 0) return;\n      pseudoProgressStartAtRef.current = Date.now();\n      pseudoProgressTimerRef.current = setInterval(() => {\n        setContentState((prev) => {\n          if (prev.ready) return prev;\n          const current = Number(prev.processingProgress || 0);\n          const elapsedMs = Math.max(\n            0,\n            Date.now() - (pseudoProgressStartAtRef.current || Date.now()),\n          );\n          const target = Math.min(90, Math.round((elapsedMs / 6000) * 90));\n          const next = Math.max(current, target);\n          if (next <= current) return prev;\n          return { ...prev, processingProgress: next };\n        });\n      }, 200);\n    }, 800);\n\n    return () => {\n      if (pseudoProgressStartRef.current) {\n        clearTimeout(pseudoProgressStartRef.current);\n        pseudoProgressStartRef.current = null;\n      }\n      pseudoProgressStartAtRef.current = null;\n      if (pseudoProgressTimerRef.current) {\n        clearInterval(pseudoProgressTimerRef.current);\n        pseudoProgressTimerRef.current = null;\n      }\n    };\n  }, [contentState.ready]);\n\n  const waitForFinalizeReady = async (recordingId) => {\n    if (!recordingId) return { ok: true };\n    const key = `freeFinalizeStatus:${recordingId}`;\n    const timeoutMs = 60_000;\n    const pollMs = 200;\n    const start = Date.now();\n    debugRecordingEventWithSession(recdbgSessionRef.current, \"poststop-wait\", {\n      recordingId,\n      key,\n      timeoutMs,\n      pollMs,\n    });\n\n    const getStatus = async () => {\n      const res = await chrome.storage.local.get([key]);\n      return res[key] || null;\n    };\n\n    return new Promise(async (resolve) => {\n      let done = false;\n      const cleanup = () => {\n        done = true;\n        chrome.storage.onChanged.removeListener(onChanged);\n        clearInterval(pollTimer);\n      };\n\n      const handleStatus = (status) => {\n        if (!status || done) return;\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][Sandbox] waitForFinalizeReady status\", {\n            status,\n          });\n        const rawPct = typeof status.percent === \"number\" ? status.percent : 0;\n        const prePct = Math.min(90, Math.max(0, Math.round(rawPct * 0.9)));\n        debugRecordingEventWithSession(\n          recdbgSessionRef.current,\n          \"poststop-status\",\n          {\n            recordingId,\n            stage: status.stage,\n            percent: status.percent,\n            updatedAt: status.updatedAt,\n          },\n        );\n        setContentState((prev) => ({\n          ...prev,\n          isFfmpegRunning: true,\n          processingProgress: Math.max(prev.processingProgress || 0, prePct),\n        }));\n        if (status.stage === \"chunks_ready\" || status.stage === \"ready\") {\n          cleanup();\n          debugRecordingEventWithSession(\n            recdbgSessionRef.current,\n            \"poststop-ready\",\n            { recordingId, stage: status.stage },\n          );\n          resolve({ ok: true });\n        } else if (status.stage === \"failed\") {\n          cleanup();\n          debugRecordingEventWithSession(\n            recdbgSessionRef.current,\n            \"poststop-failed\",\n            { recordingId, error: status.error || \"failed\" },\n          );\n          resolve({ ok: false, error: status.error || \"failed\" });\n        }\n      };\n\n      const onChanged = (changes, area) => {\n        if (area !== \"local\") return;\n        if (!changes[key]) return;\n        handleStatus(changes[key].newValue);\n      };\n\n      chrome.storage.onChanged.addListener(onChanged);\n\n      const pollTimer = setInterval(async () => {\n        if (done) return;\n        if (Date.now() - start > timeoutMs) {\n          cleanup();\n          debugRecordingEventWithSession(\n            recdbgSessionRef.current,\n            \"poststop-timeout\",\n            { recordingId, timeoutMs },\n          );\n          resolve({ ok: false, error: \"timeout\" });\n          return;\n        }\n        const status = await getStatus();\n        handleStatus(status);\n      }, pollMs);\n\n      const initial = await getStatus();\n      handleStatus(initial);\n    });\n  };\n\n  useEffect(() => {\n    if (launchModeRef.current !== \"postStop\") return;\n    if (!contentState.chunkCount) return;\n    const ratio =\n      contentState.chunkCount > 0\n        ? contentState.chunkIndex / contentState.chunkCount\n        : 0;\n    const pct = Math.min(100, Math.max(20, Math.round(ratio * 80 + 20)));\n    setContentState((prev) => ({\n      ...prev,\n      processingProgress: pct,\n    }));\n  }, [contentState.chunkIndex, contentState.chunkCount]);\n\n  const buildBlobFromChunks = async () => {\n    const items = [];\n\n    await chunksStore.ready();\n    if (DEBUG_POSTSTOP)\n      console.debug(\n        \"[Screenity][Sandbox] buildBlobFromChunks: chunksStore ready, iterating\",\n      );\n\n    await chunksStore.iterate((value) => (items.push(value), undefined));\n    if (DEBUG_POSTSTOP)\n      console.debug(\"[Screenity][Sandbox] buildBlobFromChunks: items loaded\", {\n        count: items.length,\n      });\n\n    items.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));\n    const parts = items.map((c) =>\n      c.chunk instanceof Blob ? c.chunk : new Blob([c.chunk]),\n    );\n\n    if (!parts.length) {\n      debug(\"No chunks found in IndexedDB\");\n      if (DEBUG_POSTSTOP)\n        console.warn(\n          \"[Screenity][Sandbox] buildBlobFromChunks: no parts found in IndexedDB\",\n        );\n      debugRecordingEventWithSession(recdbgSessionRef.current, \"blob-empty\", {\n        chunkCount: 0,\n      });\n      return null;\n    }\n\n    const first = parts[0];\n    const inferredType = first?.type || \"video/webm\";\n\n    const blob = new Blob(parts, { type: inferredType });\n    if (DEBUG_POSTSTOP)\n      console.debug(\n        \"[Screenity][Sandbox] buildBlobFromChunks: reconstructed blob\",\n        {\n          size: blob.size,\n          type: blob.type,\n        },\n      );\n    reconstructVideo(blob);\n    return blob;\n  };\n\n  // useEffect(() => {\n  //   contentStateRef.current = contentState;\n  // }, [contentState]);\n\n  // Check if the user is offline\n  // useEffect(() => {\n  //   if (!navigator.onLine) {\n  //     setContentState((prevState) => ({\n  //       ...prevState,\n  //       offline: true,\n  //     }));\n  //   }\n  // }, []);\n\n  // Generate a title based on the current time unless this is a tab recording\n  useEffect(() => {\n    const loadInitialTitle = async () => {\n      const date = new Date();\n      const formattedDate = date.toLocaleString(\"en-US\", {\n        month: \"short\",\n        day: \"numeric\",\n        year: \"numeric\",\n        hour: \"numeric\",\n        minute: \"2-digit\",\n        hour12: true,\n      });\n      const fallbackTitle = `Screenity video - ${formattedDate}`;\n\n      try {\n        const { recordingMeta } = await chrome.storage.local.get([\n          \"recordingMeta\",\n        ]);\n        if (recordingMeta?.type === \"tab\") {\n          const baseTitle = sanitizeFilenameBase(\n            recordingMeta.title?.trim() ||\n              getHostnameFromUrl(recordingMeta.url) ||\n              fallbackTitle,\n          );\n          const timestamp = formatLocalTimestamp(recordingMeta.startedAt);\n          setContentState((prevState) => ({\n            ...prevState,\n            title: `${baseTitle} — ${timestamp}`,\n            recordingMeta,\n          }));\n          chrome.storage.local.remove([\"recordingMeta\"]);\n          return;\n        }\n      } catch (error) {\n        console.warn(\"Failed to load recording meta:\", error);\n      }\n\n      setContentState((prevState) => ({\n        ...prevState,\n        title: fallbackTitle,\n        recordingMeta: null,\n      }));\n    };\n\n    loadInitialTitle();\n  }, []);\n\n  // Show a popup when attempting to close the tab if the user has not downloaded their video\n  useEffect(() => {\n    if (!contentState.saved) {\n      window.onbeforeunload = function () {\n        return true;\n      };\n    } else {\n      window.onbeforeunload = null;\n    }\n  }, [contentState.saved]);\n\n  const createBackup = () => {\n    setContentState((prev) => ({\n      ...prev,\n      backupBlob: prev.blob,\n    }));\n  };\n\n  const restoreBackup = () => {\n    setContentState((prev) => ({\n      ...prev,\n      blob: prev.backupBlob || prev.blob,\n      mode: \"player\",\n      start: 0,\n      end: 1,\n      backupBlob: null,\n    }));\n  };\n\n  const clearBackup = () => {\n    setContentState((prev) => ({\n      ...prev,\n      backupBlob: null,\n    }));\n  };\n\n  const addToHistory = useCallback(() => {\n    setContentState((prevState) => ({\n      ...prevState,\n      history: [...prevState.history, prevState],\n      redoHistory: [], // Clear redo history when a new state is added\n    }));\n  }, [contentState]);\n\n  const undo = useCallback(() => {\n    if (contentState.history.length > 1) {\n      const previousState =\n        contentState.history[contentState.history.length - 2];\n      const newHistory = contentState.history.slice(0, -1);\n      setContentState((prevState) => ({\n        ...prevState,\n        ...previousState,\n        history: newHistory,\n        redoHistory: [contentState, ...contentState.redoHistory],\n      }));\n    }\n  }, [contentState]);\n\n  const redo = useCallback(() => {\n    if (contentState.redoHistory.length > 0) {\n      const nextState = contentState.redoHistory[0];\n      const newRedoHistory = contentState.redoHistory.slice(1);\n      setContentState((prevState) => ({\n        ...prevState,\n        ...nextState,\n        history: [...contentState.history, contentState],\n        redoHistory: newRedoHistory,\n      }));\n    }\n  }, [contentState]);\n\n  const base64ToUint8Array = (base64) => {\n    const dataUrlRegex = /^data:(.*?);base64,/;\n    const matches = base64.match(dataUrlRegex);\n    if (matches !== null) {\n      // Base64 is a data URL\n      const mimeType = matches[1];\n      const binaryString = atob(base64.slice(matches[0].length));\n      const bytes = new Uint8Array(binaryString.length);\n      for (let i = 0; i < binaryString.length; i++) {\n        bytes[i] = binaryString.charCodeAt(i);\n      }\n      return new Blob([bytes], { type: mimeType });\n    } else {\n      // Base64 is a regular string\n      const binaryString = atob(base64);\n      const bytes = new Uint8Array(binaryString.length);\n      for (let i = 0; i < binaryString.length; i++) {\n        bytes[i] = binaryString.charCodeAt(i);\n      }\n      return bytes;\n    }\n  };\n\n  useEffect(() => {\n    if (!contentState.blob) return;\n\n    // Get duration of video blob\n    const video = document.createElement(\"video\");\n    video.preload = \"metadata\";\n    video.onloadedmetadata = async () => {\n      setContentState((prevState) => ({\n        ...prevState,\n        duration: video.duration,\n        width: video.videoWidth,\n        height: video.videoHeight,\n        prevWidth: video.videoWidth,\n        prevHeight: video.videoHeight,\n      }));\n\n      URL.revokeObjectURL(video.src);\n      video.remove();\n    };\n    video.src = URL.createObjectURL(contentState.blob);\n  }, [contentState.blob]);\n\n  const reconstructVideo = async (withBlob) => {\n    // Sniff MIME type from magic bytes (WebCodecs chunks are MP4, not WebM).\n    let inferredType = \"video/webm; codecs=vp8,opus\";\n    if (!withBlob && videoChunks.current.length > 0) {\n      try {\n        const head = videoChunks.current[0];\n        if (head && head.length >= 8) {\n          const magic = new TextDecoder().decode(head.slice(4, 8));\n          if (magic === \"ftyp\") {\n            inferredType = \"video/mp4\";\n          }\n          if (DEBUG_RECORDER)\n            console.log(\"[Screenity][Sandbox] reconstructVideo inferred type\", {\n              magic,\n              inferredType,\n              chunkCount: videoChunks.current.length,\n              totalSize: videoChunks.current.reduce((s, c) => s + c.length, 0),\n            });\n        }\n      } catch (e) {\n        console.warn(\"[Screenity][Sandbox] reconstructVideo type sniff failed\", e);\n      }\n    }\n\n    let blob = withBlob\n      ? withBlob\n      : new Blob(videoChunks.current, { type: inferredType });\n\n    if (blob.type === \"video/mp4\") {\n      if (DEBUG_RECORDER)\n        console.log(\"[Screenity][Sandbox] reconstructVideo: fast MP4 path taken\", {\n          size: blob.size,\n        });\n      //const TOO_BIG_BYTES = 200 * 1024 * 1024;\n      // const TOO_BIG_BYTES = 0;\n      // if (blob.size > TOO_BIG_BYTES) {\n      //   // Convert Blob → base64 first so we can pass to sandbox\n      //   const reader = new FileReader();\n      //   reader.onloadend = () => {\n      //     const base64 = reader.result;\n\n      //     setContentState((prev) => ({\n      //       ...prev,\n      //       base64,\n      //       compressing: true,\n      //       mp4ready: false,\n      //       ready: false,\n      //       rawBlob: prev.rawBlob || blob,\n      //     }));\n\n      //     // Send to sandbox for compression\n      //     sendMessage({\n      //       type: \"compress-video\",\n      //       base64,\n      //       topLevel: true,\n      //     });\n      //   };\n      //   reader.readAsDataURL(blob);\n\n      //   return;\n      // }\n\n      setContentState((prev) => ({\n        ...prev,\n        blob: blob,\n        webm: null,\n        mp4ready: true,\n        ready: true,\n        rawBlob: blob,\n        isFfmpegRunning: false,\n        noffmpeg: false,\n      }));\n\n      // Extract duration, width, height\n      const video = document.createElement(\"video\");\n      video.preload = \"metadata\";\n      video.onloadedmetadata = () => {\n        setContentState((prev) => ({\n          ...prev,\n          duration: video.duration,\n          width: video.videoWidth,\n          height: video.videoHeight,\n        }));\n\n        URL.revokeObjectURL(video.src);\n      };\n      video.src = URL.createObjectURL(blob);\n\n      chrome.runtime.sendMessage({ type: \"recording-complete\" });\n      chrome.runtime.sendMessage({ type: \"diag-editor-ready\", path: \"mp4-fast\" }).catch(() => {});\n      return;\n    }\n\n    let { recordingDuration } = await chrome.storage.local.get(\n      \"recordingDuration\",\n    );\n\n    // If recordingDuration is missing or 0, try to probe it from the blob\n    if (!recordingDuration || recordingDuration <= 0) {\n      console.warn(\n        \"[Screenity][WebM] recordingDuration missing or 0, probing from blob\",\n      );\n      try {\n        const probeDuration = await new Promise((resolve) => {\n          const probe = document.createElement(\"video\");\n          probe.preload = \"metadata\";\n          const timeout = setTimeout(() => {\n            URL.revokeObjectURL(probe.src);\n            resolve(0);\n          }, 5000);\n          probe.onloadedmetadata = () => {\n            clearTimeout(timeout);\n            const dur = probe.duration;\n            URL.revokeObjectURL(probe.src);\n            if (Number.isFinite(dur) && dur > 0) {\n              resolve(Math.round(dur * 1000));\n            } else {\n              resolve(0);\n            }\n          };\n          probe.onerror = () => {\n            clearTimeout(timeout);\n            URL.revokeObjectURL(probe.src);\n            resolve(0);\n          };\n          probe.src = URL.createObjectURL(blob);\n        });\n        if (probeDuration > 0) {\n          recordingDuration = probeDuration;\n        }\n      } catch (err) {\n        console.warn(\"[Screenity][WebM] blob duration probe failed:\", err);\n      }\n    }\n\n    // Check if token is present\n    const { token } = await chrome.storage.local.get(\"token\");\n\n    let driveEnabled = false;\n\n    if (token && token !== null) {\n      driveEnabled = true;\n    }\n\n    const safeDuration = Number(recordingDuration) || 0;\n    setContentState((prevState) => ({\n      ...prevState,\n      rawBlob: blob,\n      duration: safeDuration / 1000,\n    }));\n\n    // Check if user is in Windows 10\n    const isWindows10 = navigator.userAgent.match(/Windows NT 10.0/);\n\n    try {\n      if (safeDuration > 0) {\n        if (!isWindows10) {\n          fixWebmDuration(\n            blob,\n            safeDuration,\n            async (fixedWebm) => {\n              // Skip conversion only if Chrome is outdated or recording exceeds edit limit\n              if (\n                contentStateRef.current.updateChrome ||\n                contentStateRef.current.noffmpeg ||\n                (contentStateRef.current.duration >\n                  contentStateRef.current.editLimit &&\n                  !contentStateRef.current.override)\n              ) {\n                setContentState((prevState) => ({\n                  ...prevState,\n                  webm: fixedWebm,\n                  ready: true,\n                  isFfmpegRunning: false,\n                }));\n                chrome.runtime.sendMessage({ type: \"recording-complete\" });\n                return;\n              }\n\n              const reader = new FileReader();\n              reader.onloadend = function () {\n                const base64data = reader.result;\n                setContentState((prevContentState) => ({\n                  ...prevContentState,\n                  base64: base64data,\n                  driveEnabled: driveEnabled,\n                }));\n              };\n              reader.readAsDataURL(fixedWebm);\n            },\n            { logger: false },\n          );\n        } else {\n          const fixedWebm = await fixWebmDurationFallback(blob, {\n            type: \"video/webm; codecs=vp8, opus\",\n          });\n          // Skip conversion only if Chrome is outdated or recording exceeds edit limit\n          if (\n            contentStateRef.current.updateChrome ||\n            contentStateRef.current.noffmpeg ||\n            (contentStateRef.current.duration >\n              contentStateRef.current.editLimit &&\n              !contentStateRef.current.override)\n          ) {\n            setContentState((prevState) => ({\n              ...prevState,\n              webm: fixedWebm,\n              ready: true,\n              isFfmpegRunning: false,\n            }));\n            chrome.runtime.sendMessage({ type: \"recording-complete\" });\n            return;\n          }\n\n          const reader = new FileReader();\n          reader.onloadend = function () {\n            const base64data = reader.result;\n            setContentState((prevContentState) => ({\n              ...prevContentState,\n              base64: base64data,\n              driveEnabled: driveEnabled,\n            }));\n          };\n          reader.readAsDataURL(fixedWebm);\n        }\n      } else {\n        // Duration unknown — skip fixing, use raw blob as-is\n        console.warn(\n          \"[Screenity][WebM] skipping duration fix: safeDuration=0, blob will have broken seek metadata\",\n        );\n        // Skip conversion only if Chrome is outdated or recording exceeds edit limit\n        if (\n          contentStateRef.current.updateChrome ||\n          contentStateRef.current.noffmpeg ||\n          (contentStateRef.current.duration >\n            contentStateRef.current.editLimit &&\n            !contentStateRef.current.override)\n        ) {\n          setContentState((prevState) => ({\n            ...prevState,\n            webm: blob,\n            ready: true,\n            isFfmpegRunning: false,\n          }));\n          chrome.runtime.sendMessage({ type: \"recording-complete\" });\n          return;\n        }\n\n        const reader = new FileReader();\n        reader.onloadend = function () {\n          const base64data = reader.result;\n          setContentState((prevContentState) => ({\n            ...prevContentState,\n            base64: base64data,\n            driveEnabled: driveEnabled,\n          }));\n        };\n        reader.readAsDataURL(blob);\n      }\n    } catch (error) {\n      console.error(\n        \"[Screenity][WebM] duration fix failed, using unfixed blob:\",\n        error,\n      );\n      setContentState((prevState) => ({\n        ...prevState,\n        webm: blob,\n        ready: true,\n        isFfmpegRunning: false,\n      }));\n      chrome.runtime.sendMessage({ type: \"recording-complete\" });\n    }\n\n    // 45s safety timeout for the direct-blob path — force ready if\n    // fixWebmDuration or readAsDataURL hangs.\n    if (withBlob) {\n      setTimeout(() => {\n        const s = contentStateRef.current;\n        if (s?.ready) return;\n        console.warn(\n          \"[Screenity][WebM] reconstructVideo(blob) safety timeout: forcing ready with raw blob\",\n        );\n        setContentState((prev) => {\n          if (prev.ready) return prev;\n          return {\n            ...prev,\n            webm: prev.webm || prev.rawBlob || withBlob,\n            ready: true,\n            noffmpeg: true,\n            isFfmpegRunning: false,\n          };\n        });\n        chrome.runtime.sendMessage({ type: \"recording-complete\" });\n      }, 45000);\n    }\n  };\n\n  const checkMemory = () => {\n    if (typeof contentStateRef.current.openModal === \"function\") {\n      chrome.storage.local.get(\"memoryError\", (result) => {\n        if (result.memoryError && result.memoryError !== null) {\n          chrome.storage.local.set({ memoryError: false });\n          contentStateRef.current.openModal(\n            chrome.i18n.getMessage(\"memoryLimitTitle\"),\n            chrome.i18n.getMessage(\"memoryLimitDescription\"),\n            chrome.i18n.getMessage(\"understoodButton\"),\n            null,\n            () => {},\n            () => {},\n            null,\n            chrome.i18n.getMessage(\"learnMoreDot\"),\n            () => {\n              chrome.runtime.sendMessage({ type: \"memory-limit-help\" });\n            },\n            false, // colorSafe\n            chrome.i18n.getMessage(\"getHelpButton\"),\n            () => {\n              chrome.runtime.sendMessage({\n                type: \"report-error\",\n                errorCode: \"REC_RUN_MEMORY\",\n                source: \"memory-limit\",\n              });\n            },\n          );\n        }\n      });\n    }\n  };\n\n  useEffect(() => {\n    chunkCount.current = contentState.chunkCount;\n  }, [contentState.chunkCount]);\n\n  const handleBatch = (chunks, sendResponse) => {\n    if (DEBUG_POSTSTOP)\n      console.debug(\"[Screenity][Sandbox] handleBatch called\", {\n        chunksLen: chunks?.length,\n      });\n    // Process chunks asynchronously, but do not make this function async\n    (async () => {\n      try {\n        await Promise.all(\n          chunks.map(async (chunk) => {\n            // contentStateRef is sync — chunkCount.current lags a render cycle.\n            if (contentStateRef.current.chunkIndex >= contentStateRef.current.chunkCount) {\n              return; // Skip processing\n            }\n\n            const chunkData = base64ToUint8Array(chunk.chunk);\n            if (DEBUG_POSTSTOP)\n              console.debug(\"[Screenity][Sandbox] handleBatch pushing chunk\", {\n                index: chunk.index,\n                size: chunkData?.size || null,\n              });\n            videoChunks.current.push(chunkData);\n\n            // Assuming setContentState doesn't need to be awaited\n            setContentState((prevState) => ({\n              ...prevState,\n              chunkIndex: prevState.chunkIndex + 1,\n            }));\n          }),\n        );\n\n        sendResponse({ status: \"ok\" });\n      } catch (err) {\n        console.error(\"Error processing batch\", err);\n        try {\n          sendResponse?.({\n            status: \"error\",\n            error: String(err?.message || err),\n          });\n        } catch {}\n      }\n    })();\n\n    return true; // Keep the messaging channel open for the response\n  };\n\n  // Check Chrome version\n  useEffect(() => {\n    const version = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./);\n\n    const MIN_CHROME_VERSION = 109;\n\n    if (version && parseInt(version[2], 10) < MIN_CHROME_VERSION) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        updateChrome: true,\n        noffmpeg: true,\n      }));\n    }\n  }, []);\n\n  const makeVideoTab = (sendResponse = null, message) => {\n    if (makeVideoCheck.current) return;\n    makeVideoCheck.current = true;\n    if (DEBUG_POSTSTOP)\n      console.debug(\"[Screenity][Sandbox] makeVideoTab invoked\", {\n        override: message?.override,\n      });\n    setContentState((prevState) => ({\n      ...prevState,\n      override: message.override,\n    }));\n    // All chunks received, reconstruct video\n    checkMemory();\n    reconstructVideo();\n\n    // Timeout: if duration-fix hasn't finished, mark ready so the user\n    // isn't stuck forever. Don't overwrite an already-fixed webm.\n    const safetyCheck = () => {\n      const s = contentStateRef.current;\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][Sandbox] makeVideoTab: safety-check\", {\n          chunkCount: s?.chunkCount,\n          chunkIndex: s?.chunkIndex,\n          rawBlob: Boolean(s?.rawBlob),\n          webm: Boolean(s?.webm),\n          ready: s?.ready,\n        });\n      if (s?.ready) return; // Fix already completed, nothing to do\n\n      const complete = s?.chunkCount > 0 && s?.chunkIndex >= s?.chunkCount;\n      if (complete && s?.rawBlob) {\n        // Keep the fixed webm if it landed between checks.\n        if (s?.webm) {\n          if (DEBUG_RECORDER)\n            console.log(\n              \"[Screenity][WebM] safety timeout: webm already set by fix, marking ready\",\n            );\n          setContentState((prev) => ({\n            ...prev,\n            ready: true,\n            noffmpeg: true,\n            isFfmpegRunning: false,\n          }));\n        } else {\n          console.warn(\n            \"[Screenity][WebM] safety timeout: duration fix did not complete in time, using unfixed rawBlob\",\n          );\n          setContentState((prev) => ({\n            ...prev,\n            webm: prev.rawBlob,\n            ready: true,\n            noffmpeg: true,\n            isFfmpegRunning: false,\n          }));\n        }\n        chrome.runtime.sendMessage({ type: \"recording-complete\" });\n      }\n    };\n    // First check at 30s; if still not ready, final check at 60s\n    setTimeout(() => {\n      if (!contentStateRef.current?.ready) {\n        safetyCheck();\n      }\n    }, 30000);\n    setTimeout(() => {\n      if (!contentStateRef.current?.ready) {\n        console.warn(\n          \"[Screenity][WebM] 60s safety timeout: force-marking ready\",\n        );\n        safetyCheck();\n      }\n    }, 60000);\n\n    if (sendResponse) sendResponse({ status: \"ok\" });\n  };\n\n  const toBase64 = (blob) => {\n    return new Promise((resolve, reject) => {\n      const reader = new FileReader();\n      reader.readAsDataURL(blob);\n      reader.onloadend = () => {\n        resolve(reader.result);\n      };\n      reader.onerror = reject;\n    });\n  };\n\n  const onChromeMessage = useCallback(\n    (request, sender, sendResponse) => {\n      const message = request;\n      if (DEBUG_POSTSTOP)\n        console.debug(\"[Screenity][Sandbox] onChromeMessage\", {\n          type: message?.type,\n          senderTab: sender?.tab?.id,\n        });\n      if (\n        message?._targetTabId &&\n        tabIdRef.current &&\n        message._targetTabId !== tabIdRef.current\n      ) {\n        return false;\n      }\n      if (message.type === \"chunk-count\") {\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][Sandbox] received chunk-count\", {\n            count: message.count,\n          });\n        setContentState((prevState) => ({\n          ...prevState,\n          chunkCount: message.count,\n          override: message.override,\n        }));\n      } else if (message.type === \"ping\") {\n        sendResponse({ status: \"ready\" });\n      } else if (message.type === \"new-chunk-tab\") {\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][Sandbox] received new-chunk-tab\", {\n            chunksLen: message?.chunks?.length,\n          });\n        return handleBatch(message.chunks, sendResponse);\n      } else if (message.type === \"make-video-tab\") {\n        if (DEBUG_POSTSTOP)\n          console.debug(\"[Screenity][Sandbox] received make-video-tab\");\n        makeVideoTab(sendResponse, message);\n\n        return true;\n      } else if (message.type === \"saved-to-drive\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          saveDrive: false,\n          driveEnabled: true,\n          saved: true,\n        }));\n      } else if (message.type === \"restore-recording\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          fallback: true,\n          noffmpeg: false, // Pretending FFmpeg is loaded (using Mediabunny)\n          isFfmpegRunning: false,\n          editLimit: 3600, // Set high edit limit (1 hour) for recovery mode\n        }));\n\n        buildBlobFromChunks()\n          .then((blob) => {\n            if (!blob) {\n              chrome.runtime.sendMessage({ type: \"send-chunks-to-sandbox\" });\n              sendResponse({ status: \"deferred\" });\n              return;\n            }\n            sendResponse({ status: \"ok\" });\n          })\n          .catch((error) => {\n            sendResponse({ status: \"error\", error: error.message });\n          });\n\n        return true; // Keep message port open for async response\n      } else if (message.type === \"large-recording\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          noffmpeg: false,\n          isFfmpegRunning: true,\n          editLimit: 0,\n        }));\n        const shouldGate =\n          launchModeRef.current === \"postStop\" &&\n          Boolean(launchRecordingIdRef.current);\n        if (shouldGate) {\n          waitForFinalizeReady(launchRecordingIdRef.current).then((result) => {\n            if (!result.ok) {\n              setContentState((prev) => ({\n                ...prev,\n                isFfmpegRunning: false,\n                noffmpeg: true,\n                ffmpegLoaded: true,\n                processingProgress: 0,\n              }));\n              // Try to recover chunks.\n              buildBlobFromChunks()\n                .then((blob) => {\n                  if (!blob) {\n                    chrome.runtime.sendMessage({\n                      type: \"send-chunks-to-sandbox\",\n                    });\n                  }\n                })\n                .catch(() => {});\n              sendResponse({ status: \"error\", error: result.error });\n              return;\n            }\n            buildBlobFromChunks()\n              .then((blob) => {\n                if (!blob) {\n                  chrome.runtime.sendMessage({\n                    type: \"send-chunks-to-sandbox\",\n                  });\n                  sendResponse({ status: \"deferred\" });\n                  return;\n                }\n                sendResponse({ status: \"ok\" });\n              })\n              .catch((error) =>\n                sendResponse({ status: \"error\", error: error.message }),\n              );\n          });\n          return true;\n        }\n\n        buildBlobFromChunks()\n          .then((blob) => {\n            if (!blob) {\n              chrome.runtime.sendMessage({ type: \"send-chunks-to-sandbox\" });\n              sendResponse({ status: \"deferred\" });\n              return;\n            }\n            sendResponse({ status: \"ok\" });\n          })\n          .catch((error) => {\n            sendResponse({ status: \"error\", error: error.message });\n          });\n\n        return true; // Keep message port open for async response\n      } else if (message.type === \"fallback-recording\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          fallback: true,\n          noffmpeg: false,\n          isFfmpegRunning: true,\n          editLimit: 3600,\n        }));\n        const shouldGate =\n          launchModeRef.current === \"postStop\" &&\n          Boolean(launchRecordingIdRef.current);\n        if (shouldGate) {\n          waitForFinalizeReady(launchRecordingIdRef.current).then((result) => {\n            if (!result.ok) {\n              setContentState((prev) => ({\n                ...prev,\n                isFfmpegRunning: false,\n                noffmpeg: true,\n                ffmpegLoaded: true,\n                processingProgress: 0,\n              }));\n              // Try to recover chunks.\n              buildBlobFromChunks()\n                .then((blob) => {\n                  if (!blob) {\n                    chrome.runtime.sendMessage({\n                      type: \"send-chunks-to-sandbox\",\n                    });\n                  }\n                })\n                .catch(() => {});\n              sendResponse({ status: \"error\", error: result.error });\n              return;\n            }\n            buildBlobFromChunks()\n              .then((blob) => {\n                if (!blob) {\n                  chrome.runtime.sendMessage({\n                    type: \"send-chunks-to-sandbox\",\n                  });\n                  sendResponse({ status: \"deferred\" });\n                  return;\n                }\n                sendResponse({ status: \"ok\" });\n              })\n              .catch((error) =>\n                sendResponse({ status: \"error\", error: error.message }),\n              );\n          });\n          return true;\n        }\n\n        buildBlobFromChunks()\n          .then((blob) => {\n            if (!blob) {\n              chrome.runtime.sendMessage({ type: \"send-chunks-to-sandbox\" });\n              sendResponse({ status: \"deferred\" });\n              return;\n            }\n            sendResponse({ status: \"ok\" });\n          })\n          .catch((error) => {\n            sendResponse({ status: \"error\", error: error.message });\n          });\n\n        return true;\n      } else if (message.type === \"viewer-recording\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          fallback: true,\n          noffmpeg: true, // No FFmpeg\n          isFfmpegRunning: true,\n          editLimit: 0, // No editing allowed\n        }));\n        const shouldGate =\n          launchModeRef.current === \"postStop\" &&\n          Boolean(launchRecordingIdRef.current);\n        if (shouldGate) {\n          waitForFinalizeReady(launchRecordingIdRef.current).then((result) => {\n            if (!result.ok) {\n              setContentState((prev) => ({\n                ...prev,\n                isFfmpegRunning: false,\n                noffmpeg: true,\n                ffmpegLoaded: true,\n                processingProgress: 0,\n              }));\n              // Try to recover chunks.\n              buildBlobFromChunks().catch(() => {});\n              sendResponse({ status: \"error\", error: result.error });\n              return;\n            }\n            buildBlobFromChunks()\n              .then(() => sendResponse({ status: \"ok\" }))\n              .catch((error) =>\n                sendResponse({ status: \"error\", error: error.message }),\n              );\n          });\n          return true;\n        }\n\n        buildBlobFromChunks()\n          .then(() => {\n            sendResponse({ status: \"ok\" });\n          })\n          .catch((error) => {\n            sendResponse({ status: \"error\", error: error.message });\n          });\n\n        return true;\n      } else if (message.type === \"banner-support\") {\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          bannerSupport: true,\n        }));\n      }\n    },\n    [\n      makeVideoCheck.current,\n      videoChunks.current,\n      contentState,\n      contentStateRef.current,\n    ],\n  );\n\n  useEffect(() => {\n    const messageListener = (message, sender, sendResponse) => {\n      const shouldKeepPortOpen = onChromeMessage(message, sender, sendResponse);\n      return shouldKeepPortOpen === true;\n    };\n\n    chrome.runtime.onMessage.addListener(messageListener);\n\n    // Storage fallback listener: watch for background writing a `chunks_ready_for:<tabId>` key\n    const storageListener = (changes, areaName) => {\n      if (areaName !== \"local\") return;\n      try {\n        const tabId = tabIdRef.current;\n        if (!tabId) return;\n        const key = `chunks_ready_for:${tabId}`;\n        if (changes[key]) {\n          if (DEBUG_POSTSTOP)\n            console.debug(\"[Screenity][Sandbox] storage fallback triggered\", {\n              key,\n            });\n          // Ensure IndexedDB is available in this frame before attempting\n          // to read chunks. Some sandboxed/iframe contexts (ffmpeg iframe)\n          // may not expose IndexedDB; guard to avoid localforage throwing.\n          if (!window.indexedDB) {\n            if (DEBUG_POSTSTOP)\n              console.warn(\n                \"[Screenity][Sandbox] storage fallback: no indexedDB in this context, skipping\",\n              );\n            return;\n          }\n\n          // Asynchronously attempt to build the blob from IndexedDB\n          buildBlobFromChunks()\n            .then((blob) => {\n              if (!blob) {\n                if (DEBUG_POSTSTOP)\n                  console.warn(\n                    \"[Screenity][Sandbox] storage fallback: no blob built\",\n                  );\n                return;\n              }\n              if (DEBUG_POSTSTOP)\n                console.debug(\n                  \"[Screenity][Sandbox] storage fallback: blob built\",\n                  {\n                    size: blob.size,\n                  },\n                );\n            })\n            .catch((err) => {\n              if (DEBUG_POSTSTOP)\n                console.warn(\n                  \"[Screenity][Sandbox] storage fallback build error\",\n                  err,\n                );\n            });\n        }\n      } catch (err) {\n        if (DEBUG_POSTSTOP)\n          console.warn(\"[Screenity][Sandbox] storageListener error\", err);\n      }\n    };\n\n    // Only attach the storage fallback listener in the top-level window\n    // (editor/page) — avoid running this inside sandboxed iframes used for\n    // FFmpeg which may not expose IndexedDB.\n    let storageListenerAttached = false;\n    if (window.top === window.self) {\n      chrome.storage.onChanged.addListener(storageListener);\n      storageListenerAttached = true;\n    } else if (DEBUG_POSTSTOP) {\n      console.debug(\n        \"[Screenity][Sandbox] running inside an iframe; skipping storage fallback listener\",\n      );\n    }\n\n    return () => {\n      chrome.runtime.onMessage.removeListener(messageListener);\n      if (storageListenerAttached) {\n        chrome.storage.onChanged.removeListener(storageListener);\n      }\n    };\n  }, []);\n\n  const onMessage = async (event) => {\n    if (event.data.type === \"updated-blob\") {\n      // Discard results from a timed-out or superseded operation.\n      const msgOpId = event.data._opId;\n      if (msgOpId != null && msgOpId !== opIdRef.current) return;\n\n      const base64 = event.data.base64;\n\n      const blob = base64ToUint8Array(base64);\n\n      const wasCropping = contentState.cropping;\n      const isTopLevel = event.data.topLevel === true;\n      const isFromAudio = event.data.fromAudio === true;\n\n      if (isFromAudio) {\n        // Mid-chain: add-audio succeeded; reencode is still pending.\n        // Forward the same opId so the reencode result is also validated.\n        sendMessage({\n          type: \"reencode-video\",\n          blob,\n          duration: contentState.duration,\n          topLevel: isTopLevel,\n          _opId: event.data._opId,\n        });\n        return;\n      }\n\n      clearEditOp();\n\n      setContentState((prev) => {\n        const wasFirstReady = !prev.mp4ready && isTopLevel;\n        if (wasFirstReady) {\n          chrome.runtime.sendMessage({ type: \"diag-editor-ready\", path: \"updated-blob\" }).catch(() => {});\n        }\n        return {\n          ...prev,\n          blob: blob,\n          mp4ready: true,\n          hasBeenEdited: event.data.edited === false ? prev.hasBeenEdited : true,\n          isFfmpegRunning: false,\n          reencoding: false,\n          trimming: false,\n          cutting: false,\n          muting: false,\n          cropping: false,\n          processingProgress: 0,\n          editErrorType: null,\n          hasTempChanges: !isTopLevel,\n\n          ...(prev.fromCropper && { mode: \"player\", fromCropper: false }),\n          ...(prev.fromAudio && { mode: \"player\", fromAudio: false }),\n        };\n      });\n\n      const video = document.createElement(\"video\");\n      video.preload = \"metadata\";\n      video.onloadedmetadata = async () => {\n        setContentState((prev) => ({\n          ...prev,\n          duration: video.duration,\n          width: video.videoWidth,\n          height: video.videoHeight,\n          start: 0,\n          end: 1,\n          ...(wasCropping && { top: 0, left: 0 }),\n        }));\n\n        if (event.data.addToHistory) {\n          contentState.addToHistory();\n        }\n\n        URL.revokeObjectURL(video.src);\n        video.remove();\n      };\n\n      video.src = URL.createObjectURL(blob);\n\n      if (!contentState.originalBlob && isTopLevel) {\n        setContentState((prev) => ({\n          ...prev,\n          originalBlob: blob,\n        }));\n      }\n    } else if (event.data.type === \"download-mp4\") {\n      const base64 = event.data.base64;\n\n      const blob = base64ToUint8Array(base64);\n      // Download the blob\n      const url = URL.createObjectURL(blob);\n      await requestDownload(url, \".mp4\");\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        saved: true,\n        isFfmpegRunning: false,\n        downloading: false,\n      }));\n    } else if (event.data.type === \"download-gif\") {\n      const base64 = event.data.base64;\n      // const blob = new Blob([base64ToUint8Array(base64)], {\n      //   type: \"image/gif\",\n      // });\n      const blob = base64ToUint8Array(base64);\n      // Download the blob\n      const url = URL.createObjectURL(blob);\n      await requestDownload(url, \".gif\");\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        saved: true,\n        isFfmpegRunning: false,\n        downloadingGIF: false,\n      }));\n    } else if (event.data.type === \"new-frame\") {\n      const url = URL.createObjectURL(event.data.frame);\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        frame: url,\n        isFfmpegRunning: false,\n      }));\n    } else if (event.data.type === \"ffmpeg-loaded\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        ffmpeg: true,\n        ffmpegLoaded: true,\n        isFfmpegRunning: false,\n      }));\n    } else if (event.data.type === \"ffmpeg-load-error\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        ffmpeg: true,\n        noffmpeg: true,\n        ffmpegLoaded: true,\n        isFfmpegRunning: false,\n      }));\n      console.log(\"[Screenity][Editor] recording-complete sent from ffmpeg-load-error fallback\");\n      chrome.runtime.sendMessage({ type: \"recording-complete\" });\n\n      // if (!navigator.onLine) {\n      //   setContentState((prevState) => ({\n      //     ...prevState,\n      //     offline: true,\n      //   }));\n\n      //   // Try loading ffmpeg again when reconnected to the internet\n      //   window.addEventListener(\"online\", () => {\n      //     setContentState((prevState) => ({\n      //       ...prevState,\n      //       offline: false,\n      //     }));\n      //     contentState.loadFFmpeg();\n      //   });\n      // }\n    } else if (event.data.type === \"ffmpeg-error\") {\n      console.warn(\"FFmpeg error:\", event.data.error);\n      clearEditOp();\n\n      // Fallback: allow playback/download using webm/rawBlob even if conversion fails\n      setContentState((prev) => {\n        // If an edit was actively running, surface the failure to the user\n        const wasEditing = prev.isFfmpegRunning && (prev.cutting || prev.trimming || prev.muting || prev.cropping || prev.reencoding);\n        return {\n          ...prev,\n          noffmpeg: true,\n          ffmpegLoaded: true, // treat as \"done trying\"\n          isFfmpegRunning: false,\n          muting: false,\n          cutting: false,\n          trimming: false,\n          reencoding: false,\n          cropping: false,\n          processingProgress: 0,\n          editErrorType: wasEditing ? \"failed\" : prev.editErrorType,\n          ...(prev.rawBlob || prev.webm\n            ? { ready: true, webm: prev.webm || prev.rawBlob }\n            : {}),\n        };\n      });\n\n      chrome.runtime.sendMessage({ type: \"recording-complete\" });\n    } else if (event.data.type === \"edit-too-long\") {\n      // Too long for in-browser processing — reset so user can retry or trim.\n      clearEditOp();\n      setContentState((prev) => ({\n        ...prev,\n        isFfmpegRunning: false,\n        muting: false,\n        cutting: false,\n        trimming: false,\n        reencoding: false,\n        cropping: false,\n        processingProgress: 0,\n        editErrorType: \"too-long\",\n      }));\n    } else if (event.data.type === \"crop-update\") {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        mode: \"crop\",\n        cropping: false,\n        isFfmpegRunning: false,\n        processingProgress: 0,\n        start: 0,\n        end: 1,\n        fromCropper: false,\n      }));\n\n      setTimeout(() => {\n        if (contentState.getFrame) {\n          contentState.getFrame();\n        }\n      }, 100);\n    } else if (event.data.type === \"ffmpeg-progress\") {\n      const pct = Math.min(100, Math.max(0, Math.round(event.data.progress)));\n\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        processingProgress: pct,\n      }));\n    } else if (event.data.type === \"download-webm\") {\n      const base64 = event.data.base64;\n      // const blob = new Blob([base64ToUint8Array(base64)], {\n      //   type: \"video/webm\",\n      // });\n      const blob = base64ToUint8Array(base64);\n\n      const url = URL.createObjectURL(blob);\n      await requestDownload(url, \".webm\");\n\n      setContentState((prevState) => ({\n        ...prevState,\n        saved: true,\n        isFfmpegRunning: false,\n        downloadingWEBM: false,\n      }));\n    }\n  };\n\n  // Listen to PostMessage events\n  useEffect(() => {\n    window.addEventListener(\"message\", onMessage);\n    return () => window.removeEventListener(\"message\", onMessage);\n  }, [onMessage]);\n\n  const sendMessage = (message) => {\n    window.parent.postMessage(message, \"*\");\n  };\n\n  const getBlob = async () => {\n    // Skip conversion only if Chrome is outdated or recording exceeds edit limit\n    if (\n      contentState.noffmpeg ||\n      (contentState.duration > contentState.editLimit && !contentState.override)\n    ) {\n      return;\n    }\n\n    // const webmVideo = new Blob([base64ToUint8Array(contentState.base64)], {\n    //   type: \"video/webm\",\n    // });\n\n    const webmVideo = base64ToUint8Array(contentState.base64);\n\n    setContentState((prevState) => ({\n      ...prevState,\n      webm: webmVideo,\n      ready: true,\n    }));\n\n    if (contentState.offline && contentState.ffmpeg === true) {\n      // Offline (if I need to do anything differently)\n    } else if (\n      !contentState.updateChrome &&\n      (contentState.duration <= contentState.editLimit || contentState.override)\n    ) {\n      // Set isFfmpegRunning to true when starting conversion\n      setContentState((prevState) => ({\n        ...prevState,\n        isFfmpegRunning: true,\n      }));\n      sendMessage({\n        type: \"base64-to-blob\",\n        base64: contentState.base64,\n        topLevel: true,\n      });\n    }\n\n    chrome.runtime.sendMessage({ type: \"recording-complete\" });\n  };\n\n  useEffect(() => {\n    if (!contentState.base64) return;\n    if (!contentState.ffmpeg) return;\n    if (!contentState.ffmpegLoaded) return;\n\n    getBlob();\n  }, [contentState.base64, contentState.ffmpeg, contentState.ffmpegLoaded]);\n\n  // 30s fallback: if FFmpeg never loads (blocked CDN, worker crash, etc.),\n  // force recovery mode so the user can still download their recording.\n  useEffect(() => {\n    if (!contentState.base64) return;\n    if (!contentState.ffmpeg) return;\n    if (contentState.ffmpegLoaded) return; // FFmpeg loaded normally — nothing to do\n    if (contentState.noffmpeg) return;     // already in fallback — nothing to do\n\n    const timer = setTimeout(() => {\n      // Re-check via ref in case state changed since the effect ran.\n      const current = contentStateRef.current;\n      if (current.ffmpegLoaded || current.noffmpeg) return;\n      chrome.storage.local.set({ editorLoadTimeoutAt: Date.now() });\n      setContentState((prev) => {\n        // Double-check inside the updater for concurrent state races.\n        if (prev.ffmpegLoaded || prev.noffmpeg) return prev;\n        // Also set ready when falling back — getBlob() early-returns\n        // when noffmpeg is true, so ready would never be set otherwise.\n        const fallbackWebm = prev.webm || prev.rawBlob;\n        return {\n          ...prev,\n          noffmpeg: true,\n          ffmpegLoaded: true,\n          fallback: true,\n          ...(fallbackWebm && !prev.ready\n            ? { webm: fallbackWebm, ready: true }\n            : {}),\n        };\n      });\n      console.log(\"[Screenity][Editor] recording-complete sent from ffmpeg-load-timeout fallback\");\n      chrome.runtime.sendMessage({ type: \"recording-complete\" });\n    }, 30000);\n\n    return () => clearTimeout(timer);\n  }, [contentState.base64, contentState.ffmpeg, contentState.ffmpegLoaded, contentState.noffmpeg]);\n\n  const getImage = useCallback(async () => {\n    if (!contentState.blob) return;\n    if (!contentState.ffmpeg) return;\n    if (contentState.isFfmpegRunning) return;\n\n    setContentState((prevState) => ({\n      ...prevState,\n      isFfmpegRunning: true, // Set isFfmpegRunning to true to indicate that ffmpeg is running\n    }));\n\n    sendMessage({ type: \"get-frame\", time: 0, blob: contentState.blob });\n  }, [contentState.blob, contentState.ffmpeg, contentState.isFfmpegRunning]);\n\n  // Returns the opId to include in outgoing messages for stale-result validation.\n  const beginEditOp = () => {\n    if (editWatchdogRef.current) {\n      clearTimeout(editWatchdogRef.current);\n    }\n    opIdRef.current += 1;\n    const id = opIdRef.current;\n    editWatchdogRef.current = setTimeout(() => {\n      editWatchdogRef.current = null;\n      opIdRef.current += 1; // invalidate any late-arriving result\n      setContentState((prev) => ({\n        ...prev,\n        isFfmpegRunning: false,\n        muting: false,\n        cutting: false,\n        trimming: false,\n        reencoding: false,\n        cropping: false,\n        processingProgress: 0,\n        editErrorType: \"timeout\",\n      }));\n    }, 5 * 60 * 1000); // 5-minute ceiling\n    return id;\n  };\n\n  // Clears the watchdog when an op completes or fails normally.\n  const clearEditOp = () => {\n    if (editWatchdogRef.current) {\n      clearTimeout(editWatchdogRef.current);\n      editWatchdogRef.current = null;\n    }\n  };\n\n  const addAudio = async (videoBlob, audioBlob, volume) => {\n    if (contentState.isFfmpegRunning) return;\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n\n    const sourceBlob = videoBlob || contentState.blob || contentState.webm;\n    const opId = beginEditOp();\n\n    setContentState((prev) => ({\n      ...prev,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n      editErrorType: null,\n    }));\n\n    sendMessage({\n      type: \"add-audio-to-video\",\n      blob: sourceBlob,\n      audio: audioBlob,\n      duration: contentState.duration,\n      volume: volume,\n      replaceAudio: contentState.replaceAudio,\n      topLevel: false,\n      _opId: opId,\n    });\n  };\n\n  const handleTrim = async (cut) => {\n    if (contentState.isFfmpegRunning) return;\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n\n    const sourceBlob = contentState.blob;\n    const opId = beginEditOp();\n\n    setContentState((prev) => ({\n      ...prev,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n      editErrorType: null,\n      [cut ? \"cutting\" : \"trimming\"]: true,\n    }));\n\n    sendMessage({\n      type: \"cut-video\",\n      blob: sourceBlob,\n      startTime: contentState.start * contentState.duration,\n      endTime: contentState.end * contentState.duration,\n      cut,\n      duration: contentState.duration,\n      encode: false,\n      topLevel: false,\n      _opId: opId,\n    });\n  };\n\n  const handleMute = async () => {\n    if (contentState.isFfmpegRunning) return;\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n\n    const sourceBlob = contentState.blob;\n    const opId = beginEditOp();\n\n    setContentState((prev) => ({\n      ...prev,\n      muting: true,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n      editErrorType: null,\n    }));\n\n    sendMessage({\n      type: \"mute-video\",\n      blob: sourceBlob,\n      startTime: contentState.start * contentState.duration,\n      endTime: contentState.end * contentState.duration,\n      duration: contentState.duration,\n      topLevel: false,\n      _opId: opId,\n    });\n  };\n\n  const handleCrop = async (x, y, width, height) => {\n    if (contentState.isFfmpegRunning || contentState.cropping) return;\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n\n    const opId = beginEditOp();\n\n    setContentState((prevState) => ({\n      ...prevState,\n      cropping: true,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n      editErrorType: null,\n    }));\n\n    const sourceBlob = contentState.blob;\n\n    sendMessage({\n      type: \"crop-video\",\n      blob: sourceBlob,\n      x,\n      y,\n      width,\n      height,\n      topLevel: false,\n      _opId: opId,\n    });\n\n    return true;\n  };\n\n  const handleReencode = async (topLevel = false) => {\n    if (contentState.isFfmpegRunning) return;\n\n    const sourceBlob = contentState.blob;\n    const opId = beginEditOp();\n\n    setContentState((prevState) => ({\n      ...prevState,\n      isFfmpegRunning: true,\n      reencoding: true,\n      processingProgress: 0,\n      editErrorType: null,\n    }));\n\n    sendMessage({\n      type: \"reencode-video\",\n      blob: sourceBlob,\n      duration: contentState.duration,\n      topLevel,\n      _opId: opId,\n    });\n\n    return true;\n  };\n\n  const sanitizeDownloadFilename = (name, { maxLen = 180 } = {}) => {\n    let out = String(name ?? \"\");\n    out = out.replace(/[\\\\/:*?\"<>|]/g, \" \");\n    out = out.replace(/[\\u0000-\\u001F\\u007F]/g, \" \");\n    out = out.replace(/\\s+/g, \" \").trim();\n    out = out.replace(/[. ]+$/g, \"\");\n\n    if (!out) out = \"Screenity recording\";\n    if (out.length > maxLen) out = out.slice(0, maxLen).trim();\n\n    return out;\n  };\n\n  const requestDownload = async (url, ext) => {\n    const rawTitle = contentStateRef.current.title || \"Screenity recording\";\n\n    const base = sanitizeDownloadFilename(rawTitle);\n    const filename = `${base}${ext}`;\n\n    const revoke = () => {\n      try {\n        URL.revokeObjectURL(url);\n      } catch {}\n    };\n\n    // Brave fallback: send base64 to background for download\n    if ((navigator.brave && (await navigator.brave.isBrave())) || false) {\n      const resp = await fetch(url);\n      const blob = await resp.blob();\n      await new Promise((resolve) => {\n        const reader = new FileReader();\n        reader.onloadend = () => {\n          chrome.runtime.sendMessage({\n            type: \"request-download\",\n            base64: reader.result,\n            title: filename,\n          });\n          revoke();\n          resolve();\n        };\n        reader.readAsDataURL(blob);\n      });\n      return;\n    }\n\n    const downloadId = await new Promise((resolve, reject) => {\n      chrome.downloads.download({ url, filename, saveAs: true }, (id) => {\n        if (chrome.runtime.lastError || !id) {\n          reject(chrome.runtime.lastError || new Error(\"Download failed\"));\n        } else {\n          resolve(id);\n        }\n      });\n    });\n\n    await new Promise((resolve) => {\n      const handler = async (delta) => {\n        if (delta.id !== downloadId || !delta.state) return;\n\n        const done = () => {\n          chrome.downloads.onChanged.removeListener(handler);\n          revoke();\n          resolve();\n        };\n\n        if (\n          delta.state.current === \"interrupted\" &&\n          delta.error?.current !== \"USER_CANCELED\"\n        ) {\n          try {\n            const resp = await fetch(url);\n            const blob = await resp.blob();\n            await new Promise((res) => {\n              const reader = new FileReader();\n              reader.onloadend = () => {\n                chrome.runtime.sendMessage({\n                  type: \"request-download\",\n                  base64: reader.result,\n                  title: filename,\n                });\n                res();\n              };\n              reader.readAsDataURL(blob);\n            });\n          } finally {\n            done();\n          }\n        } else if (\n          delta.state.current === \"complete\" ||\n          delta.state.current === \"interrupted\"\n        ) {\n          done();\n        }\n      };\n\n      chrome.downloads.onChanged.addListener(handler);\n    });\n  };\n\n  const download = async () => {\n    if (contentState.isFfmpegRunning || contentState.downloading) return;\n\n    setContentState((prev) => ({\n      ...prev,\n      downloading: true,\n      isFfmpegRunning: true,\n    }));\n\n    try {\n      const url = URL.createObjectURL(contentState.blob);\n      await requestDownload(url, \".mp4\");\n      setContentState((prev) => ({ ...prev, saved: true }));\n    } catch (err) {\n      console.error(\"MP4 download failed:\", err);\n    } finally {\n      setContentState((prev) => ({\n        ...prev,\n        downloading: false,\n        isFfmpegRunning: false,\n      }));\n    }\n  };\n\n  const downloadWEBM = async () => {\n    if (contentState.isFfmpegRunning || contentState.downloadingWEBM) return;\n\n    const sourceBlob = contentState.blob || contentState.webm;\n\n    if (!sourceBlob) {\n      return;\n    }\n\n    const hasFFmpeg = contentState.ffmpegLoaded && !contentState.noffmpeg;\n    const isAlreadyWebm = sourceBlob.type === \"video/webm\";\n\n    if (!hasFFmpeg || isAlreadyWebm) {\n      const url = URL.createObjectURL(sourceBlob);\n      await requestDownload(url, \".webm\");\n\n      setContentState((prevState) => ({\n        ...prevState,\n        downloadingWEBM: false,\n        isFfmpegRunning: false,\n        saved: true,\n      }));\n      return;\n    }\n\n    // ➜ ADD THIS: If untouched + already webm → skip FFmpeg entirely\n    if (!contentState.hasBeenEdited && contentState.webm) {\n      const url = URL.createObjectURL(contentState.webm);\n      await requestDownload(url, \".webm\");\n\n      setContentState((prev) => ({\n        ...prev,\n        downloadingWEBM: false,\n        isFfmpegRunning: false,\n        saved: true,\n      }));\n      return;\n    }\n\n    setContentState((prevState) => ({\n      ...prevState,\n      downloadingWEBM: true,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n    }));\n\n    sendMessage({\n      type: \"to-webm\",\n      blob: sourceBlob,\n      duration: contentState.duration,\n    });\n\n    await waitForUpdatedBlob();\n\n    setContentState((prevState) => ({\n      ...prevState,\n      downloadingWEBM: false,\n      isFfmpegRunning: false,\n      saved: true,\n    }));\n  };\n\n  const downloadGIF = async () => {\n    if (contentState.isFfmpegRunning || contentState.downloading) {\n      return;\n    }\n    if (contentState.duration > 30) return;\n\n    setContentState((prevState) => ({\n      ...prevState,\n      downloadingGIF: true,\n      isFfmpegRunning: true,\n    }));\n\n    sendMessage({\n      type: \"to-gif\",\n      blob: contentState.blob,\n    });\n  };\n\n  const loadFFmpeg = async () => {\n    sendMessage({ type: \"load-ffmpeg\" });\n  };\n\n  const waitForUpdatedBlob = () => {\n    return new Promise((resolve) => {\n      const handler = (event) => {\n        if (event.data?.type === \"updated-blob\") {\n          window.removeEventListener(\"message\", handler);\n          resolve();\n        }\n      };\n      window.addEventListener(\"message\", handler);\n    });\n  };\n\n  contentState.undo = undo;\n  contentState.redo = redo;\n  contentState.addToHistory = addToHistory;\n  contentState.handleTrim = handleTrim;\n  contentState.handleMute = handleMute;\n  contentState.download = download;\n  contentState.handleCrop = handleCrop;\n  contentState.handleReencode = handleReencode;\n  contentState.getFrame = getImage;\n  contentState.downloadGIF = downloadGIF;\n  contentState.downloadWEBM = downloadWEBM;\n  contentState.addAudio = addAudio;\n  contentState.loadFFmpeg = loadFFmpeg;\n  contentState.waitForUpdatedBlob = waitForUpdatedBlob;\n  contentState.createBackup = createBackup;\n  contentState.restoreBackup = restoreBackup;\n  contentState.clearBackup = clearBackup;\n\n  return (\n    <ContentStateContext.Provider value={[contentState, setContentState]}>\n      {props.children}\n      {process.env.SCREENITY_DEV_MODE === \"true\" && (\n        <DevHUD\n          setContentState={setContentState}\n          contentStateRef={contentStateRef}\n        />\n      )}\n    </ContentStateContext.Provider>\n  );\n};\n\nexport default ContentState;\n"
  },
  {
    "path": "src/pages/Sandbox/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Screenity - Video editor</title>\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(/assets/fonts/Satoshi-Light.ttf) format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(/assets/fonts/Satoshi-Medium.ttf) format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(/assets/fonts/Satoshi-Bold.ttf) format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n    </style>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Sandbox/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport Sandbox from \"./Sandbox\";\nimport ContentState from \"./context/ContentState\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(\n    <ContentState>\n      <Sandbox />\n    </ContentState>\n  );\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/AudioNav.js",
    "content": "import React, { useContext } from \"react\";\nimport styles from \"../../styles/edit/_EditorNav.module.scss\";\nimport { ContentStateContext } from \"../../context/ContentState\";\n\nconst URL = \"/assets/\";\n\nconst AudioNav = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext);\n\n  const handleCancel = () => {\n    setContentState((prev) => ({\n      ...prev,\n      mode: \"player\",\n      start: 0,\n      end: 1,\n      pendingAudio: null,\n    }));\n    contentState.restoreBackup();\n  };\n\n  const handleRevert = () => {\n    setContentState((prev) => ({\n      ...prev,\n      blob: contentState.originalBlob,\n      start: 0,\n      end: 1,\n      pendingAudio: null,\n    }));\n  };\n\n  const saveChanges = async () => {\n    const { pendingAudio, volume } = contentState;\n    const source = contentState.blob;\n\n    setContentState((prev) => ({\n      ...prev,\n      isFfmpegRunning: true,\n      processingProgress: 0,\n      fromAudio: true,\n    }));\n\n    if (pendingAudio) {\n      contentState.addAudio(source, pendingAudio, volume);\n    } else {\n      contentState.handleReencode(true);\n    }\n\n    await contentState.waitForUpdatedBlob?.();\n\n    contentState.clearBackup();\n  };\n\n  return (\n    <div className={styles.editorNav}>\n      <div className={styles.navWrap}>\n        <div\n          className={styles.editorNavLeft}\n          onClick={() => chrome.runtime.sendMessage({ type: \"open-home\" })}\n        >\n          <img src={URL + \"editor/logo.svg\"} alt=\"Logo\" />\n        </div>\n\n        <div className={styles.editorNavCenter}>\n          <div className={styles.editorNavTitle}>\n            {chrome.i18n.getMessage(\"sandboxEditorMainTitle\")}{\" \"}\n            <span className={styles.beta}>BETA</span>\n          </div>\n        </div>\n\n        <div className={styles.editorNavRight}>\n          <button\n            className=\"button simpleButton blackButton\"\n            onClick={handleCancel}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorCancelButton\")}\n          </button>\n\n          <button\n            className=\"button secondaryButton\"\n            onClick={handleRevert}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorRevertButton\")}\n          </button>\n\n          <button\n            className=\"button primaryButton\"\n            onClick={saveChanges}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {contentState.isFfmpegRunning ? (\n              contentState.processingProgress > 0 ? (\n                <>\n                  {chrome.i18n.getMessage(\"sandboxEditorSaveProgressButton\") ||\n                    \"Saving\"}{\" \"}\n                  {Math.round(contentState.processingProgress)}%\n                </>\n              ) : (\n                chrome.i18n.getMessage(\"sandboxEditorSaveProgressButton\") ||\n                \"Saving...\"\n              )\n            ) : (\n              chrome.i18n.getMessage(\"sandboxEditorSaveButton\") ||\n              \"Save changes\"\n            )}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default AudioNav;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/AudioUI.js",
    "content": "import React, { useState, useEffect, useContext, useRef } from \"react\";\nimport styles from \"../../styles/player/_RightPanel.module.scss\";\n\n// Components\nimport Dropdown from \"../../components/editor/Dropdown\";\nimport * as Slider from \"@radix-ui/react-slider\";\nimport Switch from \"../../components/editor/Switch\";\n\n// Icons\nconst URL = \"/assets/\";\n\nimport { ReactSVG } from \"react-svg\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst AudioUI = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const [audio, setAudio] = useState(null);\n  const prevBlob = useRef(null);\n  const inputRef = useRef(null);\n\n  useEffect(() => {\n    if (contentState.blob !== prevBlob.current) {\n      prevBlob.current = contentState.blob;\n      setAudio(null);\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        volume: 1,\n      }));\n    }\n  }, []);\n\n  const handleAudio = async (e) => {\n    const file = e.target.files[0];\n    // Check if the file is an audio file\n    if (!file.type.includes(\"audio\") || file.size === 0) {\n      return;\n    }\n\n    // Use rawBlob (webm from recovery) if blob (mp4) is not available yet\n    const videoBlob =\n      contentState.blob || contentState.rawBlob || contentState.webm;\n    //contentState.addAudio(videoBlob, file, contentState.volume);\n    setAudio(file);\n    setContentState((prev) => ({\n      ...prev,\n      pendingAudio: file,\n    }));\n  };\n\n  const handleVolume = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n    if (value > 100) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      volume: parseFloat(value) / 100,\n    }));\n  };\n\n  return (\n    <div>\n      <div className={styles.section}>\n        <div className={styles.sectionTitle}>Audio upload</div>\n        <input\n          type=\"file\"\n          accept=\"audio/*\"\n          onChange={handleAudio}\n          style={{ display: \"none\" }}\n          ref={inputRef}\n        />\n        {!audio && (\n          <div\n            className={styles.uploadArea}\n            onClick={() => inputRef.current.click()}\n          >\n            <ReactSVG src={URL + \"editor/icons/upload.svg\"} />\n            <div className={styles.uploadDetails}>\n              <div className={styles.uploadText}>\n                {chrome.i18n.getMessage(\"sandboxAudioDragAndDrop\")}\n              </div>\n              <div className={styles.uploadDescription}>\n                {chrome.i18n.getMessage(\"sandboxAudioOrBrowse\")}\n              </div>\n            </div>\n          </div>\n        )}\n        {audio && (\n          <div className={styles.audioDetails}>\n            <div className={styles.audioDetailsLeft}>\n              <ReactSVG src={URL + \"editor/icons/attachment.svg\"} />\n            </div>\n            <div className={styles.audioDetailsMiddle}>\n              <span>{audio.name}</span>\n            </div>\n            <div className={styles.audioDetailsRight}>\n              <ReactSVG\n                src={URL + \"editor/icons/cross.svg\"}\n                onClick={() => {\n                  setAudio(null);\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    blob: prevBlob.current,\n                  }));\n                  inputRef.current.value = null;\n                }}\n              />\n            </div>\n          </div>\n        )}\n      </div>\n      <div className={styles.section}>\n        <div className={styles.sectionTitle}>\n          {chrome.i18n.getMessage(\"sandboxAudioSettingsTitle\")}\n        </div>\n        <div className={styles.inputs}>\n          <div className={`${styles.input} ${styles.inputVolume}`}>\n            <div className={styles.inputTitle}>\n              {chrome.i18n.getMessage(\"sandboxAudioVolumeLabel\")}\n            </div>\n            <input\n              type=\"text\"\n              className=\"input\"\n              onChange={(e) => handleVolume(e)}\n              value={Math.round(contentState.volume * 100)}\n              onBlur={(e) => {\n                if (e.target.value === \"\") {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    volume: 0,\n                  }));\n                }\n              }}\n            />\n            <span>%</span>\n          </div>\n          <Slider.Root\n            className={styles.SliderRoot}\n            max={100}\n            step={1}\n            onValueChange={(newValue) => {\n              setContentState((prevContentState) => ({\n                ...prevContentState,\n                // Round to the nearest integer\n                volume: Math.round(newValue) / 100,\n              }));\n            }}\n            value={[contentState.volume * 100]}\n          >\n            <Slider.Track className={styles.SliderTrack}>\n              <Slider.Range className={styles.SliderRange} />\n            </Slider.Track>\n            <Slider.Thumb className={styles.SliderThumb} aria-label=\"Volume\" />\n          </Slider.Root>\n        </div>\n        <Switch />\n        {/* <button\n          className={[\"button\", \"primaryButton\", styles.updateButton].join(\" \")}\n          onClick={updateAudio}\n          disabled={contentState.isFfmpegRunning || !audio}\n        >\n          {chrome.i18n.getMessage(\"sandboxAudioUpdateButton\")}\n        </button> */}\n      </div>\n    </div>\n  );\n};\n\nexport default AudioUI;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/CropNav.js",
    "content": "import React, { useContext } from \"react\";\nimport styles from \"../../styles/edit/_EditorNav.module.scss\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst URL = \"/assets/\";\n\nconst CropNav = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const handleCancel = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      mode: \"player\",\n      start: 0,\n      end: 1,\n      width: contentState.prevWidth,\n      height: contentState.prevHeight,\n      left: 0,\n      top: 0,\n      fromCropper: false,\n    }));\n\n    contentState.clearBackup();\n  };\n\n  const handleRevert = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      blob: contentState.originalBlob,\n      start: 0,\n      end: 1,\n      width: contentState.prevWidth,\n      height: contentState.prevHeight,\n      left: 0,\n      top: 0,\n      fromCropper: false,\n    }));\n  };\n\n  const saveChanges = async () => {\n    await contentState.handleCrop(\n      contentState.left,\n      contentState.top,\n      contentState.width,\n      contentState.height\n    );\n\n    setContentState((prev) => ({\n      ...prev,\n      // mode: \"player\",\n      fromCropper: true,\n      hasTempChanges: false,\n    }));\n\n    contentState.clearBackup();\n  };\n\n  return (\n    <div className={styles.editorNav}>\n      <div className={styles.navWrap}>\n        <div\n          className={styles.editorNavLeft}\n          onClick={() => {\n            chrome.runtime.sendMessage({ type: \"open-home\" });\n          }}\n        >\n          <img src={URL + \"editor/logo.svg\"} alt=\"Logo\" />\n        </div>\n        <div className={styles.editorNavCenter}>\n          <div className={styles.editorNavTitle}>\n            {chrome.i18n.getMessage(\"sandboxEditorMainTitle\") + \" \"}{\" \"}\n            <span className={styles.beta}>BETA</span>\n          </div>\n        </div>\n        <div className={styles.editorNavRight}>\n          <button\n            className=\"button simpleButton blackButton\"\n            onClick={handleCancel}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorCancelButton\")}\n          </button>\n          <button\n            className=\"button secondaryButton\"\n            onClick={handleRevert}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorRevertButton\")}\n          </button>\n          <button\n            className=\"button primaryButton\"\n            onClick={saveChanges}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {contentState.isFfmpegRunning ? (\n              contentState.processingProgress > 0 ? (\n                <>\n                  {chrome.i18n.getMessage(\"sandboxEditorSaveProgressButton\") ||\n                    \"Saving\"}{\" \"}\n                  {Math.round(contentState.processingProgress)}%\n                </>\n              ) : (\n                chrome.i18n.getMessage(\"sandboxEditorSaveProgressButton\") ||\n                \"Saving...\"\n              )\n            ) : (\n              chrome.i18n.getMessage(\"sandboxEditorSaveButton\") ||\n              \"Save changes\"\n            )}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default CropNav;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/CropUI.js",
    "content": "import React, { useState, useEffect, useContext } from \"react\";\nimport styles from \"../../styles/player/_RightPanel.module.scss\";\n\n// Components\nimport Dropdown from \"../../components/editor/Dropdown\";\n\nimport { ReactSVG } from \"react-svg\";\n\nconst URL =\n  \"chrome-extension://\" + chrome.i18n.getMessage(\"@@extension_id\") + \"/assets/\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst CropUI = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const handleWidth = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      width: parseFloat(value),\n      cropPreset: \"none\",\n      fromCropper: false,\n    }));\n  };\n\n  const handleHeight = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      height: parseFloat(value),\n      cropPreset: \"none\",\n      fromCropper: false,\n    }));\n  };\n\n  const handleTop = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      top: parseFloat(value),\n      fromCropper: false,\n    }));\n  };\n\n  const handleLeft = (e) => {\n    let value = e.target.value;\n    if (isNaN(value)) {\n      return;\n    }\n    if (value < 0) {\n      return;\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      left: parseFloat(value),\n      fromCropper: false,\n    }));\n  };\n\n  return (\n    <div>\n      {/*\n      <div className={styles.section}>\n        <div className={styles.sectionTitle}>Dimensions</div>\n        <div className={styles.inputSection}>\n          <div className={styles.inputSectionTitle}>Preset</div>\n          <Dropdown />\n        </div>\n      </div>\n\t\t\t\t\t\t\t*/}\n\n      <div className={styles.alert}>\n        <div className={styles.buttonLeft}>\n          <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n        </div>\n        <div className={styles.buttonMiddle}>\n          <div className={styles.buttonTitle}>\n            {chrome.i18n.getMessage(\"croppingInfoTitle\")}\n          </div>\n          <div className={styles.buttonDescription}>\n            {chrome.i18n.getMessage(\"videoProcessingLabelDescription\")}\n          </div>\n        </div>\n        <div\n          className={styles.buttonRight}\n          onClick={() => {\n            chrome.runtime.sendMessage({ type: \"upgrade-info\" });\n          }}\n        >\n          {chrome.i18n.getMessage(\"learnMoreLabel\")}\n        </div>\n      </div>\n      <div className={styles.section}>\n        <div className={styles.sectionTitle}>\n          {chrome.i18n.getMessage(\"sandboxCropTitle\")}\n        </div>\n        <div className={styles.inputs}>\n          <div className={styles.input}>\n            <div className={styles.inputTitle}>\n              {chrome.i18n.getMessage(\"widthLabel\")}\n            </div>\n            <input\n              type=\"text\"\n              className=\"input\"\n              onChange={(e) => handleWidth(e)}\n              onBlur={(e) => {\n                if (e.target.value === \"\") {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    width: 0,\n                  }));\n                }\n              }}\n              value={contentState.width}\n            />\n            <span>px</span>\n          </div>\n          <div className={styles.input}>\n            <div className={styles.inputTitle}>\n              {chrome.i18n.getMessage(\"heightLabel\")}\n            </div>\n            <input\n              type=\"text\"\n              className=\"input\"\n              onChange={(e) => handleHeight(e)}\n              value={contentState.height}\n              onBlur={(e) => {\n                if (e.target.value === \"\") {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    height: 0,\n                  }));\n                }\n              }}\n            />\n            <span>px</span>\n          </div>\n        </div>\n        <div className={styles.inputs}>\n          <div className={styles.input}>\n            <div className={styles.inputTitle}>\n              {chrome.i18n.getMessage(\"leftLabel\")}\n            </div>\n            <input\n              type=\"text\"\n              className=\"input\"\n              onChange={(e) => handleLeft(e)}\n              onBlur={(e) => {\n                if (e.target.value === \"\") {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    left: 0,\n                  }));\n                }\n              }}\n              value={contentState.left}\n            />\n            <span>px</span>\n          </div>\n          <div className={styles.input}>\n            <div className={styles.inputTitle}>\n              {chrome.i18n.getMessage(\"topLabel\")}\n            </div>\n            <input\n              type=\"text\"\n              className=\"input\"\n              onChange={(e) => handleTop(e)}\n              onBlur={(e) => {\n                if (e.target.value === \"\") {\n                  setContentState((prevContentState) => ({\n                    ...prevContentState,\n                    top: 0,\n                  }));\n                }\n              }}\n              value={contentState.top}\n            />\n            <span>px</span>\n          </div>\n          {/* <button\n            className={[\"button\", \"primaryButton\", styles.updateButton].join(\n              \" \"\n            )}\n            onClick={() => {\n              contentState.handleCrop(\n                contentState.left,\n                contentState.top,\n                contentState.width,\n                contentState.height\n              );\n            }}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxCropUpdateButton\") || \"Update crop\"}\n          </button> */}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default CropUI;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/Editor.js",
    "content": "import React, { useState, useEffect, useContext } from \"react\";\nimport EditorNav from \"./EditorNav\";\nimport VideoPlayer from \"../../components/editor/VideoPlayer\";\nimport TrimUI from \"./TrimUI\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nimport HelpButton from \"../../components/player/HelpButton\";\n\nconst Editor = ({ ffmpeg }) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const handleSeek = (t, updateTime) => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      updatePlayerTime: updateTime,\n      time: t,\n    }));\n  };\n\n  useEffect(() => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      history: [{}],\n      redoHistory: [],\n    }));\n    contentState.addToHistory();\n  }, []);\n\n  return (\n    <div>\n      <EditorNav />\n      <VideoPlayer onSeek={handleSeek} />\n      <TrimUI blob={contentState.blob} onSeek={handleSeek} />\n      <HelpButton />\n    </div>\n  );\n};\n\nexport default Editor;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/EditorNav.js",
    "content": "import React, { useContext } from \"react\";\nimport styles from \"../../styles/edit/_EditorNav.module.scss\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst URL = \"/assets/\";\n\nconst EditorNav = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  const handleCancel = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      mode: \"player\",\n      start: 0,\n      end: 1,\n    }));\n\n    contentState.restoreBackup();\n  };\n\n  const handleRevert = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      blob: contentState.originalBlob,\n      start: 0,\n      end: 1,\n    }));\n  };\n\n  const saveChanges = async () => {\n    if (contentState.isFfmpegRunning) return;\n\n    setContentState((prev) => ({\n      ...prev,\n      mode: \"player\",\n      hasTempChanges: false,\n      start: 0,\n      end: 1,\n    }));\n\n    contentState.clearBackup();\n  };\n\n  return (\n    <div className={styles.editorNav}>\n      <div className={styles.navWrap}>\n        <div\n          className={styles.editorNavLeft}\n          onClick={() => {\n            chrome.runtime.sendMessage({ type: \"open-home\" });\n          }}\n        >\n          <img src={URL + \"editor/logo.svg\"} alt=\"Logo\" />\n        </div>\n        <div className={styles.editorNavCenter}>\n          <div className={styles.editorNavTitle}>\n            {chrome.i18n.getMessage(\"sandboxEditorMainTitle\")}{\" \"}\n            <span className={styles.beta}>BETA</span>\n          </div>\n        </div>\n        <div className={styles.editorNavRight}>\n          <button\n            className=\"button simpleButton blackButton\"\n            onClick={handleCancel}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorCancelButton\")}\n          </button>\n          <button\n            className=\"button secondaryButton\"\n            onClick={handleRevert}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {chrome.i18n.getMessage(\"sandboxEditorRevertButton\")}\n          </button>\n\n          <button\n            className=\"button primaryButton\"\n            onClick={saveChanges}\n            disabled={contentState.isFfmpegRunning}\n          >\n            {contentState.reencoding\n              ? chrome.i18n.getMessage(\"sandboxEditorSaveProgressButton\")\n              : chrome.i18n.getMessage(\"sandboxEditorSaveButton\")}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default EditorNav;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/editor/TrimUI.js",
    "content": "import React, { useContext, useState, useEffect } from \"react\";\nimport Trimmer from \"../../components/editor/Trimmer\";\nimport styles from \"../../styles/edit/_TrimUI.module.scss\";\n\n// Icons\nimport { ReactSVG } from \"react-svg\";\n\nconst URL = \"/assets/\";\n\nconst TrimIcon = URL + \"editor/icons/trim.svg\";\nconst RemoveIcon = URL + \"editor/icons/trash.svg\";\nconst MuteIcon = URL + \"editor/icons/mute.svg\";\nconst UndoIcon = URL + \"editor/icons/undo.svg\";\nconst RedoIcon = URL + \"editor/icons/redo.svg\";\nconst TimeIcon = URL + \"editor/icons/time.svg\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst TrimUI = (props) => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const [undoDisabled, setUndoDisabled] = useState(true);\n  const [redoDisabled, setRedoDisabled] = useState(true);\n  const [startTime, setStartTime] = useState(0);\n  const [endTime, setEndTime] = useState(0);\n\n  useEffect(() => {\n    setStartTime(contentState.start * contentState.duration);\n    setEndTime(contentState.end * contentState.duration);\n  }, [contentState.duration, contentState.start, contentState.end]);\n\n  useEffect(() => {\n    if (contentState.history.length > 2) {\n      setUndoDisabled(false);\n    } else {\n      setUndoDisabled(true);\n    }\n  }, [contentState.history]);\n\n  useEffect(() => {\n    if (contentState.redoHistory.length > 0) {\n      setRedoDisabled(false);\n    } else {\n      setRedoDisabled(true);\n    }\n  }, [contentState.redoHistory]);\n\n  const toTimeStamp = (time) => {\n    const minutes = Math.floor(time / 60);\n    const seconds = Math.floor(time - minutes * 60);\n\n    if (seconds < 10) {\n      return `${minutes}:0${seconds}`;\n    } else {\n      return `${minutes}:${seconds}`;\n    }\n  };\n\n  return (\n    <div className={styles.trimWrap}>\n      <div className={styles.controls}>\n        <div className={styles.actions}>\n          <button\n            className=\"button secondaryButton\"\n            onClick={() => contentState.handleTrim(false)}\n            disabled={\n              contentState.isFfmpegRunning ||\n              (contentState.start === 0 && contentState.end === 1)\n            }\n          >\n            <ReactSVG src={TrimIcon} />\n            {contentState.trimming\n              ? chrome.i18n.getMessage(\"sandboxEditorTrimProgressButton\") +\n                (contentState.processingProgress > 0\n                  ? ` ${contentState.processingProgress}%`\n                  : \"\")\n              : chrome.i18n.getMessage(\"sandboxEditorTrimButton\")}\n          </button>\n          <button\n            className=\"button secondaryButton\"\n            disabled={\n              (contentState.start === 0 && contentState.end === 1) ||\n              contentState.isFfmpegRunning\n            }\n            onClick={() => contentState.handleTrim(true)}\n          >\n            <ReactSVG src={RemoveIcon} />\n            {contentState.cutting\n              ? chrome.i18n.getMessage(\"sandboxEditorCutProgressButton\") +\n                (contentState.processingProgress > 0\n                  ? ` ${contentState.processingProgress}%`\n                  : \"\")\n              : chrome.i18n.getMessage(\"sandboxEditorCutButton\")}\n          </button>\n          <button\n            className=\"button secondaryButton\"\n            onClick={() => contentState.handleMute()}\n            disabled={contentState.isFfmpegRunning}\n          >\n            <ReactSVG src={MuteIcon} />\n            {contentState.muting\n              ? chrome.i18n.getMessage(\"sandboxEditorMuteProgressButton\") +\n                (contentState.processingProgress > 0\n                  ? ` ${contentState.processingProgress}%`\n                  : \"\")\n              : chrome.i18n.getMessage(\"sandboxEditorMuteButton\")}\n          </button>\n        </div>\n        <div className={styles.timeWrap}>\n          <ReactSVG src={TimeIcon} />\n          <span>{toTimeStamp(startTime) + \" - \" + toTimeStamp(endTime)}</span>\n        </div>\n\n        <div className={styles.controlsRight}>\n          <button\n            className=\"button simpleButton\"\n            onClick={() => contentState.undo()}\n            disabled={undoDisabled || contentState.isFfmpegRunning}\n          >\n            <ReactSVG src={UndoIcon} />\n            {chrome.i18n.getMessage(\"undoLabel\")}\n          </button>\n          <button\n            className=\"button simpleButton\"\n            onClick={() => contentState.redo()}\n            disabled={redoDisabled || contentState.isFfmpegRunning}\n          >\n            <ReactSVG src={RedoIcon} />\n            {chrome.i18n.getMessage(\"redoLabel\")}\n          </button>\n        </div>\n      </div>\n      <Trimmer />\n      {(!contentState.dragInteracted || contentState.duration > 3) && (\n        <div className={styles.trimInfo}>\n          <div className={styles.trimInfoLeft}>\n            <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n          </div>\n          <div className={styles.trimInfoRight}>\n            {chrome.i18n.getMessage(\"sandboxEditorTrimInfo\")}\n            <div\n              className={styles.trimInfoLink}\n              onClick={() => {\n                chrome.runtime.sendMessage({ type: \"trim-info\" });\n              }}\n            >\n              {chrome.i18n.getMessage(\"learnMoreDot\")}\n            </div>\n          </div>\n        </div>\n      )}\n      {contentState.dragInteracted && contentState.duration <= 3 && (\n        <div className={styles.trimInfo}>\n          <div className={styles.trimInfoLeft}>\n            <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n          </div>\n          <div className={styles.trimInfoRight}>\n            {chrome.i18n.getMessage(\"sandboxEditorTooSmallInfo\")}\n            <div\n              className={styles.trimInfoLink}\n              onClick={() => {\n                chrome.runtime.sendMessage({ type: \"trim-info\" });\n              }}\n            >\n              {chrome.i18n.getMessage(\"learnMoreDot\")}\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default TrimUI;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/player/Content.js",
    "content": "import React, { useContext, useState } from \"react\";\nimport styles from \"../../styles/player/_Content.module.scss\";\n\n// Components\nimport VideoPlayer from \"../../components/player/VideoPlayer\";\nimport CropperWrap from \"../../components/editor/CropperWrap\";\nimport HelpButton from \"../../components/player/HelpButton\";\nimport ProBanner from \"../../components/global/ProBanner\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Content = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  return (\n    <div className={styles.content}>\n      <div className={styles.wrap}>\n        {contentState.mode === \"audio\" && <VideoPlayer />}\n        {contentState.mode === \"player\" && <VideoPlayer />}\n        {contentState.mode === \"crop\" && <CropperWrap />}\n      </div>\n      <HelpButton />\n      {contentState.bannerSupport && <ProBanner />}\n    </div>\n  );\n};\n\nexport default Content;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/player/Player.js",
    "content": "import React, { useContext, useState } from \"react\";\n\n// Components\nimport PlayerNav from \"./PlayerNav\";\nimport CropNav from \"../editor/CropNav\";\nimport AudioNav from \"../editor/AudioNav\";\nimport RightPanel from \"./RightPanel\";\nimport Content from \"./Content\";\n\nimport styles from \"../../styles/player/_Player.module.scss\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst Player = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n\n  return (\n    <div className={styles.layout}>\n      {contentState.mode === \"crop\" && <CropNav />}\n      {contentState.mode === \"player\" && <PlayerNav />}\n      {contentState.mode === \"audio\" && <AudioNav />}\n      <div className={styles.content}>\n        <Content />\n        <RightPanel />\n      </div>\n    </div>\n  );\n};\n\nexport default Player;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/player/PlayerNav.js",
    "content": "import React, { useContext, useRef, useEffect } from \"react\";\nimport styles from \"../../styles/player/_Nav.module.scss\";\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\n// Icons\nimport { ReactSVG } from \"react-svg\";\n\nconst URL = \"/assets/\";\n\nconst StarIcon = URL + \"editor/icons/help-nav.svg\";\nconst HeartIcon = URL + \"editor/icons/heart.svg\";\nconst UnlockIcon = URL + \"editor/icons/unlock.svg\";\n\nconst PlayerNav = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const contentStateRef = useRef(null);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  return (\n    <div className={styles.nav}>\n      <div className={styles.navWrap}>\n        <div\n          onClick={() => {\n            chrome.runtime.sendMessage({ type: \"open-home\" });\n          }}\n          aria-label=\"home\"\n          className={styles.navLeft}\n        >\n          <img src={URL + \"editor/logo.svg\"} alt=\"Screenity Logo\" />\n        </div>\n        <div className={styles.navRight}>\n          <button\n            className=\"button simpleButton blueButton\"\n            onClick={() => {\n              chrome.runtime.sendMessage({ type: \"open-help\" });\n            }}\n          >\n            <ReactSVG src={StarIcon} />\n            {chrome.i18n.getMessage(\"getHelpNav\")}\n          </button>\n          <button\n            className=\"button primaryButton\"\n            onClick={() => {\n              chrome.runtime.sendMessage({ type: \"pricing\" });\n            }}\n          >\n            <ReactSVG src={UnlockIcon} />{\" \"}\n            {chrome.i18n.getMessage(\"unlockMoreFeatures\") ||\n              \"Unlock more features\"}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default PlayerNav;\n"
  },
  {
    "path": "src/pages/Sandbox/layout/player/RightPanel.js",
    "content": "import React, { useContext, useEffect, useState, useRef } from \"react\";\nimport styles from \"../../styles/player/_RightPanel.module.scss\";\n\nimport { buildDiagnosticZip } from \"../../../utils/buildDiagnosticZip\";\n\nimport { ReactSVG } from \"react-svg\";\n\nconst URL =\n  \"chrome-extension://\" + chrome.i18n.getMessage(\"@@extension_id\") + \"/assets/\";\n\n// Components\nimport CropUI from \"../editor/CropUI\";\nimport AudioUI from \"../editor/AudioUI\";\n\n// Context\nimport { ContentStateContext } from \"../../context/ContentState\"; // Import the ContentState context\n\nconst RightPanel = () => {\n  const [contentState, setContentState] = useContext(ContentStateContext); // Access the ContentState context\n  const contentStateRef = useRef(contentState);\n  const consoleErrorRef = useRef([]);\n\n  useEffect(() => {\n    console.error = (error) => {\n      consoleErrorRef.current.push(error);\n    };\n  }, []);\n\n  useEffect(() => {\n    contentStateRef.current = contentState;\n  }, [contentState]);\n\n  // Returns a context-specific \"not available\" label for disabled buttons\n  const getNotAvailableLabel = () => {\n    if (contentState.fallback && contentState.noffmpeg && contentState.editLimit === 0) {\n      return chrome.i18n.getMessage(\"notAvailableLongRecording\");\n    }\n    if (contentState.fallback && contentState.noffmpeg) {\n      return chrome.i18n.getMessage(\"notAvailableRecoveryMode\");\n    }\n    return chrome.i18n.getMessage(\"notAvailableLabel\");\n  };\n\n  const getPreparingLabel = () => {\n    const base = chrome.i18n.getMessage(\"preparingLabel\");\n    const pct = Math.round(contentState.processingProgress || 0);\n    if (!contentState.mp4ready && pct > 0) {\n      return `${base} (${pct}%)`;\n    }\n    return base;\n  };\n\n  const saveToDrive = () => {\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      saveDrive: true,\n    }));\n\n    const handleDriveResponse = (response) => {\n      if (!response || response.status === \"ew\" || response.error) {\n        console.error(\"[Drive] drive_save_failed:\", response?.error || \"unknown error\");\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          saveDrive: false,\n        }));\n      }\n      // On success, saveDrive is reset by the \"saved-to-drive\" message from background\n    };\n\n    const handleDriveError = (err) => {\n      console.error(\"[Drive] drive_save_error:\", err);\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        saveDrive: false,\n      }));\n    };\n\n    if (contentState.noffmpeg || !contentState.mp4ready || !contentState.blob) {\n      // Prefer the duration-fixed webm blob over rebuilding from raw chunks\n      const fixedWebm = contentState.webm;\n      if (fixedWebm && fixedWebm instanceof Blob && fixedWebm.size > 0) {\n        const reader = new FileReader();\n        reader.onload = () => {\n          const dataUrl = reader.result;\n          const base64 = dataUrl.split(\",\")[1];\n          chrome.runtime\n            .sendMessage({\n              type: \"save-to-drive\",\n              base64: base64,\n              title: contentState.title,\n              isWebm: true,\n            })\n            .then(handleDriveResponse)\n            .catch(handleDriveError);\n        };\n        reader.onerror = () => {\n          chrome.runtime\n            .sendMessage({\n              type: \"save-to-drive-fallback\",\n              title: contentState.title,\n            })\n            .then(handleDriveResponse)\n            .catch(handleDriveError);\n        };\n        reader.readAsDataURL(fixedWebm);\n      } else {\n        chrome.runtime\n          .sendMessage({\n            type: \"save-to-drive-fallback\",\n            title: contentState.title,\n          })\n          .then(handleDriveResponse)\n          .catch(handleDriveError);\n      }\n    } else {\n      // Blob to base64\n      const reader = new FileReader();\n      reader.onload = () => {\n        const dataUrl = reader.result;\n        const base64 = dataUrl.split(\",\")[1];\n\n        chrome.runtime\n          .sendMessage({\n            type: \"save-to-drive\",\n            base64: base64,\n            title: contentState.title,\n          })\n          .then(handleDriveResponse)\n          .catch(handleDriveError);\n      };\n      reader.onerror = () => {\n        console.error(\"[Drive] FileReader failed to read blob for Drive upload\");\n        setContentState((prevContentState) => ({\n          ...prevContentState,\n          saveDrive: false,\n        }));\n      };\n      if (\n        !contentState.noffmpeg &&\n        contentState.mp4ready &&\n        contentState.blob\n      ) {\n        reader.readAsDataURL(contentState.blob);\n      } else {\n        reader.readAsDataURL(contentState.webm);\n      }\n    }\n  };\n\n  const signOutDrive = () => {\n    chrome.runtime.sendMessage({ type: \"sign-out-drive\" });\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      driveEnabled: false,\n    }));\n  };\n\n  const handleEdit = () => {\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n    if (!contentState.mp4ready) return;\n\n    contentState.createBackup();\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      mode: \"edit\",\n      dragInteracted: false,\n    }));\n\n    if (!contentState.hasBeenEdited) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        hasBeenEdited: true,\n      }));\n    }\n  };\n\n  const handleCrop = () => {\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n\n    if (!contentState.mp4ready) return;\n\n    contentState.createBackup();\n\n    if (!contentState.frame && !contentState.isFfmpegRunning) {\n      contentState.getFrame();\n    }\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      mode: \"crop\",\n    }));\n\n    if (!contentState.hasBeenEdited) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        hasBeenEdited: true,\n      }));\n    }\n  };\n\n  const handleAddAudio = async () => {\n    if (\n      contentState.duration > contentState.editLimit &&\n      !contentState.override\n    )\n      return;\n    if (!contentState.mp4ready) return;\n\n    contentState.createBackup();\n\n    setContentState((prevContentState) => ({\n      ...prevContentState,\n      mode: \"audio\",\n    }));\n\n    if (!contentState.hasBeenEdited) {\n      setContentState((prevContentState) => ({\n        ...prevContentState,\n        hasBeenEdited: true,\n      }));\n    }\n  };\n\n  // Download best available blob: edited MP4 → fixed WebM → raw WebM.\n  const handleDownloadOriginal = () => {\n    const s = contentStateRef.current;\n    const source = s.blob || s.webm || s.rawBlob;\n    if (!source) return;\n    const blob =\n      source instanceof Blob\n        ? source\n        : new Blob([source], { type: \"video/webm\" });\n    const ext = blob.type.includes(\"mp4\") ? \"mp4\" : \"webm\";\n    const rawTitle = s.title || \"screenity-recording\";\n    const safe = rawTitle\n      .replace(/[\\\\:*?\"<>|]/g, \" \")\n      .replace(/[\\u0000-\\u001F\\u007F]/g, \" \")\n      .replace(/\\s+/g, \" \")\n      .trim() || \"screenity-recording\";\n    const url = window.URL.createObjectURL(blob);\n    chrome.downloads.download({ url, filename: `${safe}.${ext}` }, () => {\n      window.URL.revokeObjectURL(url);\n    });\n  };\n\n  const handleRawRecording = () => {\n    if (typeof contentStateRef.current.openModal === \"function\") {\n      contentStateRef.current.openModal(\n        chrome.i18n.getMessage(\"rawRecordingModalTitle\"),\n        chrome.i18n.getMessage(\"rawRecordingModalDescription\"),\n        chrome.i18n.getMessage(\"rawRecordingModalButton\"),\n        chrome.i18n.getMessage(\"sandboxEditorCancelButton\"),\n        () => {\n          const s = contentStateRef.current;\n          const blob = s.rawBlob;\n          if (!blob) {\n            console.error(\"[Screenity][WebM] raw download: no rawBlob available\");\n            return;\n          }\n          const url = window.URL.createObjectURL(blob);\n\n          const ext = blob.type.includes(\"mp4\") ? \"mp4\" : \"webm\";\n\n          chrome.downloads.download(\n            {\n              url: url,\n              filename: `raw-recording.${ext}`,\n            },\n            () => {\n              window.URL.revokeObjectURL(url);\n            }\n          );\n        },\n        () => {}\n      );\n    }\n  };\n\n  const handleTroubleshooting = () => {\n    if (typeof contentStateRef.current.openModal === \"function\") {\n      contentStateRef.current.openModal(\n        chrome.i18n.getMessage(\"troubleshootModalTitle\"),\n        chrome.i18n.getMessage(\"troubleshootModalDescription\"),\n        chrome.i18n.getMessage(\"troubleshootModalButton\"),\n        chrome.i18n.getMessage(\"sandboxEditorCancelButton\"),\n        async () => {\n          try {\n            const cs = contentStateRef.current;\n            const { blob, filename } = await buildDiagnosticZip({\n              source: \"sandbox-editor\",\n              extraConfig: {\n                editorMode: cs.mode || null,\n                duration: cs.duration || null,\n                width: cs.width || null,\n                height: cs.height || null,\n                hasBlobReady: Boolean(cs.blob || cs.rawBlob),\n                mp4ready: Boolean(cs.mp4ready),\n                ffmpegLoaded: Boolean(cs.ffmpegLoaded),\n                fallback: Boolean(cs.fallback),\n                offline: Boolean(cs.offline),\n                noffmpeg: Boolean(cs.noffmpeg),\n                updateChrome: Boolean(cs.updateChrome),\n                hasBeenEdited: Boolean(cs.hasBeenEdited),\n                editLimit: cs.editLimit || null,\n              },\n            });\n            const url = window.URL.createObjectURL(blob);\n            chrome.downloads.download(\n              { url, filename },\n              () => {\n                window.URL.revokeObjectURL(url);\n              },\n            );\n          } catch (err) {\n            console.error(\"[Screenity] Troubleshooting export failed:\", err);\n          }\n        },\n        () => {},\n      );\n    }\n  };\n\n  return (\n    <div className={styles.panel}>\n      {contentState.mode === \"audio\" && <AudioUI />}\n      {contentState.mode === \"crop\" && <CropUI />}\n      {contentState.mode === \"player\" && (\n        <div>\n          {!contentState.fallback && contentState.offline && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/no-internet.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"offlineLabelTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"offlineLabelDescription\")}\n                </div>\n              </div>\n              <div className={styles.buttonRight}>\n                {chrome.i18n.getMessage(\"offlineLabelTryAgain\")}\n              </div>\n            </div>\n          )}\n          {contentState.fallback && contentState.noffmpeg && contentState.editLimit === 0 && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"longRecordingTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"longRecordingDescription\")}\n                </div>\n              </div>\n              <div\n                className={styles.buttonRight}\n                onClick={handleDownloadOriginal}\n              >\n                {chrome.i18n.getMessage(\"rawRecordingModalButton\")}\n              </div>\n            </div>\n          )}\n          {contentState.fallback && contentState.noffmpeg && contentState.editLimit !== 0 && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"recoveryModeTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"recoveryModeDescription\")}\n                </div>\n              </div>\n              <div\n                className={styles.buttonRight}\n                onClick={handleDownloadOriginal}\n              >\n                {chrome.i18n.getMessage(\"rawRecordingModalButton\")}\n              </div>\n            </div>\n          )}\n          {!contentState.fallback &&\n            contentState.updateChrome &&\n            !contentState.offline &&\n            contentState.duration <= contentState.editLimit && (\n              <div className={styles.alert}>\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"updateChromeLabelTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {chrome.i18n.getMessage(\"updateChromeLabelDescription\")}\n                  </div>\n                </div>\n                <div\n                  className={styles.buttonRight}\n                  onClick={() => {\n                    chrome.runtime.sendMessage({ type: \"chrome-update-info\" });\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"learnMoreLabel\")}\n                </div>\n              </div>\n            )}\n          {!contentState.fallback &&\n            contentState.duration > contentState.editLimit &&\n            !contentState.override &&\n            !contentState.offline &&\n            !contentState.updateChrome && (\n              <div className={styles.alert}>\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"overLimitLabelTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {chrome.i18n.getMessage(\"overLimitLabelDescription\")}\n                  </div>\n                </div>\n                <div\n                  className={styles.buttonRight}\n                  onClick={() => {\n                    //chrome.runtime.sendMessage({ type: \"upgrade-info\" });\n                    if (typeof contentState.openModal === \"function\") {\n                      contentState.openModal(\n                        chrome.i18n.getMessage(\"overLimitModalTitle\"),\n                        chrome.i18n.getMessage(\"overLimitModalDescription\"),\n                        chrome.i18n.getMessage(\"overLimitModalButton\"),\n                        chrome.i18n.getMessage(\"sandboxEditorCancelButton\"),\n                        () => {\n                          setContentState((prevContentState) => ({\n                            ...prevContentState,\n                            saved: true,\n                          }));\n                          chrome.runtime.sendMessage({\n                            type: \"force-processing\",\n                          });\n                        },\n                        () => {},\n                        null,\n                        chrome.i18n.getMessage(\"overLimitModalLearnMore\"),\n                        () => {\n                          chrome.runtime.sendMessage({ type: \"upgrade-info\" });\n                        }\n                      );\n                    }\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"learnMoreLabel\")}\n                </div>\n              </div>\n            )}\n          {(!contentState.mp4ready || contentState.isFfmpegRunning) &&\n            (contentState.duration <= contentState.editLimit ||\n              contentState.override) &&\n            !contentState.offline &&\n            !contentState.updateChrome &&\n            !contentState.noffmpeg && (\n              <div className={styles.alert}>\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"videoProcessingLabelTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.isFfmpegRunning\n                      ? chrome.i18n.getMessage(\"editProcessingSafeDescription\")\n                      : chrome.i18n.getMessage(\"videoProcessingLabelDescription\")}\n                  </div>\n                </div>\n                {!contentState.isFfmpegRunning && (\n                <div\n                  className={styles.buttonRight}\n                  onClick={() => {\n                    chrome.runtime.sendMessage({\n                      type: \"pricing\",\n                    });\n                  }}\n                >\n                  {chrome.i18n.getMessage(\"learnMoreLabel\")}\n                </div>\n                )}\n              </div>\n            )}\n\n          {!contentState.fallback && contentState.editErrorType === \"too-long\" && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"editTooLongTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"editTooLongDescription\")}\n                </div>\n              </div>\n              <div\n                className={styles.buttonRight}\n                onClick={() =>\n                  setContentState((prev) => ({ ...prev, editErrorType: null }))\n                }\n              >\n                {chrome.i18n.getMessage(\"permissionsModalDismiss\")}\n              </div>\n            </div>\n          )}\n          {!contentState.fallback && contentState.editErrorType === \"timeout\" && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"editTimeoutTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"editTimeoutDescription\")}\n                </div>\n              </div>\n              <div\n                className={styles.buttonRight}\n                onClick={() =>\n                  setContentState((prev) => ({ ...prev, editErrorType: null }))\n                }\n              >\n                {chrome.i18n.getMessage(\"permissionsModalDismiss\")}\n              </div>\n            </div>\n          )}\n          {!contentState.fallback && contentState.editErrorType === \"failed\" && (\n            <div className={styles.alert}>\n              <div className={styles.buttonLeft}>\n                <ReactSVG src={URL + \"editor/icons/alert.svg\"} />\n              </div>\n              <div className={styles.buttonMiddle}>\n                <div className={styles.buttonTitle}>\n                  {chrome.i18n.getMessage(\"editFailedTitle\")}\n                </div>\n                <div className={styles.buttonDescription}>\n                  {chrome.i18n.getMessage(\"editFailedDescription\")}\n                </div>\n              </div>\n              <div\n                className={styles.buttonRight}\n                onClick={() =>\n                  setContentState((prev) => ({ ...prev, editErrorType: null }))\n                }\n              >\n                {chrome.i18n.getMessage(\"permissionsModalDismiss\")}\n              </div>\n            </div>\n          )}\n          <div className={styles.section}>\n            <div className={styles.sectionTitle}>\n              {chrome.i18n.getMessage(\"sandboxEditTitle\")}\n            </div>\n            <div className={styles.buttonWrap}>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={handleEdit}\n                disabled={\n                  (contentState.duration > contentState.editLimit &&\n                    !contentState.override) ||\n                  !contentState.mp4ready ||\n                  contentState.noffmpeg\n                }\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/trim.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"editButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline && !contentState.ffmpegLoaded\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome ||\n                        contentState.noffmpeg ||\n                        (contentState.duration > contentState.editLimit &&\n                          !contentState.override)\n                      ? getNotAvailableLabel()\n                      : contentState.mp4ready\n                      ? chrome.i18n.getMessage(\"editButtonDescription\")\n                      : getPreparingLabel()}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={handleCrop}\n                disabled={\n                  (contentState.duration > contentState.editLimit &&\n                    !contentState.override) ||\n                  !contentState.mp4ready ||\n                  contentState.noffmpeg\n                }\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/crop.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"cropButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline && !contentState.ffmpegLoaded\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome ||\n                        contentState.noffmpeg ||\n                        (contentState.duration > contentState.editLimit &&\n                          !contentState.override)\n                      ? getNotAvailableLabel()\n                      : contentState.mp4ready\n                      ? chrome.i18n.getMessage(\"cropButtonDescription\")\n                      : getPreparingLabel()}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={handleAddAudio}\n                disabled={\n                  (contentState.duration > contentState.editLimit &&\n                    !contentState.override) ||\n                  !contentState.mp4ready ||\n                  contentState.noffmpeg\n                }\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/audio.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"addAudioButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline && !contentState.ffmpegLoaded\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome ||\n                        contentState.noffmpeg ||\n                        (contentState.duration > contentState.editLimit &&\n                          !contentState.override)\n                      ? getNotAvailableLabel()\n                      : contentState.mp4ready\n                      ? chrome.i18n.getMessage(\"addAudioButtonDescription\")\n                      : getPreparingLabel()}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n            </div>\n          </div>\n          <div className={styles.section}>\n            <div className={styles.sectionTitle}>\n              {chrome.i18n.getMessage(\"sandboxSaveTitle\")}\n            </div>\n            {contentState.driveEnabled && (\n              <div\n                className={styles.buttonLogout}\n                onClick={() => {\n                  signOutDrive();\n                }}\n              >\n                {chrome.i18n.getMessage(\"signOutDriveLabel\")}\n              </div>\n            )}\n            <div className={styles.buttonWrap}>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={saveToDrive}\n                disabled={contentState.saveDrive}\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/drive.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {contentState.saveDrive\n                      ? chrome.i18n.getMessage(\"savingDriveLabel\")\n                      : contentState.driveEnabled\n                      ? chrome.i18n.getMessage(\"saveDriveButtonTitle\")\n                      : chrome.i18n.getMessage(\"signInDriveLabel\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome\n                      ? chrome.i18n.getMessage(\"notAvailableLabel\")\n                      : chrome.i18n.getMessage(\"saveDriveButtonDescription\")}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n            </div>\n          </div>\n          <div className={styles.section}>\n            <div className={styles.sectionTitle}>\n              {chrome.i18n.getMessage(\"sandboxExportTitle\")}\n            </div>\n            <div className={styles.buttonWrap}>\n              {contentState.fallback && (\n                <div\n                  role=\"button\"\n                  className={styles.button}\n                  onClick={() => contentState.downloadWEBM()}\n                  disabled={contentState.isFfmpegRunning}\n                >\n                  <div className={styles.buttonLeft}>\n                    <ReactSVG src={URL + \"editor/icons/download.svg\"} />\n                  </div>\n                  <div className={styles.buttonMiddle}>\n                    <div className={styles.buttonTitle}>\n                      {contentState.downloadingWEBM\n                        ? chrome.i18n.getMessage(\"downloadingLabel\")\n                        : chrome.i18n.getMessage(\"downloadWEBMButtonTitle\")}\n                    </div>\n                    <div className={styles.buttonDescription}>\n                      {chrome.i18n.getMessage(\"downloadWEBMButtonDescription\")}\n                    </div>\n                  </div>\n                  <div className={styles.buttonRight}>\n                    <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                  </div>\n                </div>\n              )}\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={() => {\n                  if (!contentState.mp4ready) return;\n                  contentState.download();\n                }}\n                disabled={\n                  contentState.isFfmpegRunning ||\n                  contentState.noffmpeg ||\n                  !contentState.mp4ready ||\n                  contentState.noffmpeg\n                }\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/download.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {contentState.downloading\n                      ? chrome.i18n.getMessage(\"downloadingLabel\")\n                      : chrome.i18n.getMessage(\"downloadMP4ButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline && !contentState.ffmpegLoaded\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome ||\n                        contentState.noffmpeg ||\n                        (contentState.duration > contentState.editLimit &&\n                          !contentState.override)\n                      ? getNotAvailableLabel()\n                      : contentState.mp4ready && !contentState.isFfmpegRunning\n                      ? chrome.i18n.getMessage(\"downloadMP4ButtonDescription\")\n                      : getPreparingLabel()}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n              {!contentState.fallback && (\n                <div\n                  role=\"button\"\n                  className={styles.button}\n                  onClick={() => contentState.downloadWEBM()}\n                  disabled={contentState.isFfmpegRunning}\n                >\n                  <div className={styles.buttonLeft}>\n                    <ReactSVG src={URL + \"editor/icons/download.svg\"} />\n                  </div>\n                  <div className={styles.buttonMiddle}>\n                    <div className={styles.buttonTitle}>\n                      {contentState.downloadingWEBM\n                        ? chrome.i18n.getMessage(\"downloadingLabel\")\n                        : chrome.i18n.getMessage(\"downloadWEBMButtonTitle\")}\n                    </div>\n                    <div className={styles.buttonDescription}>\n                      {!contentState.isFfmpegRunning\n                        ? chrome.i18n.getMessage(\n                            \"downloadWEBMButtonDescription\"\n                          )\n                        : getPreparingLabel()}\n                    </div>\n                  </div>\n                  <div className={styles.buttonRight}>\n                    <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                  </div>\n                </div>\n              )}\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={() => {\n                  if (!contentState.mp4ready) return;\n                  contentState.downloadGIF();\n                }}\n                disabled={\n                  contentState.isFfmpegRunning ||\n                  contentState.duration > 30 ||\n                  !contentState.mp4ready ||\n                  contentState.noffmpeg\n                }\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/gif.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {contentState.downloadingGIF\n                      ? chrome.i18n.getMessage(\"downloadingLabel\")\n                      : chrome.i18n.getMessage(\"downloadGIFButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {contentState.offline && !contentState.ffmpegLoaded\n                      ? chrome.i18n.getMessage(\"noConnectionLabel\")\n                      : contentState.updateChrome ||\n                        contentState.noffmpeg ||\n                        (contentState.duration > contentState.editLimit &&\n                          !contentState.override)\n                      ? getNotAvailableLabel()\n                      : contentState.mp4ready\n                      ? chrome.i18n.getMessage(\"downloadGIFButtonDescription\")\n                      : getPreparingLabel()}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n            </div>\n          </div>\n          <div className={styles.section}>\n            {/* Create an advanced section with a button to send logs and to download raw video file as a backup */}\n            <div className={styles.sectionTitle}>\n              {chrome.i18n.getMessage(\"sandboxAdvancedTitle\")}\n            </div>\n            <div className={styles.buttonWrap}>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={() => {\n                  handleRawRecording();\n                }}\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/download.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"rawRecordingButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {chrome.i18n.getMessage(\"rawRecordingButtonDescription\")}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n              <div\n                role=\"button\"\n                className={styles.button}\n                onClick={() => {\n                  handleTroubleshooting();\n                }}\n              >\n                <div className={styles.buttonLeft}>\n                  <ReactSVG src={URL + \"editor/icons/flag.svg\"} />\n                </div>\n                <div className={styles.buttonMiddle}>\n                  <div className={styles.buttonTitle}>\n                    {chrome.i18n.getMessage(\"troubleshootButtonTitle\")}\n                  </div>\n                  <div className={styles.buttonDescription}>\n                    {chrome.i18n.getMessage(\"troubleshootButtonDescription\")}\n                  </div>\n                </div>\n                <div className={styles.buttonRight}>\n                  <ReactSVG src={URL + \"editor/icons/right-arrow.svg\"} />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default RightPanel;\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_Dropdown.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.SelectTrigger {\n  display: inline-flex;\n  align-items: center;\n  justify-content: space-between;\n  border-radius: $container-border-radius;\n  line-height: 1;\n  height: 44px;\n  gap: 5px;\n  background-color: $color-background;\n  color: $color-text-primary;\n  width: 100%;\n  box-sizing: border-box;\n  margin-top: $spacing-02;\n  margin-bottom: $spacing-02;\n  border: 1px solid $color-border;\n  text-align: left !important;\n  font-family: $font-medium !important;\n  text-indent: 18px;\n}\n.SelectTrigger:hover {\n  box-shadow: $container-shadow-focus;\n  cursor: pointer;\n}\n.SelectTrigger:focus {\n  box-shadow: $focus-border !important;\n}\n.SelectTrigger[data-placeholder] {\n  color: var(--violet9);\n}\n.SelectTrigger[data-state=\"open\"] {\n  box-shadow: $focus-border;\n}\n\n.SelectValue {\n  text-align: left;\n  flex: 1;\n  display: block;\n  width: 100%;\n  height: 100%;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  box-sizing: border-box;\n  text-indent: 18px !important;\n\n  span {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    height: 100%;\n    line-height: 44px;\n    text-indent: 18px !important;\n  }\n}\n\n.SelectIconDrop,\n.SelectIconType {\n  text-align: center;\n}\n.SelectIconType {\n  padding-left: $spacing-05;\n  padding-right: $spacing-02;\n}\n.SelectIconDrop {\n  padding-right: $spacing-05;\n}\n.SelectTrigger[data-state=\"open\"] .SelectIconDrop img {\n  transform: rotate(180deg);\n}\n.Portal {\n  position: absolute;\n}\n.SelectContent {\n  position: absolute;\n  overflow: hidden;\n  z-index: $z-index-max;\n  width: var(--radix-select-trigger-width);\n  max-height: var(--radix-select-content-available-height);\n  font-family: $font-medium;\n  background-color: white;\n  border-radius: 15px;\n  margin-top: $spacing-02;\n  box-shadow: $container-shadow-focus;\n}\n.SelectItem {\n  font-size: $font-size-normal;\n  line-height: 1;\n  color: var(--violet11);\n  display: flex;\n  align-items: center;\n  height: 44px;\n  padding-left: $spacing-05;\n  padding-right: $spacing-05;\n  position: relative;\n  user-select: none;\n}\n.SelectItem[data-disabled] {\n  color: $color-text-secondary;\n  pointer-events: none;\n}\n.SelectItem[data-highlighted] {\n  background: $color-light-grey;\n  outline: none !important;\n}\n.SelectItem:hover {\n  background: $color-light-grey;\n  cursor: pointer;\n}\n\n.SelectSeparator {\n  height: 1px;\n  background-color: $color-border;\n  width: calc(100% - $spacing-04 * 2);\n  margin: auto;\n  border-radius: $container-border-radius;\n  margin-top: $spacing-02;\n  margin-bottom: $spacing-02;\n}\n\n.SelectItemIndicator {\n  position: absolute;\n  right: $spacing-04;\n  width: 24px;\n  height: 24px;\n  background: $color-primary;\n  border-radius: 50%;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.SelectScrollButton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 25px;\n  background-color: white;\n  color: var(--violet11);\n  cursor: default;\n}\n\n.SelectOff {\n  background: $color-red-light;\n  color: $color-red;\n  padding-left: $spacing-04;\n  padding-right: $spacing-04;\n  padding-top: $spacing-03;\n  padding-bottom: $spacing-03;\n  margin-right: $spacing-02;\n  border-radius: $container-border-radius;\n  font-size: $font-size-small;\n  font-weight: $font-weight-bold;\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_EditorNav.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.editorNav {\n  width: 100%;\n  margin: auto;\n  height: 80px;\n  left: 0px;\n  top: 0px;\n  position: fixed;\n  z-index: 999999999999;\n  background-color: #fff;\n  border-bottom: 1px solid $color-border;\n}\n.navWrap {\n  margin: auto;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  height: 100%;\n  width: 95%;\n}\n.editorNavCenter {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  width: fit-content;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.editorNavTitle {\n  font-size: $font-size-normal;\n  color: $color-text-primary;\n  font-weight: 600;\n  text-align: center;\n  margin: auto;\n\n  span {\n    margin-left: 4px;\n    font-weight: 400;\n    font-size: $font-size-small;\n    color: $color-text-contrast;\n    background-color: $color-primary;\n    border-radius: $container-border-radius;\n    padding: $spacing-02 $spacing-03;\n  }\n}\n.editorNavRight {\n  display: flex;\n  height: fit-content;\n  align-items: center;\n  gap: 8px;\n}\n.editorNavLeft {\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  img {\n    height: 30px;\n  }\n}\n\n// max 1000px\n@media (max-width: 1000px) {\n  .editorNavCenter {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_Switch.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.SwitchRow {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  height: 40px;\n}\n\n.SwitchRoot {\n  width: 34px;\n  height: 22px;\n  background-color: $color-border;\n  border-radius: 9999px;\n  position: relative;\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n  border: none !important;\n}\n.SwitchRoot[disabled] {\n  opacity: 0.5;\n  cursor: not-allowed !important;\n}\n.SwitchRoot:focus {\n  box-shadow: $focus-border;\n}\n.SwitchRoot[data-state=\"checked\"] {\n  background-color: $color-primary;\n}\n.SwitchRoot:hover {\n  cursor: pointer;\n}\n\n.SwitchThumb {\n  display: block;\n  width: 14px;\n  height: 14px;\n  background-color: white;\n  border-radius: 9999px;\n  box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.1);\n  transition: transform 100ms;\n  transform: translateX(4px);\n  will-change: transform;\n}\n.SwitchThumb[data-state=\"checked\"] {\n  transform: translateX(16px);\n}\n\n.Label {\n  color: $color-text-secondary;\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_TrimUI.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.trimWrap {\n  position: absolute;\n  bottom: 0px;\n  left: 0px;\n  width: 100%;\n  height: 230px;\n  border-top: 1px solid $color-border;\n  background-color: #fff;\n}\n.controls {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  width: 90%;\n  margin: auto;\n  margin-top: 12px;\n}\n.actions {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.controlsRight {\n  display: flex;\n  align-items: center;\n}\n.timeWrap {\n  color: $color-text-primary;\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  width: fit-content;\n\n  span {\n    position: relative;\n    font-variant-numeric: tabular-nums;\n  }\n  svg {\n    color: $color-icon;\n    margin-top: 5px;\n  }\n}\n.trimInfo {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  background-color: $color-light-grey;\n  border-radius: $container-border-radius;\n  padding: 8px 16px;\n  width: fit-content;\n  margin-left: auto;\n  margin-right: auto;\n  margin-top: 8px;\n  margin-bottom: 8px;\n  justify-content: center;\n  align-items: center;\n  color: $color-text-primary;\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  bottom: 8px;\n\n  svg {\n    color: $color-icon;\n    margin-top: 5px;\n    margin-right: 5px;\n  }\n  .trimInfoLink {\n    color: $color-primary;\n    cursor: pointer;\n    display: inline-block;\n    margin-left: 5px;\n  }\n}\n\n@media (max-width: 1000px) {\n  .timeWrap {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_Trimmer.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n.trimmerContainer {\n  position: absolute;\n  bottom: 75px;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  width: 90%;\n  height: 80px;\n}\n.thumbstrip {\n  width: 100%;\n  display: flex;\n  overflow: hidden;\n  position: relative;\n}\n.trimSection {\n  pointer-events: none;\n}\n.trimmer {\n  position: absolute; /* Position the trimmer over the thumbstrip */\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n}\n.trimWrap {\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  z-index: 999999;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n}\n.handle {\n  position: absolute;\n  width: 12px;\n  height: 30px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  background-color: $color-primary;\n  cursor: pointer;\n  pointer-events: all !important;\n\n  &::after {\n    content: \"\";\n    width: 2px;\n    height: 12px;\n    border-radius: 30px;\n    background-color: #fff;\n    position: absolute;\n    top: 0px;\n    left: 0px;\n    bottom: 0px;\n    right: 0px;\n    margin: auto;\n  }\n\n  &:hover {\n    cursor: ew-resize;\n    background-color: #2969d8 !important;\n  }\n}\n\n.startHandle {\n  border-radius: 5px 0px 0px 5px;\n  transform: translateX(-10px);\n}\n\n.endHandle {\n  border-radius: 0px 5px 5px 0px;\n  transform: translateX(2px);\n}\n\n.overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(255, 255, 255, 0.5);\n  pointer-events: none !important;\n}\n\n.leftOverlay {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.05);\n  border-radius: 5px 0px 0px 5px;\n}\n\n.rightOverlay {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: auto;\n  background: rgba(0, 0, 0, 0.05);\n  border-radius: 0px 5px 5px 0px;\n}\n\n.trimSection {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  border: 2px solid $color-primary;\n  border-radius: 5px;\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_VideoPlayer.scss",
    "content": "@use \"../global/variables.scss\" as *;\n:root {\n  --plyr-color-main: #3080f8;\n  --plyr-font-family: \"Satoshi-Medium\", sans-serif;\n}\n\n.videoPlayer {\n  position: absolute;\n  top: 0px;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  height: 100vh;\n}\n.playerWrap {\n  height: calc(100% - 300px);\n  position: absolute;\n  top: 80px;\n  left: 0px;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  gap: 32px;\n}\n.plyr {\n  max-width: 900px !important;\n  left: 0px !important;\n  right: 0px !important;\n  margin: 0px !important;\n  top: 0px !important;\n  bottom: 0px !important;\n  position: relative !important;\n  border-radius: 15px !important;\n  width: 90% !important;\n}\n/* Customize progress bar color */\n.plyr__progress--played {\n  background-color: #ff5733 !important; /* Your custom color */\n}\n\n// Max width 900px\n@media (max-width: 900px) {\n  .videoPlayer {\n    height: 100% !important;\n  }\n  .playerWrap {\n    position: relative !important;\n    height: 100% !important;\n    padding-top: 40px !important;\n    padding-bottom: 60px !important;\n    top: 0px !important;\n    position: relative !important;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/edit/_Waveform.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.waveform {\n  height: 100%;\n}\n.waveform ::part(cursor) {\n  display: none;\n}\n.cursor {\n  height: 100%;\n  background-color: $color-primary;\n  z-index: 999999;\n  width: 3px;\n  position: absolute;\n  border-radius: 30px;\n  padding-top: 20px;\n  transform: translateY(-10px);\n}\n.ghostCursor {\n  height: 100%;\n  padding-top: 20px;\n  transform: translateY(-10px);\n  background-color: #8aafee;\n  z-index: 999999;\n  width: 3px;\n  position: absolute;\n  border-radius: 30px;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/global/_app.scss",
    "content": "@use \"./variables.scss\" as *;\n@use \"sass:color\";\n\nbody {\n  background: $color-light-grey;\n  font-family: $font-medium;\n  font-size: $font-size-normal;\n  height: 100vh;\n  overflow-y: hidden;\n  margin: 0px;\n}\n\n// max width 900px\n@media (max-width: 900px) {\n  body {\n    overflow-y: unset !important;\n  }\n}\n\n/* Button component */\n.button {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 4px;\n  border: none;\n  padding: 8px 16px;\n  gap: 6px;\n  text-align: center;\n  box-sizing: border-box;\n  border-radius: $container-border-radius;\n  font-family: $font-bold;\n  font-weight: 500;\n\n  &.primaryButton:disabled,\n  &.secondaryButton:disabled {\n    background-color: $color-light-grey !important;\n    color: $color-icon !important;\n    cursor: not-allowed !important;\n    border: 1px solid transparent !important;\n\n    svg {\n      color: $color-icon !important;\n      margin-top: 5px;\n    }\n  }\n\n  &.simpleButton:disabled {\n    opacity: 0.5;\n    cursor: not-allowed !important;\n  }\n\n  &.primaryButton {\n    color: $color-text-contrast;\n    background-color: $color-primary;\n    height: 40px;\n\n    svg {\n      margin-top: 5px;\n      color: $color-text-contrast;\n    }\n    &:hover {\n      background-color: color.adjust($color-primary, $lightness: -10%);\n      cursor: pointer;\n    }\n  }\n\n  &.secondaryButton {\n    color: $color-text-primary;\n    background-color: #fff;\n    border: 1px solid $color-border;\n    height: 40px;\n    svg {\n      margin-top: 5px;\n      color: $color-icon;\n    }\n    &:hover {\n      background-color: $color-light-grey;\n      cursor: pointer;\n    }\n  }\n\n  &.simpleButton {\n    color: $color-text-secondary;\n    background: none;\n\n    svg {\n      margin-top: 5px;\n    }\n\n    &:hover {\n      background-color: $color-light-grey;\n      cursor: pointer;\n    }\n  }\n\n  &.blueButton {\n    color: $color-primary;\n  }\n\n  &.blackButton {\n    color: $color-text-primary !important;\n  }\n}\n\n/* Define variables for handle size, border width, and margin */\n$handle-size: 15px;\n$border-width: 4px;\n$margin-offset: 8px;\n\n/* Disable transitions for Cropper */\n.cropper,\n.advanced-cropper-artificial-transition,\n.advanced-cropper-background-image,\n.advanced-cropper-wrapper,\n.advanced-cropper,\n.CropperBackgroundWrapper,\n.cropper * {\n  transition: none !important;\n}\n\n.cropper {\n  max-width: 80%;\n  height: 80%;\n  border-radius: 15px;\n  margin: auto !important;\n  position: absolute !important;\n  top: 0px;\n  left: 0px;\n  right: 0px;\n  bottom: 0px !important;\n  display: block;\n  transition: none !important;\n  padding-left: 40px;\n  padding-right: 40px;\n}\n.advanced-cropper-artificial-transition {\n  transition: none !important;\n}\n\n.advanced-cropper-background-image {\n  transition: none !important;\n}\n\n.advanced-cropper-wrapper,\n.advanced-cropper,\n.CropperBackgroundWrapper {\n  /* tile background image svg */\n  background-image: url(\"chrome-extension://__MSG_@@extension_id__/assets/editor/transparenttile.png\") !important;\n  background-repeat: repeat !important;\n  background-size: 20px !important;\n  transition: none !important;\n}\n/* Custom styles for the handles */\n.advanced-cropper-simple-handler {\n  width: $handle-size !important; /* Adjust the handle width */\n  height: $handle-size !important; /* Adjust the handle height */\n  background-color: transparent !important;\n  position: absolute;\n  border-style: solid; /* Specify the border style as solid */\n  border-width: 0 !important; /* Initialize border width to 0 */\n  border-radius: 2px;\n\n  &--west-north,\n  &--west-south {\n    margin-left: $margin-offset !important; /* Offset from the left */\n  }\n\n  &--west-north,\n  &--east-north {\n    margin-top: $margin-offset !important; /* Offset from the top */\n  }\n\n  &--west-south,\n  &--east-south {\n    margin-bottom: $margin-offset !important; /* Offset from the bottom */\n  }\n\n  &--east {\n    margin-right: $margin-offset !important;\n    border-right-width: $border-width !important; /* Border on the right */\n  }\n\n  &--west {\n    margin-left: $margin-offset;\n    border-left-width: $border-width !important; /* Border on the left */\n  }\n\n  &--north {\n    margin-top: $margin-offset;\n    border-top-width: $border-width !important; /* Border on the top */\n  }\n\n  &--south {\n    margin-bottom: $margin-offset;\n    border-bottom-width: $border-width !important; /* Border on the bottom */\n  }\n}\n\n/* reset */\nbutton {\n  all: unset;\n}\n\n.AlertDialogOverlay {\n  background-color: rgba(0, 0, 0, 0.5);\n  position: fixed;\n  inset: 0;\n  animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: 99999999999;\n}\n\n.AlertDialogContent {\n  background-color: white;\n  border-radius: 30px;\n  box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px,\n    hsl(206 22% 7% / 20%) 0px 10px 20px -15px;\n  position: fixed;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  width: 90vw;\n  max-width: 500px;\n  max-height: 85vh;\n  padding: 35px 25px;\n  animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);\n  z-index: $z-index-max;\n}\n.AlertDialogContent:focus {\n  outline: none;\n}\n\n.AlertDialogTitle {\n  margin: 0;\n  color: $color-text-primary;\n  font-size: $font-size-normal;\n  font-family: $font-bold;\n  font-weight: 700;\n}\n\n.AlertDialogDescription {\n  margin-bottom: 20px;\n  color: $color-text-secondary;\n  font-size: $font-size-normal;\n  line-height: 1.5;\n\n  a {\n    color: $color-primary !important;\n    font-weight: 600 !important;\n    text-decoration: none !important;\n    display: inline-block;\n    cursor: pointer;\n  }\n}\n\n.Button {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n}\n.Button.blue {\n  background-color: rgba(48, 128, 248, 0.1);\n  color: $color-primary;\n\n  &:hover {\n    background-color: rgba(48, 128, 248, 0.15);\n    cursor: pointer;\n  }\n  &:focus {\n    box-shadow: $focus-border;\n  }\n}\n.Button.red {\n  background-color: rgba(247, 56, 90, 0.1);\n  color: rgba(247, 56, 90, 1);\n}\n.Button.red:hover {\n  background-color: rgba(247, 56, 90, 0.15);\n  cursor: pointer;\n}\n.Button.red:focus {\n  box-shadow: $focus-border;\n}\n.Button.grey {\n  background: rgba(110, 118, 132, 0.1);\n  color: $color-text-secondary;\n}\n.Button.grey:hover {\n  background: rgba(110, 118, 132, 0.15);\n  cursor: pointer;\n}\n.Button.grey:focus {\n  box-shadow: $focus-border;\n}\n\n@keyframes overlayShow {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes contentShow {\n  from {\n    opacity: 0;\n    transform: translate(-50%, -48%) scale(0.96);\n  }\n  to {\n    opacity: 1;\n    transform: translate(-50%, -50%) scale(1);\n  }\n}\n\n.SideButtonModal {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 30px;\n  padding: 0 15px;\n  font-size: 14px;\n  line-height: 1;\n  font-weight: 500;\n  height: 35px;\n  color: $color-text-secondary;\n  font-family: $font-medium;\n\n  &:hover {\n    cursor: pointer;\n    background: rgba(110, 118, 132, 0.05);\n  }\n}\n.pro-banner {\n  background-color: $color-background;\n  border-top-left-radius: 25px;\n  border-top-right-radius: 25px;\n  display: flex;\n  flex-direction: row;\n  justify-content: space-between;\n  align-items: center;\n  min-height: 110px;\n  max-height: 150px;\n  border: 1px solid $color-border;\n  border-bottom: 0 !important;\n  box-sizing: border-box;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  margin: auto;\n  max-width: 990px;\n  width: 80%;\n  font-size: 13px;\n  padding: 12px;\n  gap: 8px;\n}\n\n.pro-banner-content {\n  display: flex;\n  flex-direction: row;\n  gap: 12px;\n  align-items: center;\n  flex: 1;\n  overflow: hidden;\n  min-height: 0;\n}\n\n.pro-banner-left {\n  height: 100%;\n  width: 80px;\n  min-width: 80px;\n  flex-shrink: 0;\n  overflow: hidden;\n  aspect-ratio: 640 / 480;\n}\n\n.pro-banner-left video {\n  height: 100%;\n  width: 100%;\n  object-fit: cover;\n  border-radius: 6px;\n  display: block;\n}\n\n.pro-banner-right {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  gap: 4px;\n  overflow: hidden;\n  flex: 1;\n  min-width: 0;\n}\n\n.pro-banner-title {\n  font-family: $font-bold;\n  color: $color-text-primary;\n  font-size: 13px;\n  line-height: 1.2;\n  margin: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n}\n\n.pro-banner-description {\n  color: $color-text-secondary;\n  font-family: $font-medium;\n  font-size: 13px;\n  line-height: 1.3;\n  margin: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n}\n\n.pro-banner-cta {\n  display: flex;\n  gap: 8px;\n  justify-content: flex-end;\n  flex-shrink: 0;\n}\n\n.altButton {\n  background: transparent !important;\n  color: $color-text-primary;\n  font-family: $font-bold;\n  height: 37px !important;\n  white-space: nowrap !important;\n  padding: 0 12px;\n  border-radius: 30px;\n  border: 1px solid transparent;\n  font-size: 13px;\n\n  &:hover {\n    background: $color-light-grey !important;\n    cursor: pointer;\n  }\n}\n\n.primaryDarkButton {\n  background: $color-text-primary !important;\n  color: $color-text-contrast !important;\n  height: 37px !important;\n  white-space: nowrap !important;\n  padding: 0 12px;\n  border-radius: 30px;\n  border: none;\n  font-size: 13px;\n  font-family: $font-bold;\n\n  &:hover {\n    background: #000 !important;\n    cursor: pointer;\n  }\n}\n\n@media (max-width: 1400px) {\n  .pro-banner {\n    width: 100%;\n    border-radius: 0px !important;\n    min-height: 100px;\n    max-height: 140px;\n    border-right: none !important;\n  }\n\n  .pro-banner-title,\n  .pro-banner-description {\n    font-size: 12px;\n  }\n\n  .pro-banner-cta {\n    gap: 6px;\n  }\n\n  .altButton,\n  .primaryDarkButton {\n    height: 32px !important;\n    padding: 0 12px;\n    font-size: 12px;\n  }\n}\n\n@media (max-width: 1020px) {\n  .pro-banner {\n    flex-direction: column;\n    justify-content: flex-end;\n  }\n  .pro-banner-cta {\n    width: 100%;\n    justify-content: right;\n    text-align: right;\n  }\n}\n\n@media (max-width: 900px) {\n  .pro-banner {\n    position: fixed;\n    bottom: 0;\n    z-index: 99999999999;\n    flex-direction: row !important;\n    justify-content: center !important;\n  }\n  .pro-banner-cta {\n    width: auto !important;\n    justify-content: center;\n    text-align: center;\n  }\n}\n\n@media (max-width: 300px) {\n  .pro-banner {\n    flex-direction: column;\n    align-items: flex-start;\n    max-width: none;\n    width: 100%;\n    height: auto !important;\n    border-right: none;\n    border-top-left-radius: 12px;\n    border-top-right-radius: 12px;\n    padding: 12px;\n    gap: 10px;\n  }\n\n  .pro-banner-content {\n    flex-direction: column;\n    align-items: flex-start;\n    gap: 8px;\n    width: 100%;\n  }\n\n  .pro-banner-left {\n    width: 100%;\n    min-width: 0;\n    height: auto;\n    aspect-ratio: 16 / 9;\n    border-radius: 8px;\n  }\n\n  .pro-banner-left video {\n    width: 100%;\n    height: auto;\n    object-fit: cover;\n    border-radius: 8px;\n  }\n\n  .pro-banner-right {\n    width: 100%;\n    align-items: flex-start;\n  }\n\n  .pro-banner-title,\n  .pro-banner-description {\n    font-size: 12px;\n    -webkit-line-clamp: 2;\n  }\n\n  .pro-banner-cta {\n    width: 100%;\n    justify-content: flex-end;\n    gap: 6px;\n    flex-wrap: wrap;\n  }\n\n  .altButton,\n  .primaryDarkButton {\n    height: 32px !important;\n    padding: 0 12px;\n    font-size: 12px;\n    flex: 1;\n    min-width: 100px;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/global/_variables.scss",
    "content": "@mixin font($font-family, $font-file) {\n  @font-face {\n    font-family: $font-family;\n         src: url($font-file+'.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n  }\n}\n\n@include font('Satoshi-Light', 'chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light');\n@include font('Satoshi-Medium', 'chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium');\n@include font('Satoshi-Bold', 'chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold');\n\n/* Colors */\n$color-background: #FFF;\n$color-light-grey: #F6F7FB;\n$color-primary: #3080F8;\n$color-border: #E8E8E8;\n$color-text-contrast: #FFF;\n$color-text-primary: #29292F;\n$color-text-secondary: #6E7684;\n$color-red: #D2234D;\n$color-red-light: #FAF0F4; \n$color-primary-transparent: rgba(48, 128, 248, 0.1);\n$color-icon: #9797A4;\n\n/* Font */\n//$font-size-normal: 0.875rem;\n//$font-size-small: 0.75rem;\n$font-size-normal: 14px;\n$font-size-small: 12px;\n$font-weight-bold: 700;\n$font-weight-normal: 500;\n$font-weight-light: 400;\n$font-light: 'Satoshi-Light', sans-serif;\n$font-medium: 'Satoshi-Medium', sans-serif;\n$font-bold: 'Satoshi-Bold', sans-serif;\n\n/* Spacing */\n/*\n$spacing-01: 0.125rem;\n$spacing-02: 0.25rem;\n$spacing-03: 0.5rem;\n$spacing-04: 0.75rem;\n$spacing-05: 1rem;\n*/\n$spacing-01: 2px;\n$spacing-02: 4px; \n$spacing-03: 8px;\n$spacing-04: 12px; \n$spacing-05: 16px;\n\n/* Container */\n$container-border-radius: 30px;\n$container-border: 1px solid $color-border;\n$container-shadow: drop-shadow(0px 4px 100px rgba(0, 0, 0, 0.35));\n$container-shadow-focus: 2px 2px 10px rgba(0, 0, 0, 0.1);\n\n/* Gradients */\n$gradient-primary: radial-gradient(117.41% 117.78% at 35.44% 0%, #2BAEF8 23.13%, #3582F6 46.35%, #486DEF 74.48%, #7B9AEA 100%);\n$gradient-shadow-primary: drop-shadow(0px 4px 20px rgba(86, 123, 218, 0.5));\n\n/* Events */\n$focus-border: 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n$focus-inner-border: inset 0px 0px 0px 2px rgba(48, 128, 248, 0.5);\n\n/* Z-index */\n$z-index-max: 99999999999;"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_Content.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.content {\n  flex: 1;\n  height: 100%;\n  position: relative;\n  overflow: hidden;\n  min-height: 300px;\n}\n.wrap {\n  max-width: 1400px;\n  margin: 0 auto;\n  position: relative;\n  height: 100%;\n  min-height: 50vh;\n}\n\n@media (max-width: 900px) {\n  .content {\n    display: block !important;\n    height: auto !important;\n    overflow: unset !important;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_HelpButton.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.HelpButton {\n  position: absolute;\n  bottom: 20px;\n  right: 20px;\n  z-index: 99999;\n  background: $color-background;\n  border-radius: 50%;\n  border: 1px solid $color-border;\n  box-shadow: 0px 4px 30px 0px rgba(63, 67, 72, 0.15);\n  cursor: pointer;\n  pointer-events: all;\n  width: 48px;\n  height: 48px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n\n  &:hover {\n    border: $focus-border !important;\n  }\n\n  svg {\n    fill: $color-text-secondary;\n    color: $color-text-secondary;\n    margin-top: 3px;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_Nav.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.nav {\n  width: 100%;\n  margin: auto;\n  height: 80px;\n  left: 0px;\n  top: 0px;\n  position: fixed;\n  z-index: 99999999999999;\n  background-color: #fff;\n  border-bottom: 1px solid $color-border;\n}\n.navWrap {\n  margin: auto;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  height: 100%;\n  width: 95%;\n}\n.navCenter {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  margin: auto;\n  width: fit-content;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n.navTitle {\n  font-size: $font-size-normal;\n  color: $color-text-primary;\n  font-weight: 600;\n  text-align: center;\n  margin: auto;\n}\n.beta {\n  margin-left: 4px;\n  font-weight: 400;\n  font-size: $font-size-small;\n  color: $color-text-contrast;\n  background-color: $color-primary;\n  border-radius: $container-border-radius;\n  padding: $spacing-02 $spacing-03;\n}\n.navLeft {\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n\n  img {\n    height: 30px;\n  }\n}\n.navRight {\n  display: flex;\n  height: fit-content;\n  align-items: center;\n  gap: 8px;\n\n  .button {\n    padding: 8px 16px !important;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_Player.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.content {\n  display: flex;\n  position: absolute;\n  top: 80px;\n  width: 100%;\n  height: calc(100% - 80px);\n  box-sizing: border-box;\n  left: 0px;\n}\n.player {\n  position: absolute;\n}\n\n@media (max-width: 900px) {\n  .content {\n    display: block !important;\n    height: auto !important;\n  }\n  .player {\n    position: relative;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_RightPanel.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.panel {\n  height: 100%;\n  width: 460px;\n  background-color: #fff;\n  border: 1px solid $color-border;\n  box-sizing: border-box;\n  overflow-y: auto;\n  z-index: 9999999;\n}\n// Max width 900px\n@media (max-width: 900px) {\n  .panel {\n    width: 100% !important;\n    position: relative !important;\n    top: 0px;\n    left: 0px;\n    overflow: unset !important;\n  }\n}\n.section {\n  width: 90%;\n  margin: auto;\n  padding-top: 16px;\n  padding-bottom: 16px;\n  border-bottom: 1px solid $color-border;\n  position: relative;\n\n  &:last-child {\n    border-bottom: none;\n  }\n}\n.sectionTitle {\n  margin-top: 8px;\n  margin-bottom: 16px;\n  font-family: $font-bold;\n  color: $color-text-primary;\n}\n.buttonLogout {\n  position: absolute;\n  right: 8px;\n  top: 26px;\n  font-family: $font-medium;\n  color: $color-text-secondary;\n  cursor: pointer;\n}\n.buttonWrap {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n.alert {\n  display: flex;\n  width: 100%;\n  min-height: 80px;\n  padding: 12px 0;\n  justify-content: space-between;\n  align-items: center;\n  position: relative;\n  background-color: rgba(56, 126, 247, 0.1);\n  box-sizing: border-box;\n\n  .buttonRight {\n    color: $color-primary;\n    font-family: $font-bold;\n    width: 150px;\n\n    &:hover {\n      cursor: pointer;\n    }\n  }\n}\n.button {\n  display: flex;\n  width: 100%;\n  height: 80px;\n  border-radius: $container-border-radius;\n  border: 1px solid $color-border;\n  justify-content: space-between;\n  align-items: center;\n  position: relative;\n  overflow: hidden;\n\n  &:hover {\n    cursor: pointer;\n    background-color: $color-light-grey;\n  }\n  &:disabled,\n  &[disabled] {\n    background-color: $color-light-grey !important;\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n  &.progress::after {\n    content: \"\";\n    display: block;\n    position: absolute;\n    background-color: $color-primary;\n    opacity: 0.1;\n    height: 100%;\n    width: 80px;\n    left: 0px;\n    top: 0px;\n  }\n}\n.buttonLeft,\n.buttonRight {\n  width: 60px;\n  display: flex;\n  align-items: center;\n  text-align: center;\n  height: 100%;\n  justify-content: center;\n\n  svg {\n    color: $color-icon;\n  }\n}\n.buttonMiddle {\n  flex: 1;\n\n  .buttonTitle {\n    font-family: $font-bold;\n    color: $color-text-primary;\n  }\n  .buttonDescription {\n    font-family: $font-medium;\n    color: $color-text-secondary;\n    margin-top: 4px;\n  }\n}\n.inputSection {\n  padding-top: 4px;\n  padding-bottom: 4px;\n}\n.inputSectionTitle {\n  font-family: $font-medium;\n  color: $color-text-secondary;\n  margin-bottom: 4px;\n}\n.inputs {\n  display: flex;\n  gap: 18px;\n  margin-bottom: 16px;\n}\n.inputVolume {\n  flex: none !important;\n  width: 100px;\n}\n.input,\n.inputVolume {\n  flex: 1;\n  position: relative;\n\n  input {\n    border-radius: $container-border-radius;\n    border: 1px solid $color-border;\n    height: 40px;\n    box-sizing: border-box;\n    position: relative;\n    width: 100%;\n    padding-left: 18px;\n    padding-right: 40px;\n    font-family: $font-medium;\n  }\n  span {\n    color: $color-text-secondary;\n    font-family: $font-medium;\n    position: absolute;\n    right: 18px;\n    bottom: 12px;\n    user-select: none;\n  }\n}\n.inputTitle {\n  font-family: $font-medium;\n  color: $color-text-secondary;\n  margin-bottom: 8px;\n}\n.uploadArea {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  margin-bottom: 16px;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n  border: 1px solid $color-border;\n  border-radius: $container-border-radius;\n  text-align: center;\n  padding-top: 20px;\n  padding-bottom: 20px;\n\n  svg {\n    color: #6e7684;\n  }\n\n  .uploadTitle {\n    font-family: $font-bold;\n    color: $color-text-primary;\n  }\n  .uploadDescription {\n    margin-top: 10px;\n    font-family: $font-medium;\n    color: $color-text-secondary;\n  }\n\n  &:hover {\n    cursor: pointer;\n    background-color: $color-light-grey;\n  }\n}\n.audioDetails {\n  display: flex;\n  border-radius: $container-border-radius;\n  border: 1px solid $color-border;\n  height: 43px;\n  align-items: center;\n  position: relative;\n\n  .audioDetailsLeft {\n    display: flex;\n    padding-left: 14px;\n    padding-right: 8px;\n    svg {\n      color: #6e7684;\n    }\n  }\n  .audioDetailsMiddle {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    overflow: hidden;\n\n    span {\n      text-overflow: ellipsis;\n      overflow: hidden;\n      white-space: nowrap;\n      width: 90%;\n    }\n  }\n  .audioDetailsRight {\n    right: 0px;\n    top: 0px;\n    bottom: 0px;\n    display: flex;\n    align-items: center;\n    padding-right: 14px;\n    position: absolute;\n    height: 100%;\n    svg {\n      color: #6e7684;\n    }\n\n    &:hover {\n      cursor: pointer;\n    }\n  }\n}\n\n.SliderRoot {\n  position: relative;\n  display: flex;\n  align-items: center;\n  user-select: none;\n  touch-action: none;\n  width: 200px;\n  height: 20px;\n  margin-top: 36px;\n  flex: 1;\n}\n\n.SliderTrack {\n  background-color: $color-border;\n  position: relative;\n  flex-grow: 1;\n  border-radius: 9999px;\n  height: 6px;\n}\n\n.SliderRange {\n  position: absolute;\n  background-color: $color-primary;\n  border-radius: 9999px;\n  height: 100%;\n}\n\n.SliderThumb {\n  display: block;\n  width: 16px;\n  height: 16px;\n  background-color: #fff;\n  box-sizing: border-box;\n  border: 2px solid $color-primary;\n  border-radius: 10px;\n}\n.SliderThumb:focus {\n  outline: none;\n  box-shadow: $focus-border;\n}\n\n.updateButton {\n  margin-top: 20px;\n  float: right;\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_ShareModal.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.modalWrap {\n  position: fixed;\n  width: 100%;\n  height: 100%;\n  top: 0px;\n  left: 0px;\n  overflow: hidden;\n  z-index: $z-index-max;\n}\n\n.modal {\n  position: absolute;\n  left: 0px;\n  right: 0px;\n  top: 0px;\n  bottom: 0px;\n  margin: auto;\n  padding: 24px;\n  background: $color-background;\n  z-index: $z-index-max;\n  text-align: center;\n  width: fit-content;\n  height: fit-content;\n  border-radius: $container-border-radius;\n\n  .emoji {\n    font-size: 22px;\n  }\n\n  .title {\n    font-family: $font-bold;\n    color: $color-text-primary;\n    text-align: center;\n    margin-top: 8px;\n  }\n\n  .subtitle {\n    font-family: $font-medium;\n    color: $color-text-secondary;\n    margin-top: 8px;\n    text-align: center;\n    width: 300px;\n    line-height: 150%;\n  }\n\n  .button {\n    background: radial-gradient(\n        118.3% 119.01% at 35.44% 0%,\n        #2baef8 23.13%,\n        #3582f6 46.35%,\n        #486def 74.48%,\n        #7b9aea 100%\n      ),\n      #1b58f6;\n    color: $color-text-contrast;\n    text-align: center;\n    padding: 8px 16px;\n    border-radius: $container-border-radius;\n    margin-top: 18px;\n    font-family: $font-medium;\n    width: fit-content;\n    margin-left: auto;\n    margin-right: auto;\n\n    &:hover {\n      cursor: pointer;\n      opacity: 0.8;\n    }\n  }\n}\n\n.modalBackground {\n  width: 100%;\n  height: 100%;\n  position: absolute;\n  background: rgba(0, 0, 0, 0.6);\n}\n\n.close {\n  position: absolute;\n  right: 18px;\n  top: 18px;\n\n  &:hover {\n    cursor: pointer;\n    opacity: 0.8;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/player/_Title.module.scss",
    "content": "@use \"../global/variables.scss\" as *;\n\n.TitleParent {\n  position: relative;\n  bottom: 0;\n  width: 90%;\n  max-width: 900px;\n  left: 0;\n  right: 0;\n  margin: 0px !important;\n}\n\n.TitleWrap {\n  position: relative;\n  margin: auto;\n  display: flex;\n  flex-direction: column;\n  justify-content: flex-start; /* Align items at the top of the flex container */\n  height: auto; /* Allow the container to expand with multiple lines of text */\n\n  &:hover {\n    cursor: text;\n  }\n\n  h1 {\n    margin: 0;\n    padding: 0;\n    font-size: 20px;\n    font-family: $font-medium;\n    position: absolute;\n    top: 100%;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    width: calc(100% - 140px);\n  }\n\n  input {\n    position: absolute;\n    top: 100%;\n    font-family: $font-medium;\n    font-size: 20px;\n    border-radius: $container-border-radius;\n    border: 1px solid $color-border;\n    width: 100%;\n    height: 32px;\n    box-sizing: border-box;\n\n    &:focus {\n      outline: none;\n      box-shadow: $focus-border;\n    }\n  }\n}\n.pencil {\n  display: inline-block;\n  color: $color-icon;\n}\n.shareButton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: $color-text-primary;\n  color: $color-text-contrast;\n  border-radius: $container-border-radius;\n  padding: 0px 16px;\n  height: 40px;\n  position: absolute;\n  right: 0px;\n  gap: 8px;\n  font-size: 500;\n  font-family: $font-medium;\n  top: -6px;\n\n  &:hover {\n    cursor: pointer;\n    background-color: #000 !important;\n  }\n}\n.shareIcon {\n  width: 16px;\n  svg {\n    color: #fff;\n    margin-top: 1px;\n  }\n}\n"
  },
  {
    "path": "src/pages/Sandbox/styles/plyr.css",
    "content": "@charset \"UTF-8\";@keyframes plyr-progress{to{background-position:25px 0;background-position:var(--plyr-progress-loading-size,25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:inherit;font-family:var(--plyr-font-family,inherit);font-variant-numeric:tabular-nums;font-weight:400;font-weight:var(--plyr-font-weight-regular,400);line-height:1.7;line-height:var(--plyr-line-height,1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr audio,.plyr iframe,.plyr video{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui :after,.plyr--full-ui :before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:#4a5464;background:var(--plyr-badge-background,#4a5464);border-radius:2px;border-radius:var(--plyr-badge-border-radius,2px);color:#fff;color:var(--plyr-badge-text-color,#fff);font-size:9px;font-size:var(--plyr-font-size-badge,9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:13px;font-size:var(--plyr-font-size-small,13px);left:0;padding:10px;padding:var(--plyr-control-spacing,10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width:480px){.plyr__captions{font-size:15px;font-size:var(--plyr-font-size-base,15px);padding:20px;padding:calc(var(--plyr-control-spacing, 10px)*2)}}@media (min-width:768px){.plyr__captions{font-size:18px;font-size:var(--plyr-font-size-large,18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(-40px);transform:translateY(calc(var(--plyr-control-spacing, 10px)*-4))}.plyr__caption{background:#000c;background:var(--plyr-captions-background,#000c);border-radius:2px;-webkit-box-decoration-break:clone;box-decoration-break:clone;color:#fff;color:var(--plyr-captions-text-color,#fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:#0000;border:0;border-radius:3px;border-radius:var(--plyr-control-radius,3px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:7px;padding:calc(var(--plyr-control-spacing, 10px)*.7);position:relative;transition:all .3s ease}.plyr__control svg{fill:currentColor;display:block;height:18px;height:var(--plyr-control-icon-size,18px);pointer-events:none;width:18px;width:var(--plyr-control-icon-size,18px)}.plyr__control:focus{outline:0}.plyr__control:focus-visible{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}a.plyr__control{text-decoration:none}.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control.plyr__control--pressed .label--not-pressed,.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,a.plyr__control:after,a.plyr__control:before{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:2.5px;margin-left:calc(var(--plyr-control-spacing, 10px)/4)}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:2.5px;padding-left:calc(var(--plyr-control-spacing, 10px)/4)}.plyr__controls .plyr__controls__item.plyr__time{padding:0 5px;padding:0 calc(var(--plyr-control-spacing, 10px)/2)}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time,.plyr__controls .plyr__controls__item.plyr__time:first-child{padding-left:0}.plyr [data-plyr=airplay],.plyr [data-plyr=captions],.plyr [data-plyr=fullscreen],.plyr [data-plyr=pip],.plyr__controls:empty{display:none}.plyr--airplay-supported [data-plyr=airplay],.plyr--captions-enabled [data-plyr=captions],.plyr--fullscreen-enabled [data-plyr=fullscreen],.plyr--pip-supported [data-plyr=pip]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:#ffffffe6;background:var(--plyr-menu-background,#ffffffe6);border-radius:4px;border-radius:var(--plyr-menu-radius,4px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-menu-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-menu-color,#4a5464);font-size:15px;font-size:var(--plyr-font-size-base,15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:4px solid #0000;border-top-color:#ffffffe6;border:var(--plyr-menu-arrow-size,4px) solid #0000;border-top-color:var(--plyr-menu-background,#ffffffe6);content:\"\";height:0;position:absolute;right:14px;right:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7 - var(--plyr-menu-arrow-size, 4px)/2);top:100%;width:0}.plyr__menu__container [role=menu]{padding:7px;padding:calc(var(--plyr-control-spacing, 10px)*.7)}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:#4a5464;color:var(--plyr-menu-color,#4a5464);display:flex;font-size:13px;font-size:var(--plyr-font-size-menu,var(--plyr-font-size-small,13px));padding:4.66667px 10.5px;padding:calc(var(--plyr-control-spacing, 10px)*.7/1.5) calc(var(--plyr-control-spacing, 10px)*.7*1.5);-webkit-user-select:none;user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:4px solid #0000;border:var(--plyr-menu-item-arrow-size,4px) solid #0000;content:\"\";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:28px;padding-right:calc(var(--plyr-control-spacing, 10px)*.7*4)}.plyr__menu__container .plyr__control--forward:after{border-left-color:#728197;border-left-color:var(--plyr-menu-arrow-color,#728197);right:6.5px;right:calc(var(--plyr-control-spacing, 10px)*.7*1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--forward:focus-visible:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:initial}.plyr__menu__container .plyr__control--back{font-weight:400;font-weight:var(--plyr-font-weight-regular,400);margin:7px;margin:calc(var(--plyr-control-spacing, 10px)*.7);margin-bottom:3.5px;margin-bottom:calc(var(--plyr-control-spacing, 10px)*.7/2);padding-left:28px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7*4);position:relative;width:calc(100% - 14px);width:calc(100% - var(--plyr-control-spacing, 10px)*.7*2)}.plyr__menu__container .plyr__control--back:after{border-right-color:#728197;border-right-color:var(--plyr-menu-arrow-color,#728197);left:6.5px;left:calc(var(--plyr-control-spacing, 10px)*.7*1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--back:before{background:#dcdfe5;background:var(--plyr-menu-back-border-color,#dcdfe5);box-shadow:0 1px 0 #fff;box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color,#fff);content:\"\";height:1px;left:0;margin-top:3.5px;margin-top:calc(var(--plyr-control-spacing, 10px)*.7/2);overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back:focus-visible:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:initial}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:7px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7)}.plyr__menu__container .plyr__control[role=menuitemradio]:after,.plyr__menu__container .plyr__control[role=menuitemradio]:before{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:#0000001a;content:\"\";display:block;flex-shrink:0;height:16px;margin-right:10px;margin-right:var(--plyr-control-spacing,10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:#00b2ff;background:var(--plyr-control-toggle-checked-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio]:focus-visible:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:#23282f1a}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:-5px;margin-right:calc(var(--plyr-control-spacing, 10px)*.7*-1 - -2px);overflow:hidden;padding-left:24.5px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7*3.5);pointer-events:none}.plyr--full-ui input[type=range]{-webkit-appearance:none;appearance:none;background:#0000;border:0;border-radius:26px;border-radius:calc(var(--plyr-range-thumb-height, 13px)*2);color:#00b2ff;color:var(--plyr-range-fill-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));display:block;height:19px;height:calc(var(--plyr-range-thumb-active-shadow-width, 3px)*2 + var(--plyr-range-thumb-height, 13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:#0000;background-image:linear-gradient(90deg,currentColor 0,#0000 0);background-image:linear-gradient(to right,currentColor var(--value,0),#0000 var(--value,0));border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-webkit-transition:box-shadow .3s ease;transition:box-shadow .3s ease;-webkit-user-select:none;user-select:none}.plyr--full-ui input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:-4px;margin-top:calc((var(--plyr-range-thumb-height, 13px) - var(--plyr-range-track-height, 5px))/2*-1);position:relative;-webkit-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-moz-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-moz-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px)}.plyr--full-ui input[type=range]::-ms-track{color:#0000}.plyr--full-ui input[type=range]::-ms-fill-upper,.plyr--full-ui input[type=range]::-ms-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:#0000;background:currentColor;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:0;position:relative;-ms-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range]:focus-visible::-webkit-slider-runnable-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-moz-range-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-ms-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr__poster{background-color:#000;background-color:var(--plyr-video-background,var(--plyr-video-background,#000));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__time+.plyr__time:before{content:\"⁄\";margin-right:10px;margin-right:var(--plyr-control-spacing,10px)}@media (max-width:767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:#ffffffe6;background:var(--plyr-tooltip-background,#ffffffe6);border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-tooltip-color,#4a5464);font-size:13px;font-size:var(--plyr-font-size-small,13px);font-weight:400;font-weight:var(--plyr-font-weight-regular,400);left:50%;line-height:1.3;margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing, 10px)/2*2);opacity:0;padding:5px 7.5px;padding:calc(var(--plyr-control-spacing, 10px)/2) calc(var(--plyr-control-spacing, 10px)/2*1.5);pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #ffffffe6;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#ffffffe6);bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size, 4px)*-1);content:\"\";height:0;left:50%;position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr .plyr__control:focus-visible .plyr__tooltip,.plyr .plyr__control:hover .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:16px;left:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7)}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:16px;right:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7);transform:translateX(50%)}.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip{transform:translate(0) scale(1)}.plyr__progress{left:6.5px;left:calc(var(--plyr-range-thumb-height, 13px)*.5);margin-right:13px;margin-right:var(--plyr-range-thumb-height,13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:-6.5px;margin-left:calc(var(--plyr-range-thumb-height, 13px)*-.5);margin-right:-6.5px;margin-right:calc(var(--plyr-range-thumb-height, 13px)*-.5);width:calc(100% + 13px);width:calc(100% + var(--plyr-range-thumb-height, 13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:#0000;border:0;border-radius:100px;height:5px;height:var(--plyr-range-track-height,5px);left:0;margin-top:-2.5px;margin-top:calc((var(--plyr-range-track-height, 5px)/2)*-1);padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:#0000}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-webkit-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-moz-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;-ms-transition:width .2s ease;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,#23282f99 25%,#0000 0,#0000 50%,#23282f99 0,#23282f99 75%,#0000 0,#0000);background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background,#23282f99) 25%,#0000 25%,#0000 50%,var(--plyr-progress-loading-background,#23282f99) 50%,var(--plyr-progress-loading-background,#23282f99) 75%,#0000 75%,#0000);background-repeat:repeat-x;background-size:25px 25px;background-size:var(--plyr-progress-loading-size,25px) var(--plyr-progress-loading-size,25px);color:#0000}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:#ffffff40;background-color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:#c1c8d199;background-color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr__progress__marker{background-color:#fff;background-color:var(--plyr-progress-marker-background,#fff);border-radius:1px;height:5px;height:var(--plyr-range-track-height,5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:3px;width:var(--plyr-progress-marker-width,3px);z-index:3}.plyr__volume{align-items:center;display:flex;position:relative}.plyr__volume input[type=range]{margin-left:5px;margin-left:calc(var(--plyr-control-spacing, 10px)/2);margin-right:5px;margin-right:calc(var(--plyr-control-spacing, 10px)/2);max-width:90px;min-width:60px;position:relative;z-index:2}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:#fff;background:var(--plyr-audio-controls-background,#fff);border-radius:inherit;color:#4a5464;color:var(--plyr-audio-control-color,#4a5464);padding:10px;padding:var(--plyr-control-spacing,10px)}.plyr--audio .plyr__control:focus-visible,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-audio-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-audio-control-color-hover,#fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--audio .plyr__progress__buffer{color:#c1c8d199;color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr--video{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));height:100%;margin:auto;overflow:hidden;position:relative;width:100%}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}@supports not (aspect-ratio:16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:#fff;color:var(--plyr-video-control-color,#fff);left:0;padding:5px;padding:calc(var(--plyr-control-spacing, 10px)/2);padding-top:20px;padding-top:calc(var(--plyr-control-spacing, 10px)*2);position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width:480px){.plyr--video .plyr__controls{padding:10px;padding:var(--plyr-control-spacing,10px);padding-top:35px;padding-top:calc(var(--plyr-control-spacing, 10px)*3.5)}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control:focus-visible,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-video-control-color-hover,#fff)}.plyr__control--overlaid{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));border:0;border-radius:100%;color:#fff;color:var(--plyr-video-control-color,#fff);display:none;left:50%;opacity:.9;padding:15px;padding:calc(var(--plyr-control-spacing, 10px)*1.5);position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:focus,.plyr__control--overlaid:hover{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--video .plyr__progress__buffer{color:#ffffff40;color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen video{height:100%}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr:fullscreen .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;bottom:0;height:100%;left:0;margin:0;position:fixed;right:0;top:0;width:100%;z-index:10000000}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr__ads{border-radius:inherit;bottom:0;cursor:pointer;left:0;overflow:hidden;position:absolute;right:0;top:0;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:#23282f;border-radius:2px;bottom:10px;bottom:var(--plyr-control-spacing,10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:10px;right:var(--plyr-control-spacing,10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:5px;height:var(--plyr-range-track-height,5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:#ffffffe6;background-color:var(--plyr-tooltip-background,#ffffffe6);border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing, 10px)/2*2);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #ffffffe6;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#ffffffe6);bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size, 4px)*-1);content:\"\";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:#c1c8d1;border-radius:4px;border-radius:calc(var(--plyr-tooltip-radius, 5px) - 1px);overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px #00000026;content:\"\";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:4px;border-bottom-left-radius:calc(var(--plyr-tooltip-radius, 5px) - 1px);border-bottom-right-radius:4px;border-bottom-right-radius:calc(var(--plyr-tooltip-radius, 5px) - 1px);bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__preview-scrubbing{bottom:0;filter:blur(1px);height:100%;left:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;right:0;top:0;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);border:0!important;height:1px!important;overflow:hidden;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important}"
  },
  {
    "path": "src/pages/Setup/Setup.jsx",
    "content": "import React, { useEffect, useState } from \"react\";\n\nconst Setup = () => {\n  const [setupComplete, setSetupComplete] = useState(false);\n\n  useEffect(() => {\n    // Inject content script\n    const script = document.createElement(\"script\");\n    script.src = chrome.runtime.getURL(\"contentScript.bundle.js\");\n    script.async = true;\n    document.body.appendChild(script);\n\n    // Also inject CSS\n    const style = document.createElement(\"link\");\n    style.rel = \"stylesheet\";\n    style.type = \"text/css\";\n    style.href = chrome.runtime.getURL(\"assets/fonts/fonts.css\");\n    document.body.appendChild(style);\n\n    // Return\n    return () => {\n      document.body.removeChild(script);\n      document.body.removeChild(style);\n    };\n  }, []);\n\n  useEffect(() => {\n    chrome.runtime.onMessage.addListener(function (\n      request,\n      sender,\n      sendResponse\n    ) {\n      if (request.type === \"setup-complete\") {\n        setSetupComplete(true);\n      }\n    });\n  }, []);\n\n  return (\n    <div className=\"setupBackground\">\n      {!setupComplete && (\n        <div className=\"setupContainer\">\n          <div className=\"setupImage\">\n            <img src={chrome.runtime.getURL(\"assets/helper/pin.gif\")} />\n          </div>\n          <div className=\"setupText\">\n            <div className=\"setupEmoji\">👋</div>\n            <div className=\"setupTitle\">\n              {chrome.i18n.getMessage(\"setupTitle\")}\n            </div>\n            <div className=\"setupDescription\">\n              <div className=\"setupStep\">\n                {chrome.i18n.getMessage(\"setupStep1Before\")}\n                <span>\n                  <img\n                    src={chrome.runtime.getURL(\"assets/helper/puzzle.svg\")}\n                  />\n                </span>\n                {chrome.i18n.getMessage(\"setupStep1After\")}\n              </div>\n              <div className=\"setupStep\">\n                {chrome.i18n.getMessage(\"setupStep2Before\")}\n                <span>\n                  <img src={chrome.runtime.getURL(\"assets/helper/pin.svg\")} />\n                </span>{\" \"}\n                {chrome.i18n.getMessage(\"setupStep2After\")}\n              </div>\n              <div className=\"setupStep\">\n                {chrome.i18n.getMessage(\"setupStep3Before\")}\n                <span>\n                  <img\n                    src={chrome.runtime.getURL(\n                      \"assets/helper/mini-screenity.png\"\n                    )}\n                  />\n                </span>\n                {chrome.i18n.getMessage(\"setupStep3After\")}\n              </div>\n            </div>\n          </div>\n        </div>\n      )}\n      {setupComplete && (\n        <div className=\"setupContainer center\">\n          <div className=\"setupText center\">\n            <div className=\"setupEmoji\">🥳</div>\n            <div className=\"setupTitle\">\n              {chrome.i18n.getMessage(\"setupCompleteTitle\")}\n            </div>\n            <div className=\"setupDescription\">\n              {chrome.i18n.getMessage(\"setupCompleteDescription\")}\n            </div>\n          </div>\n        </div>\n      )}\n      <img\n        className=\"setupLogo\"\n        src={chrome.runtime.getURL(\"assets/logo-text.svg\")}\n      />\n      <style>\n        {`\n\t\t\t\tbody {\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\tmargin: 0px;\n\t\t\t\t\tmargin: 0;\n\tpadding: 0;\n\tmin-height: 100%;\n\t\tbackground-color: #F6F7FB!important;\n\t\tbackground: url('` +\n          chrome.runtime.getURL(\"assets/helper/pattern-svg.svg\") +\n          `') repeat;\n\t\tbackground-size: 62px 23.5px;\n\t\tanimation: moveBackground 138s linear infinite;\n\t\ttransform: rotate(0deg);\n\t\t\t\t}\n\n\t\t\t\t.setupInfo {\n\t\t\t\t\tmargin-top: 20px;\n\t\t\t\t}\n\t\t\t\ta {\n\t\t\t\t\ttext-decoration: none!important;\n\t\t\t\t\tcolor: #4C7DE2;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@keyframes moveBackground {\n\t\t\t\t\t0% {\n\t\t\t\t\t\tbackground-position: 0 0;\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\tbackground-position: 100% 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\n\t\t\t\t.setupLogo {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\tbottom: 30px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\twidth: 120px;\n\t\t\t\t}\n\n\n\t\t\t\t.setupBackground {\n\t\t\t\t\theight: 100vh;\n\t\t\t\t\twidth: 100vw;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupContainer {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0px;\n\t\t\t\t\tleft: 0px;\n\t\t\t\t\tright: 0px;\n\t\t\t\t\tbottom: 0px;\n\t\t\t\t\tmargin: auto;\n\t\t\t\t\tz-index: 999;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\twidth: 60%;\n\t\t\t\t\theight: fit-content;\n\t\t\t\t\tbackground-color: #fff;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tpadding: 50px 50px;\n\t\t\t\t\tgap: 80px;\n\t\t\t\t\tfont-family: 'Satoshi-Medium', sans-serif;\n\t\t\t\t}\n\n\t\t\t\t.setupImage {\n\t\t\t\t\twidth: 70%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: center;\n\t\t\t\t}\n\n\t\t\t\t.setupImage img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t}\n\n\t\t\t\t.setupText {\n\t\t\t\t\twidth: 50%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: left;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\ttext-align: left;\n\t\t\t\t}\n\n\t\t\t\t.setupEmoji {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t}\n\n\t\t\t\t.setupTitle {\n\t\t\t\t\tfont-size: 20px;\n\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tcolor: #29292F;\n\t\t\t\t\tfont-family: 'Satoshi-Bold', sans-serif!important;\n\t\t\t\t\tletter-spacing: -0.5px;\n\t\t\t\t}\n\n\t\t\t\t.setupDescription {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tflex-direction: column;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\talign-items: left;\n\t\t\t\t\tmargin-top: 10px;\n\t\t\t\t\tcolor: #6E7684;\n\t\t\t\t\tfont-size: 14px;\n\t\t\t\t}\n\n\t\t\t\t.setupStep {\n\t\t\t\t\tmargin-bottom: 10px;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t}\n\n\t\t\t\t.setupStep span {\n\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\twidth: 20px;\n\t\t\t\t\theight: 20px;\n\t\t\t\t\tpadding: 2px;\n\t\t\t\t\tborder-radius: 30px;\n\t\t\t\t\tdisplay: inline-flex;\n\t\t\t\t\tvertical-align: middle;\n\t\t\t\t\tmargin-left: 3px;\n\t\t\t\t\tmargin-right: 3px;\n\t\t\t\t\tbackground-color: #F4F2F2;\n\t\t\t\t}\n\n\t\t\t\t.setupStep img {\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tdisplay: block;\n\t\t\t\t}\n\n\t\t\t\t.center {\n\t\t\t\t\ttext-align: center!important;\n\t\t\t\t}\n\t\t\t\t.setupText.center {\n\t\t\t\t\twidth: auto!important;\n\t\t\t\t}\n\t\t\t\t.setupContainer.center {\n\t\t\t\t\twidth: 40%!important;\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\t@media only screen and (max-width: 800px) {\n\t\t\t\t\t.setupContainer {\n\t\t\t\t\t\tflex-direction: column;\n\t\t\t\t\t\tgap: 40px;\n\n\t\t\t\t\t}\n\n\t\t\t\t\t.setupText, .setupImage {\n\t\t\t\t\t\twidth: 100%!important;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t@media only screen and (max-width: 500px) {\n\t\t\t\t\t.setupContainer {\n\t\t\t\t\t\twidth: 80%!important;\n\t\t\t\t\t\tpadding: 20px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupTitle {\n\t\t\t\t\t\tfont-size: 18px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupDescription {\n\t\t\t\t\t\tfont-size: 12px!important;\n\t\t\t\t\t}\n\t\t\t\t\t.setupStep {\n\t\t\t\t\t\tfont-size: 12px!important;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\n\t\t\t\t`}\n      </style>\n    </div>\n  );\n};\n\nexport default Setup;\n"
  },
  {
    "path": "src/pages/Setup/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Screenity - First Steps</title>\n    <style>\n      @font-face {\n        font-family: Satoshi-Light;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Light.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Medium;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Medium.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Satoshi-Bold;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/Satoshi-Bold.ttf)\n          format(\"truetype\");\n        font-weight: normal;\n        font-style: normal;\n      }\n\n      @font-face {\n        font-family: Gloria-Hallelujah;\n        src: url(chrome-extension://__MSG_@@extension_id__/assets/fonts/GloriaHallelujah-Regular.ttf)\n          format(\"truetype\");\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Setup/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport Setup from \"./Setup\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Setup />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/Waveform/Waveform.jsx",
    "content": "import React, { useEffect, useRef } from \"react\";\n\nconst Waveform = () => {\n  const canvasRef = useRef(null);\n  let audioContext;\n  let analyser;\n  let dataArray;\n  let animationFrameId;\n  let audioStream;\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const canvasContext = canvas.getContext(\"2d\");\n\n    function initializeAudioContext() {\n      if (!audioContext) {\n        audioContext = new (window.AudioContext || window.webkitAudioContext)();\n        analyser = audioContext.createAnalyser();\n        analyser.fftSize = 2048;\n        analyser.maxDecibels = -60;\n        dataArray = new Float32Array(analyser.fftSize);\n        const source = audioContext.createMediaStreamDestination();\n        analyser.connect(source);\n        audioContext.resume();\n        startVisualization();\n      }\n    }\n\n    function startVisualization() {\n      analyser.getFloatTimeDomainData(dataArray);\n      canvasContext.clearRect(0, 0, canvas.width, canvas.height);\n      canvasContext.beginPath();\n      const sliceWidth = 0.9;\n      const waveformHeight = canvas.height;\n      const waveformOffset = (canvas.height - waveformHeight) / 2;\n      let x = 0;\n      let sum = 0;\n      let count = 0;\n      for (let i = 0; i < dataArray.length; i++) {\n        const v = (dataArray[i] + 1) / 2;\n        sum += v;\n        count++;\n        if (count === 10) {\n          const avg = sum / count;\n          const y = avg * waveformHeight * 2 + waveformOffset;\n          if (i === 0) {\n            canvasContext.moveTo(x, y);\n          } else {\n            canvasContext.lineTo(x, y);\n          }\n          x += sliceWidth;\n          sum = 0;\n          count = 0;\n        }\n      }\n      canvasContext.strokeStyle = \"#78C072\";\n      canvasContext.stroke();\n\n      animationFrameId = requestAnimationFrame(startVisualization);\n    }\n\n    function startVisualization() {\n      analyser.getFloatTimeDomainData(dataArray);\n      canvasContext.clearRect(0, 0, canvas.width, canvas.height);\n      canvasContext.beginPath();\n      const sliceWidth = 0.7;\n      const waveformHeight = canvas.height * 0.9;\n      const waveformOffset = (canvas.height - waveformHeight) / 2;\n      let x = 0;\n      for (let i = 0; i < dataArray.length; i++) {\n        const v = (dataArray[i] + 1) / 2;\n        const y = v * waveformHeight + waveformOffset;\n        if (i === 0) {\n          canvasContext.moveTo(x, y);\n        } else {\n          canvasContext.lineTo(x, y);\n        }\n        x += sliceWidth;\n      }\n      canvasContext.strokeStyle = \"#78C072\";\n      canvasContext.lineWidth = 1.5;\n      canvasContext.stroke();\n\n      animationFrameId = requestAnimationFrame(startVisualization);\n    }\n\n    function stopVisualization() {\n      if (animationFrameId) {\n        cancelAnimationFrame(animationFrameId);\n      }\n    }\n\n    function startAudioCapture() {\n      navigator.mediaDevices\n        .getUserMedia({ audio: true, video: false })\n        .then((stream) => {\n          audioStream = stream;\n          initializeAudioContext();\n          const audioSource = audioContext.createMediaStreamSource(audioStream);\n          audioSource.connect(analyser);\n        })\n        .catch((error) => {\n          console.error(\"Error capturing audio:\", error);\n        });\n    }\n\n    function stopAudioCapture() {\n      if (audioStream) {\n        const tracks = audioStream.getTracks();\n        tracks.forEach((track) => track.stop());\n        audioStream = null;\n        stopVisualization();\n      }\n    }\n\n    startAudioCapture();\n\n    return () => {\n      stopAudioCapture();\n    };\n  }, []);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      width=\"324\"\n      height=\"30\"\n      style={{ background: \"#f5f6fa\" }}\n    />\n  );\n};\n\nexport default Waveform;\n"
  },
  {
    "path": "src/pages/Waveform/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body style=\"margin: 0px;overflow: hidden;\">\n    <div id=\"app-container\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/pages/Waveform/index.jsx",
    "content": "import React from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport Waveform from \"./Waveform\";\n\n// Find the container to render into\nconst container = window.document.querySelector(\"#app-container\");\n\nif (container) {\n  const root = createRoot(container);\n  root.render(<Waveform />);\n}\n\n// Hot Module Replacement\nif (module.hot) {\n  module.hot.accept();\n}\n"
  },
  {
    "path": "src/pages/utils/buildDiagnosticZip.js",
    "content": "/**\n * Builds a diagnostic ZIP for troubleshooting. Returns { blob, filename }.\n * Used by the popup settings menu and the editor \"Get help\" button.\n */\nimport JSZip from \"jszip\";\nimport { getStartFlowTrace } from \"./startFlowTrace\";\n\nconst FAST_RECORDER_KEYS = [\n  \"fastRecorderBeta\",\n  \"fastRecorderDecision\",\n  \"fastRecorderDisabledForDevice\",\n  \"fastRecorderDisabledReason\",\n  \"fastRecorderDisabledDetails\",\n  \"fastRecorderDisabledAt\",\n  \"fastRecorderProbe\",\n  \"fastRecorderValidation\",\n  \"fastRecorderValidationFailed\",\n  \"fastRecorderInUse\",\n];\n\nexport const buildDiagnosticZip = async ({\n  extraConfig = {},\n  source = \"unknown\",\n} = {}) => {\n  const userAgent = navigator.userAgent;\n\n  // Parallel fetches from background\n  const [platformInfo, diagData, fastRecorderData] = await Promise.all([\n    chrome.runtime.sendMessage({ type: \"get-platform-info\" }),\n    chrome.runtime.sendMessage({ type: \"get-diagnostic-log\" }),\n    new Promise((resolve) =>\n      chrome.storage.local.get(FAST_RECORDER_KEYS, resolve),\n    ),\n  ]);\n\n  const manifestVersion = chrome.runtime.getManifest().version;\n  const now = new Date();\n  const ts = now.toISOString().replace(/[:.]/g, \"-\").slice(0, 19);\n\n  const zip = new JSZip();\n\n  // manifest.json — export metadata\n  zip.file(\n    \"manifest.json\",\n    JSON.stringify({\n      extensionVersion: manifestVersion,\n      schemaVersion: 1,\n      exportedAt: now.toISOString(),\n      chromeVersion: /Chrome\\/([\\d.]+)/.exec(userAgent)?.[1] || null,\n      source,\n    }),\n  );\n\n  // environment.json\n  zip.file(\n    \"environment.json\",\n    JSON.stringify({\n      userAgent,\n      platformInfo,\n      screen: {\n        width: window.screen.availWidth,\n        height: window.screen.availHeight,\n        devicePixelRatio: window.devicePixelRatio,\n      },\n      deviceMemory: navigator.deviceMemory || null,\n    }),\n  );\n\n  // config.json — recording settings + caller-specific extras\n  zip.file(\n    \"config.json\",\n    JSON.stringify({\n      fastRecorder: fastRecorderData,\n      ...extraConfig,\n    }),\n  );\n\n  // sessions.json — diagnostic session timeline with derived hints\n  if (diagData?.log) {\n    const annotated = JSON.parse(JSON.stringify(diagData.log));\n    if (annotated.sessions) {\n      for (const session of annotated.sessions) {\n        if (!session.events?.length) continue;\n        const hints = [];\n        const hasEditorOpen = session.events.some(\n          (ev) => ev.e === \"editor-open\" && ev.d?.type === \"editorwebcodecs\",\n        );\n        const hasEditorReady = session.events.some(\n          (ev) => ev.e === \"editor-load-ready\",\n        );\n        if (hasEditorOpen && !hasEditorReady) {\n          hints.push(\"Editor handoff incomplete: editor opened but never finished loading\");\n        }\n        if (hints.length > 0) session.hints = hints;\n      }\n    }\n    zip.file(\"sessions.json\", JSON.stringify(annotated));\n  }\n\n  // errors.json — consolidated error-state keys\n  if (diagData?.errors) {\n    zip.file(\"errors.json\", JSON.stringify(diagData.errors));\n  }\n\n  // storage-flags.json — current boolean/numeric state flags\n  if (diagData?.flags) {\n    zip.file(\"storage-flags.json\", JSON.stringify(diagData.flags));\n  }\n\n  // start-flow-trace.json\n  try {\n    const trace = await getStartFlowTrace();\n    if (trace) {\n      zip.file(\"start-flow-trace.json\", JSON.stringify(trace));\n    }\n  } catch {\n    // best effort\n  }\n\n  const blob = await zip.generateAsync({ type: \"blob\" });\n  const filename = `screenity-diagnostics-${ts}.zip`;\n\n  return { blob, filename };\n};\n"
  },
  {
    "path": "src/pages/utils/buildSupportContext.js",
    "content": "/**\n * Builds a URL-safe support-context object for Tally form prefills.\n * No URLs, tokens, page content, or blobs — only sanitized technical metadata.\n */\n\nconst MAX_ERR_LEN = 120;\n\nconst sanitizeError = (err) => {\n  if (!err) return \"\";\n  const str =\n    typeof err === \"string\"\n      ? err\n      : err.message || err.errorCode || JSON.stringify(err);\n  return str\n    .replace(/https?:\\/\\/[^\\s)]+/gi, \"[url]\")\n    .replace(/chrome-extension:\\/\\/[^\\s)]+/gi, \"[ext]\")\n    .slice(0, MAX_ERR_LEN);\n};\n\nconst shortBrowser = () => {\n  const ua = navigator.userAgent || \"\";\n  const edg = ua.match(/Edg\\/(\\d+)/);\n  if (edg) return `Edge/${edg[1]}`;\n  const ch = ua.match(/Chrome\\/(\\d+)/);\n  if (ch) return `Chrome/${ch[1]}`;\n  return \"Unknown\";\n};\n\n/** Build support context. Returns flat key-value pairs for URLSearchParams. */\nexport const buildSupportContext = async (opts = {}) => {\n  const ctx = {};\n\n  // ---- always included ----\n  ctx.v = chrome.runtime.getManifest().version;\n  ctx.lang = chrome.i18n.getMessage(\"@@ui_locale\") || \"unknown\";\n  ctx.br = shortBrowser();\n\n  try {\n    const info = await chrome.runtime.getPlatformInfo();\n    ctx.os = info.os; // \"mac\", \"win\", \"linux\", \"cros\", etc.\n  } catch {\n    ctx.os = \"unknown\";\n  }\n\n  // Human-readable platform for Tally choice prefill\n  const platformMap = { mac: \"macOS\", win: \"Windows\", linux: \"Linux\", cros: \"ChromeOS\" };\n  ctx.platform = platformMap[ctx.os] || \"Other\";\n\n  ctx.cloud =\n    process.env.SCREENITY_ENABLE_CLOUD_FEATURES === \"true\" ? \"1\" : \"0\";\n\n  if (opts.source) {\n    ctx.src = opts.source;\n  }\n\n  // ---- recording / error state (opt-in) ----\n  if (opts.includeRecordingState) {\n    try {\n      const keys = [\n        \"lastRecordingError\",\n        \"lastRecordingType\",\n        \"recordingType\",\n        \"fastRecorderInUse\",\n        \"fastRecorderDisabledReason\",\n        \"isSubscribed\",\n        \"diagnosticLog\",\n        \"recordingAttemptId\",\n      ];\n      const store = await chrome.storage.local.get(keys);\n\n      const recType = store.lastRecordingType || store.recordingType;\n      if (recType) ctx.recType = recType;\n      if (store.fastRecorderInUse != null)\n        ctx.fast = store.fastRecorderInUse ? \"1\" : \"0\";\n      if (store.fastRecorderDisabledReason)\n        ctx.fastOff = String(store.fastRecorderDisabledReason).slice(0, 60);\n      ctx.sub = store.isSubscribed ? \"1\" : \"0\";\n      ctx.cloud = store.isSubscribed ? \"1\" : \"0\";\n      if (store.lastRecordingError) {\n        ctx.lastErr = sanitizeError(\n          store.lastRecordingError.why || store.lastRecordingError.error,\n        );\n        if (store.lastRecordingError.errorCode)\n          ctx.errCode = store.lastRecordingError.errorCode;\n      }\n      if (store.recordingAttemptId)\n        ctx.attemptId = store.recordingAttemptId;\n\n      // Last diagnostic session outcome (compact — just the outcome string)\n      if (store.diagnosticLog?.sessions?.length) {\n        const last =\n          store.diagnosticLog.sessions[\n            store.diagnosticLog.sessions.length - 1\n          ];\n        if (last?.outcome) ctx.lastOutcome = last.outcome;\n        // Detect incomplete editor handoff (editor opened but never loaded)\n        if (last?.events?.length) {\n          const hasEditorOpen = last.events.some(\n            (ev) => ev.e === \"editor-open\" && ev.d?.type === \"editorwebcodecs\",\n          );\n          const hasEditorReady = last.events.some(\n            (ev) => ev.e === \"editor-load-ready\",\n          );\n          if (hasEditorOpen && !hasEditorReady) ctx.editorHandoff = \"incomplete\";\n        }\n      }\n    } catch {\n      // storage read failed — continue without recording state\n    }\n  }\n\n  // ---- explicit error overrides (from error modal \"Get help\" button) ----\n  if (opts.errorCode) ctx.errCode = opts.errorCode;\n  if (opts.errorWhy) ctx.lastErr = sanitizeError(opts.errorWhy);\n\n  // ---- support code (short human-readable reference) ----\n  if (ctx.attemptId) {\n    const hash = ctx.attemptId.replace(/[^a-z0-9]/gi, \"\").slice(-4).toUpperCase();\n    const date = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n    ctx.supportCode = `SCR-${hash}-${date}`;\n  }\n\n  // ---- user info (only when explicitly provided) ----\n  if (opts.user) {\n    if (opts.user.name) ctx.name = opts.user.name;\n    if (opts.user.email) ctx.email = opts.user.email;\n  }\n\n  return ctx;\n};\n\n/** Build context and return as a query string. */\nexport const supportContextQuery = async (opts = {}) => {\n  const ctx = await buildSupportContext(opts);\n  const params = new URLSearchParams();\n  for (const [k, val] of Object.entries(ctx)) {\n    if (val !== undefined && val !== null && val !== \"\") {\n      params.set(k, val);\n    }\n  }\n  return params.toString();\n};\n"
  },
  {
    "path": "src/pages/utils/buildSupportDebugInfo.js",
    "content": "/**\n * Clipboard-friendly debug summary for support.\n * Same privacy rules as buildSupportContext — no URLs, tokens, or user content.\n */\n\nimport { makeSupportCode } from \"./errorCodes\";\nimport { getStartFlowTrace, formatStartFlowTimeline } from \"./startFlowTrace\";\n\nconst MAX_ERR_LEN = 120;\n\nconst sanitizeError = (err) => {\n  if (!err) return \"\";\n  const str = typeof err === \"string\" ? err : String(err);\n  return str\n    .replace(/https?:\\/\\/[^\\s)]+/gi, \"[url]\")\n    .replace(/chrome-extension:\\/\\/[^\\s)]+/gi, \"[ext]\")\n    .slice(0, MAX_ERR_LEN);\n};\n\nconst shortBrowser = () => {\n  const ua = navigator.userAgent || \"\";\n  const edg = ua.match(/Edg\\/(\\d+)/);\n  if (edg) return `Edge/${edg[1]}`;\n  const ch = ua.match(/Chrome\\/(\\d+)/);\n  if (ch) return `Chrome/${ch[1]}`;\n  return \"Unknown\";\n};\n\nconst shortOS = async () => {\n  try {\n    const info = await chrome.runtime.getPlatformInfo();\n    return info.os || \"unknown\";\n  } catch {\n    return \"unknown\";\n  }\n};\n\n/** Build a plain-text debug info block for clipboard. */\nexport const buildSupportDebugInfo = async (opts = {}) => {\n  const version = chrome.runtime.getManifest().version;\n  const browser = shortBrowser();\n  const os = await shortOS();\n  const ts = new Date().toISOString();\n\n  // Pull relevant state from storage\n  let store = {};\n  try {\n    store = await chrome.storage.local.get([\n      \"recordingAttemptId\",\n      \"recordingType\",\n      \"fastRecorderInUse\",\n      \"fastRecorderDisabledReason\",\n      \"isSubscribed\",\n      \"lastRecordingError\",\n      \"lastStreamCheckFail\",\n      \"lastAutoDiscardableError\",\n      \"streamLifecycleLog\",\n      \"diagnosticLog\",\n    ]);\n  } catch {\n    // storage read failed — continue with defaults\n  }\n\n  const attemptId = store.recordingAttemptId || null;\n  const supportCode = makeSupportCode(attemptId);\n\n  // Determine error code: prefer explicit, fall back to stored\n  const errorCode =\n    opts.errorCode ||\n    store.lastRecordingError?.errorCode ||\n    null;\n\n  const errorWhy = sanitizeError(\n    opts.errorWhy || store.lastRecordingError?.why || \"\"\n  );\n\n  // Recording mode\n  const recType = store.recordingType || \"unknown\";\n  const fastRec = store.fastRecorderInUse ? \"active\" : \"off\";\n  const fastOff = store.fastRecorderDisabledReason || null;\n  const sub = store.isSubscribed ? \"pro\" : \"free\";\n\n  // Last diagnostic session outcome + editor handoff check\n  let lastOutcome = null;\n  let editorHandoffIncomplete = false;\n  if (store.diagnosticLog?.sessions?.length) {\n    const last =\n      store.diagnosticLog.sessions[store.diagnosticLog.sessions.length - 1];\n    lastOutcome = last?.outcome || null;\n    if (last?.events?.length) {\n      const hasEditorOpen = last.events.some(\n        (ev) => ev.e === \"editor-open\" && ev.d?.type === \"editorwebcodecs\",\n      );\n      const hasEditorReady = last.events.some(\n        (ev) => ev.e === \"editor-load-ready\",\n      );\n      editorHandoffIncomplete = hasEditorOpen && !hasEditorReady;\n    }\n  }\n\n  // Build the text block\n  const lines = [\n    `Screenity Debug Info`,\n    `====================`,\n    `Code:      ${supportCode}`,\n  ];\n  if (errorCode) lines.push(`Error:     ${errorCode}`);\n  if (errorWhy) lines.push(`Detail:    ${errorWhy}`);\n  lines.push(`Version:   ${version}`);\n  lines.push(`Browser:   ${browser}`);\n  lines.push(`OS:        ${os}`);\n  lines.push(`Mode:      ${recType}`);\n  lines.push(`Fast MP4:  ${fastRec}${fastOff ? ` (${fastOff})` : \"\"}`);\n  lines.push(`Plan:      ${sub}`);\n  if (lastOutcome) lines.push(`Session:   ${lastOutcome}`);\n  if (editorHandoffIncomplete) lines.push(`EditorLoad: incomplete (editor opened but never became ready)`);\n  if (store.lastStreamCheckFail) {\n    const sc = store.lastStreamCheckFail;\n    lines.push(`StreamChk: ${sc.bucket || \"?\"} vis=${sc.docVisibility || sc.docHidden} ms=${sc.msSinceReady ?? \"?\"}`);\n  }\n  if (store.lastAutoDiscardableError) {\n    lines.push(`AutoDisc:  failed tab=${store.lastAutoDiscardableError.tabId}`);\n  }\n  if (store.streamLifecycleLog?.length) {\n    const sl = store.streamLifecycleLog;\n    const tags = sl.map((e) => `${e.tag}@${e.t}`).join(\" → \");\n    lines.push(`SL(${sl.length}): ${tags.slice(0, 300)}`);\n  }\n  if (attemptId) lines.push(`Ref:       ${attemptId}`);\n  lines.push(`Time:      ${ts}`);\n\n  // Start-flow trace timeline\n  try {\n    const trace = await getStartFlowTrace();\n    if (trace) {\n      const timeline = formatStartFlowTimeline(trace);\n      if (timeline) {\n        lines.push(\"\");\n        lines.push(\"Start Flow:\");\n        lines.push(timeline);\n      }\n    }\n  } catch {\n    // best effort\n  }\n\n  return lines.join(\"\\n\");\n};\n"
  },
  {
    "path": "src/pages/utils/diagnosticLog.js",
    "content": "/**\n * Ring-buffer diagnostic log stored in chrome.storage.local (\"diagnosticLog\").\n * Max 5 sessions, 100 events each. Used in the background service worker.\n */\n\nconst MAX_SESSIONS = 5;\nconst MAX_EVENTS = 100;\nconst FLUSH_EVERY_N = 10; // flush to storage every N events\n\nlet _log = null; // in-memory mirror of diagnosticLog\nlet _dirty = 0; // events written since last flush\n\nconst now = () => Date.now();\n\nconst makeId = () =>\n  `diag-${now()}-${Math.random().toString(16).slice(2, 7)}`;\n\nconst currentSession = () => {\n  if (!_log || !_log.sessions || _log.sessions.length === 0) return null;\n  const last = _log.sessions[_log.sessions.length - 1];\n  return last.endedAt == null ? last : null;\n};\n\nconst flush = async () => {\n  if (!_log) return;\n  _dirty = 0;\n  try {\n    await chrome.storage.local.set({ diagnosticLog: _log });\n  } catch {\n    // SW may be shutting down — best-effort\n  }\n};\n\nconst maybeFlush = () => {\n  _dirty += 1;\n  if (_dirty >= FLUSH_EVERY_N) {\n    flush(); // fire-and-forget\n  }\n};\n\n/** Hydrate the in-memory log from storage. Call once on SW init. */\nexport const hydrateDiagnosticLog = async () => {\n  try {\n    const res = await chrome.storage.local.get(\"diagnosticLog\");\n    _log = res?.diagnosticLog || { schemaVersion: 1, sessions: [] };\n  } catch {\n    _log = { schemaVersion: 1, sessions: [] };\n  }\n};\n\n/** Start a new session. Call from startRecording(). */\nexport const initDiagSession = async (config = {}) => {\n  if (!_log) await hydrateDiagnosticLog();\n\n  const session = {\n    id: makeId(),\n    startedAt: now(),\n    endedAt: null,\n    outcome: \"in-progress\",\n    config,\n    events: [],\n  };\n\n  _log.sessions.push(session);\n\n  // Trim to MAX_SESSIONS\n  while (_log.sessions.length > MAX_SESSIONS) {\n    _log.sessions.shift();\n  }\n\n  await flush();\n  return session.id;\n};\n\n/** Append an event to the current open session. */\nexport const diagEvent = (eventType, data) => {\n  const s = currentSession();\n  if (!s) return; // no open session\n\n  const entry = {\n    t: now() - s.startedAt,\n    e: eventType,\n  };\n  if (data !== undefined && data !== null) {\n    entry.d = data;\n  }\n\n  s.events.push(entry);\n\n  // Ring-buffer trim\n  while (s.events.length > MAX_EVENTS) {\n    s.events.shift();\n  }\n\n  // Force-flush on lifecycle/error events so they survive SW termination.\n  const ALWAYS_FLUSH = [\n    \"error\",\n    \"crash\",\n    \"start-fail\",\n    \"warning\",\n    \"session-start\",\n    \"stop\",\n    \"stop-tab\",\n    \"editor-open\",\n    \"chunks-sent\",\n    \"chunks-fail\",\n    \"sw-init\",\n    \"countdown-started\",\n    \"countdown-cancelled\",\n    \"countdown-finished\",\n    \"pause\",\n    \"resume\",\n    \"restart-requested\",\n    \"restart-completed\",\n    \"restart-failed\",\n    \"alarm-fired\",\n    \"recorded-tab-closed\",\n    \"recorded-tab-navigated\",\n    \"drive-upload-start\",\n    \"drive-upload-ok\",\n    \"drive-upload-fail\",\n    \"drive-save-fail\",\n    \"drive-auth-fail\",\n    \"editor-load-ready\",\n  ];\n  if (ALWAYS_FLUSH.includes(eventType)) {\n    flush();\n  } else {\n    maybeFlush();\n  }\n};\n\n/** Close the current session with a final outcome. */\nexport const endDiagSession = async (outcome = \"ok\") => {\n  const s = currentSession();\n  if (!s) return;\n  s.endedAt = now();\n  s.outcome = outcome;\n  await flush();\n};\n\n/** Return the full diagnostic log (for export). */\nexport const getDiagnosticLog = async () => {\n  if (!_log) await hydrateDiagnosticLog();\n  return _log;\n};\n\n/** Collect error-state keys from storage into one object. */\nexport const getErrorSnapshot = async () => {\n  const keys = [\n    \"lastRecordingError\",\n    \"lastChunkSendFailure\",\n    \"recorderSession\",\n    \"freeRecorderSession\",\n    \"fastRecorderValidation\",\n    \"fastRecorderProbe\",\n    \"fastRecorderDecision\",\n    \"fastRecorderDisabledReason\",\n    \"fastRecorderDisabledDetails\",\n    \"fastRecorderDisabledAt\",\n    \"memoryError\",\n    \"recordingAttemptId\",\n  ];\n  try {\n    return await chrome.storage.local.get(keys);\n  } catch {\n    return {};\n  }\n};\n\n/** Read current state flags for export. */\nexport const getStorageFlags = async () => {\n  const keys = [\n    \"recording\",\n    \"pendingRecording\",\n    \"restarting\",\n    \"paused\",\n    \"offscreen\",\n    \"sendingChunks\",\n    \"postStopEditorOpening\",\n    \"postStopEditorOpened\",\n    \"recordingStartTime\",\n    \"totalPausedMs\",\n    \"recordingDuration\",\n    \"recordingTab\",\n    \"sandboxTab\",\n    \"recordingUiTabId\",\n    \"fastRecorderInUse\",\n    \"memoryError\",\n    \"lowStorageAbortAt\",\n    \"lowStorageAbortChunks\",\n    \"editorLoadTimeoutAt\",\n  ];\n  try {\n    return await chrome.storage.local.get(keys);\n  } catch {\n    return {};\n  }\n};\n"
  },
  {
    "path": "src/pages/utils/errorCodes.js",
    "content": "/**\n * Error codes for recording flows.\n *\n * Codes are stored in `lastRecordingError.code` and flow through diagnostics\n * and support context. They never contain URLs, page content, or user data.\n */\n\n// -- Recording Start --\nexport const REC_START_PERM = \"REC_START_PERM\"; // permission denied\nexport const REC_START_STREAM = \"REC_START_STREAM\"; // getUserMedia / getDisplayMedia failed\nexport const REC_START_CODEC = \"REC_START_CODEC\"; // MediaRecorder or WebCodecs init failed\nexport const REC_START_REGION = \"REC_START_REGION\"; // region/crop target setup failed\nexport const REC_START_TAB = \"REC_START_TAB\"; // tab capture or message routing failed\nexport const REC_START_TIMEOUT = \"REC_START_TIMEOUT\"; // start flow timed out\nexport const REC_START_CANCEL = \"REC_START_CANCEL\"; // user cancelled the capture picker\nexport const REC_START_NOT_READY = \"REC_START_NOT_READY\"; // recorder tab not ready (likely discarded)\n\n// -- Recording Runtime --\nexport const REC_RUN_STREAM_END = \"REC_RUN_STREAM_END\"; // stream ended unexpectedly\nexport const REC_RUN_MEMORY = \"REC_RUN_MEMORY\"; // memory/quota pressure\n\n// -- Recording Stop --\nexport const REC_STOP_CHUNKS = \"REC_STOP_CHUNKS\"; // chunk delivery failed\nexport const REC_STOP_EDITOR = \"REC_STOP_EDITOR\"; // editor tab open failed\n\n// -- Drive --\nexport const DRV_AUTH = \"DRV_AUTH\"; // auth failed\nexport const DRV_FOLDER = \"DRV_FOLDER\"; // folder operation failed\nexport const DRV_UPLOAD = \"DRV_UPLOAD\"; // file upload failed\n\n// -- Fallback --\nexport const UNKNOWN = \"UNKNOWN\";\n\n// -- Classifier --\n\n// Patterns that strongly indicate the user explicitly cancelled the picker.\n// Note: \"aborterror\" is intentionally NOT here — Chrome throws AbortError for\n// picker dismissal but also for unrelated abort scenarios.  We only classify\n// AbortError as cancel when the caller passes the \"cancel-modal\" hint.\nconst CANCEL_PATTERNS = [\n  \"cancelled\",\n  \"canceled\",\n  \"cancel-modal\",\n  \"user cancelled\",\n  \"user canceled\",\n];\n\nconst PERM_PATTERNS = [\n  \"permission\",\n  \"not allowed\",\n  \"denied\",\n  \"notallowederror\",\n  \"dismissedbyuser\",\n  \"permission is blocked\",\n];\n\nconst STREAM_PATTERNS = [\n  \"no video track\",\n  \"no audio\",\n  \"getusermedia\",\n  \"getdisplaymedia\",\n  \"stream unavailable\",\n  \"camera stream\",\n  \"display stream missing\",\n  \"failed to get user media\",\n  \"failed to start streaming\",\n  \"failed to start stream\",\n  \"failed to setup streaming\",\n  \"failed to get stream\",\n  \"failed to access screen stream\",\n  \"failed to access region stream\",\n  \"no streams to record\",\n  \"no screen video track\",\n  \"no camera video track\",\n  \"notreadableerror\",\n  \"notfounderror\",\n  \"overconstrainederror\",\n];\n\nconst CODEC_PATTERNS = [\n  \"mediarecorder\",\n  \"webcodecs\",\n  \"failed to start recording\",\n  \"codec\",\n  \"mimeType\",\n  \"mime type\",\n  \"recording error:\",\n];\n\nconst TAB_PATTERNS = [\n  \"no recording tab\",\n  \"tab stream\",\n  \"unable to resolve tab\",\n  \"tab capture\",\n  \"message routing\",\n];\n\nconst TIMEOUT_PATTERNS = [\n  \"taking too long\",\n  \"timed out\",\n  \"timeout\",\n];\n\nconst REGION_PATTERNS = [\n  \"crop target\",\n  \"crop region\",\n  \"failed to crop\",\n  \"region capture\",\n  \"no crop target\",\n];\n\n/**\n * Classify an error string into an error code.\n *\n * @param {string} errorStr  — raw error message\n * @param {string} [errorType] — optional hint (\"stream-error\", \"cancel-modal\", etc.)\n * @returns {string} one of the exported error code constants\n */\nexport const classifyError = (errorStr = \"\", errorType = \"\") => {\n  const lower = String(errorStr).toLowerCase();\n  const typeLower = String(errorType).toLowerCase();\n\n  // Explicit cancel — either the caller flagged it or the message clearly says so.\n  // AbortError only counts as cancel when the caller confirms via \"cancel-modal\",\n  // since Chrome reuses AbortError for non-cancel scenarios too.\n  if (typeLower === \"cancel-modal\") {\n    return REC_START_CANCEL;\n  }\n  if (CANCEL_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_CANCEL;\n  }\n\n  // Permission denied\n  if (PERM_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_PERM;\n  }\n\n  // Recorder not ready (stream ref missing, likely tab discarded during countdown)\n  if (lower.includes(\"recording not ready\") || lower.includes(\"screen stream is missing\")) {\n    return REC_START_NOT_READY;\n  }\n\n  // Stream acquisition\n  if (STREAM_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_STREAM;\n  }\n\n  // Codec / recorder init\n  if (CODEC_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_CODEC;\n  }\n\n  // Timeout\n  if (TIMEOUT_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_TIMEOUT;\n  }\n\n  // Region / crop\n  if (REGION_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_REGION;\n  }\n\n  // Stream ended (runtime, not start)\n  if (typeLower === \"stream-ended\" || lower.includes(\"stream ended\")) {\n    return REC_RUN_STREAM_END;\n  }\n\n  // Tab routing\n  if (TAB_PATTERNS.some((p) => lower.includes(p))) {\n    return REC_START_TAB;\n  }\n\n  // Memory / storage\n  if (\n    lower.includes(\"memory\") ||\n    lower.includes(\"quota\") ||\n    lower.includes(\"indexeddb\") ||\n    lower.includes(\"local storage is blocked\") ||\n    lower.includes(\"local buffer\")\n  ) {\n    return REC_RUN_MEMORY;\n  }\n\n  // Bare AbortError without a cancel-modal hint — this is ambiguous, so treat\n  // it as a stream-level failure rather than assuming user intent.\n  if (lower.includes(\"aborterror\")) {\n    return REC_START_STREAM;\n  }\n\n  return UNKNOWN;\n};\n\n// -- Helpers --\n\n/** Short ID for a recording attempt (no crypto dependency). */\nexport const makeRecordingAttemptId = () => {\n  const ts = Date.now().toString(36);\n  const rand = Math.random().toString(36).slice(2, 8);\n  return `ra-${ts}-${rand}`;\n};\n\n/** Derive a short support code from an attempt ID. */\nexport const makeSupportCode = (attemptId) => {\n  if (!attemptId) return \"SCR-0000\";\n  const hash = attemptId.replace(/[^a-z0-9]/gi, \"\").slice(-4).toUpperCase();\n  const date = new Date().toISOString().slice(0, 10).replace(/-/g, \"\");\n  return `SCR-${hash}-${date}`;\n};\n"
  },
  {
    "path": "src/pages/utils/filenameHelpers.js",
    "content": "export const sanitizeFilenameBase = (value) => {\n  const fallback = \"Screenity recording\";\n  if (typeof value !== \"string\") return fallback;\n\n  let sanitized = value.replace(/[\\u0000-\\u001f\\u007f]/g, \"\");\n  sanitized = sanitized.replace(/[\\\\/:*?\"<>|]/g, \"-\");\n  sanitized = sanitized.replace(/\\s+/g, \" \").trim();\n\n  if (sanitized.length > 80) {\n    sanitized = sanitized.slice(0, 80).trim();\n  }\n\n  return sanitized.length > 0 ? sanitized : fallback;\n};\n\nexport const formatLocalTimestamp = (value) => {\n  const date = value ? new Date(value) : new Date();\n  const safeDate = Number.isNaN(date.getTime()) ? new Date() : date;\n  // Use Intl.DateTimeFormat to respect user locale and time format.\n  return new Intl.DateTimeFormat(undefined, {\n    month: \"short\",\n    day: \"numeric\",\n    hour: \"numeric\",\n    minute: \"2-digit\",\n  }).format(safeDate);\n};\n\nexport const getHostnameFromUrl = (url) => {\n  if (!url) return \"\";\n  try {\n    const parsed = new URL(url);\n    let hostname = parsed.hostname || \"\";\n    if (hostname.startsWith(\"www.\")) {\n      hostname = hostname.slice(4);\n    }\n    return hostname;\n  } catch {\n    return \"\";\n  }\n};\n\nexport const getTabRecordingBaseTitle = (title, url) => {\n  const candidate = title && title.trim().length > 0 ? title : getHostnameFromUrl(url);\n  return sanitizeFilenameBase(candidate);\n};\n"
  },
  {
    "path": "src/pages/utils/mediaDeviceFallback.js",
    "content": "const DEVICE_ID_ERRORS = new Set([\"OverconstrainedError\", \"NotFoundError\"]);\n\nconst isDeviceIdError = (err) => err && DEVICE_ID_ERRORS.has(err.name);\n\nconst cloneConstraints = (constraints) =>\n  JSON.parse(JSON.stringify(constraints || {}));\n\nconst updateConstraintsDeviceId = (constraints, kind, deviceId) => {\n  if (!constraints || !deviceId) return;\n\n  if (kind === \"audioinput\") {\n    if (!constraints.audio || typeof constraints.audio !== \"object\") return;\n    constraints.audio = {\n      ...constraints.audio,\n      deviceId: { exact: deviceId },\n    };\n    return;\n  }\n\n  if (kind === \"videoinput\") {\n    if (!constraints.video || typeof constraints.video !== \"object\") return;\n    constraints.video = {\n      ...constraints.video,\n      deviceId: { exact: deviceId },\n    };\n  }\n};\n\nexport const enumerateCurrentDevices = async () => {\n  try {\n    return await navigator.mediaDevices.enumerateDevices();\n  } catch (err) {\n    console.warn(\"Failed to enumerate devices:\", err);\n    return [];\n  }\n};\n\nexport const resolveDeviceIdByLabel = async (kind, desiredLabel) => {\n  if (!desiredLabel) return null;\n  const devices = await enumerateCurrentDevices();\n  const matches = devices.filter(\n    (device) => device.kind === kind && device.label === desiredLabel\n  );\n  if (matches.length !== 1) return null;\n  return matches[0].deviceId;\n};\n\nexport const getUserMediaWithFallback = async ({\n  constraints,\n  fallbacks = [],\n}) => {\n  try {\n    return await navigator.mediaDevices.getUserMedia(constraints);\n  } catch (err) {\n    if (!isDeviceIdError(err) || fallbacks.length === 0) {\n      throw err;\n    }\n\n    const nextConstraints = cloneConstraints(constraints);\n    const resolved = [];\n\n    for (const fallback of fallbacks) {\n      const { kind, desiredLabel, desiredDeviceId } = fallback || {};\n      if (!kind || !desiredLabel || !desiredDeviceId) continue;\n      const resolvedId = await resolveDeviceIdByLabel(kind, desiredLabel);\n      if (!resolvedId || resolvedId === desiredDeviceId) continue;\n      updateConstraintsDeviceId(nextConstraints, kind, resolvedId);\n      resolved.push({ ...fallback, resolvedId });\n    }\n\n    if (resolved.length === 0) {\n      throw err;\n    }\n\n    console.warn(\n      \"[Screenity] Retrying getUserMedia with label-matched device IDs\"\n    );\n\n    const stream = await navigator.mediaDevices.getUserMedia(nextConstraints);\n    resolved.forEach(({ resolvedId, onResolved }) => {\n      if (typeof onResolved === \"function\") {\n        onResolved(resolvedId);\n      }\n    });\n    return stream;\n  }\n};\n"
  },
  {
    "path": "src/pages/utils/recordingDebug.js",
    "content": "let recordingDebugEnabled = false;\nlet recordingDebugSession = null;\n\nconst hasWindowDebugFlag = () => {\n  if (typeof window === \"undefined\") return false;\n  return !!window.SCREENITY_DEBUG_RECORDER;\n};\n\nconst ensureSession = (sessionOverride) => {\n  if (sessionOverride?.sessionId) return sessionOverride;\n  if (recordingDebugSession?.sessionId) return recordingDebugSession;\n\n  const sessionId = `recdbg-${Date.now()}-${Math.random()\n    .toString(16)\n    .slice(2, 7)}`;\n  const startTimeMs = Date.now();\n  recordingDebugSession = {\n    sessionId,\n    startTimeMs,\n    startPerfMs: null,\n  };\n\n  try {\n    chrome.storage?.local?.set?.({\n      recordingDebugEnabled: true,\n      recordingDebugSessionId: sessionId,\n      recordingDebugStartMs: startTimeMs,\n    });\n  } catch {}\n\n  return recordingDebugSession;\n};\n\nexport const hydrateRecordingDebugFlag = async () => {\n  try {\n    const res = await chrome.storage?.local?.get?.([\n      \"recordingDebugEnabled\",\n      \"recordingDebugSessionId\",\n      \"recordingDebugStartMs\",\n    ]);\n    recordingDebugEnabled = Boolean(\n      res?.recordingDebugEnabled || res?.recordingDebugSessionId,\n    );\n    if (res?.recordingDebugSessionId) {\n      recordingDebugSession = {\n        sessionId: res.recordingDebugSessionId,\n        startTimeMs: res.recordingDebugStartMs || Date.now(),\n        startPerfMs: null,\n      };\n    }\n  } catch {}\n};\n\nexport const resetRecordingDebugSession = async () => {\n  recordingDebugEnabled = false;\n  recordingDebugSession = null;\n  try {\n    await chrome.storage?.local?.remove?.([\n      \"recordingDebugEnabled\",\n      \"recordingDebugSessionId\",\n      \"recordingDebugStartMs\",\n    ]);\n  } catch {}\n};\n\nexport const isRecordingDebugEnabled = () =>\n  recordingDebugEnabled || hasWindowDebugFlag();\n\nexport const debugRecordingEventWithSession = (session, eventType, payload) => {\n  if (!isRecordingDebugEnabled()) return;\n  const activeSession = ensureSession(session);\n  const now = Date.now();\n  const tSinceStartMs =\n    activeSession?.startTimeMs != null ? now - activeSession.startTimeMs : null;\n\n  try {\n    chrome.runtime?.sendMessage?.({\n      type: \"recdbg\",\n      eventType,\n      payload,\n      sessionId: activeSession?.sessionId || null,\n      tSinceStartMs,\n      ts: now,\n    });\n  } catch {}\n};\n\nexport const debugRecordingEvent = (sessionRef, eventType, payload) => {\n  const session = sessionRef?.current || sessionRef || null;\n  debugRecordingEventWithSession(session, eventType, payload);\n};\n"
  },
  {
    "path": "src/pages/utils/startFlowTrace.js",
    "content": "/**\n * Start-flow trace for recording diagnostics.\n *\n * Single bounded object in chrome.storage.local (\"startFlowTrace\"),\n * overwritten each attempt. No URLs, no page content, no user identity\n * beyond isPro boolean. Error text is truncated and URL-stripped.\n */\n\nconst STORAGE_KEY = \"startFlowTrace\";\nconst MAX_ERR_LEN = 120;\n\nconst sanitize = (str) => {\n  if (!str) return null;\n  return String(str)\n    .replace(/https?:\\/\\/[^\\s)]+/gi, \"[url]\")\n    .replace(/chrome-extension:\\/\\/[^\\s)]+/gi, \"[ext]\")\n    .slice(0, MAX_ERR_LEN);\n};\n\n/** Create a fresh trace for a new recording attempt. */\nexport const initStartFlowTrace = async (attemptId, config = {}) => {\n  const trace = {\n    attemptId: attemptId || null,\n    recordingType: config.recordingType || null,\n    surface: config.surface || null,\n    isPro: Boolean(config.isPro),\n    countdown: Boolean(config.countdown),\n    outcome: \"in-progress\",\n\n    t: {\n      startStreaming: null,\n      desktopCaptureSent: null,\n      recorderTabCreated: null,\n      streamAcquired: null,\n      preparingSent: null,\n      preparingReceived: null,\n      apiProjectCreated: null,\n      apiUploadersReady: null,\n      resetActiveTabSent: null,\n      readyToRecordSent: null,\n      readyToRecordReceived: null,\n      countdownStart: null,\n      countdownEnd: null,\n      recordingStarted: null,\n    },\n\n    routing: {\n      targetTabId: null,\n      activeTab: null,\n      currentTabId: null,\n      shouldFocusTab: null,\n    },\n\n    error: null,\n    errorCode: null,\n    stuck: null,\n  };\n\n  try {\n    await chrome.storage.local.set({ [STORAGE_KEY]: trace });\n  } catch {\n    // best effort\n  }\n  return trace;\n};\n\n/** Write a timestamp checkpoint. Merges extra fields without overwriting others. */\nexport const traceStep = async (stepName, extra = {}) => {\n  try {\n    const res = await chrome.storage.local.get(STORAGE_KEY);\n    const trace = res?.[STORAGE_KEY];\n    if (!trace) return;\n\n    if (trace.t && stepName in trace.t) {\n      trace.t[stepName] = Date.now();\n    }\n\n    // Merge extra fields (surface, routing, etc.)\n    for (const [key, value] of Object.entries(extra)) {\n      if (key === \"routing\" && trace.routing) {\n        Object.assign(trace.routing, value);\n      } else if (key !== \"t\") {\n        trace[key] = value;\n      }\n    }\n\n    await chrome.storage.local.set({ [STORAGE_KEY]: trace });\n  } catch {\n    // best effort\n  }\n};\n\n/** Set the final outcome. Only overwrites \"in-progress\". */\nexport const setStartFlowOutcome = async (outcome, extra = {}) => {\n  try {\n    const res = await chrome.storage.local.get(STORAGE_KEY);\n    const trace = res?.[STORAGE_KEY];\n    if (!trace) return;\n\n    const wasInProgress = trace.outcome === \"in-progress\";\n    if (wasInProgress) {\n      trace.outcome = outcome;\n    }\n\n    if (extra.error) trace.error = sanitize(extra.error);\n    if (extra.errorCode) trace.errorCode = extra.errorCode;\n    if (extra.stuck) trace.stuck = extra.stuck;\n\n    await chrome.storage.local.set({ [STORAGE_KEY]: trace });\n\n    // Submit to server for Pro users on terminal outcomes\n    if (wasInProgress) {\n      if (outcome === \"error\" || outcome === \"stuck\") {\n        submitDiagnosticReport(outcome);\n      } else if (outcome === \"cancelled\" && extra.error) {\n        // Only submit cancelled if there's a diagnostic reason\n        // (permission-denied, quota issues) not plain user dismiss\n        submitDiagnosticReport(\"cancelled\");\n      } else if (outcome === \"ok\") {\n        submitDiagnosticReport(\"success-summary\");\n      }\n    }\n  } catch {\n    // best effort\n  }\n};\n\n/** Read the current trace for export. */\nexport const getStartFlowTrace = async () => {\n  try {\n    const res = await chrome.storage.local.get(STORAGE_KEY);\n    return res?.[STORAGE_KEY] || null;\n  } catch {\n    return null;\n  }\n};\n\n// Track submitted attempt IDs to avoid duplicate submissions\nconst submittedAttempts = new Set();\n\n/**\n * Submit the current trace to the server for Pro users.\n * Delegates to the background service worker which has no origin\n * restrictions. Best-effort, fire-and-forget. No retries.\n */\nexport const submitDiagnosticReport = async (trigger) => {\n  try {\n    const res = await chrome.storage.local.get([\n      STORAGE_KEY,\n      \"isSubscribed\",\n      \"isLoggedIn\",\n    ]);\n    const trace = res?.[STORAGE_KEY];\n    if (!trace) return;\n\n    // Only submit for Pro users\n    if (!res.isSubscribed || !res.isLoggedIn) return;\n\n    // Dedupe by attemptId\n    if (!trace.attemptId || submittedAttempts.has(trace.attemptId)) return;\n    submittedAttempts.add(trace.attemptId);\n\n    // Send to background for the actual network call\n    chrome.runtime.sendMessage({\n      type: \"submit-diagnostic-report\",\n      trigger,\n    }).catch(() => {});\n  } catch {\n    // best effort\n  }\n};\n\n/** Format the trace as a compact human-readable timeline. */\nexport const formatStartFlowTimeline = (trace) => {\n  if (!trace?.t) return null;\n\n  const base = trace.t.startStreaming;\n  if (!base) return null;\n\n  const STEP_ORDER = [\n    \"startStreaming\",\n    \"desktopCaptureSent\",\n    \"recorderTabCreated\",\n    \"streamAcquired\",\n    \"preparingSent\",\n    \"preparingReceived\",\n    \"apiProjectCreated\",\n    \"apiUploadersReady\",\n    \"resetActiveTabSent\",\n    \"readyToRecordSent\",\n    \"readyToRecordReceived\",\n    \"countdownStart\",\n    \"countdownEnd\",\n    \"recordingStarted\",\n  ];\n\n  const lines = [];\n  for (const step of STEP_ORDER) {\n    const ts = trace.t[step];\n    if (ts != null) {\n      const delta = ts - base;\n      lines.push(`  ${step.padEnd(26)} T+${delta}ms`);\n    } else {\n      lines.push(`  ${step.padEnd(26)} (not reached)`);\n    }\n  }\n\n  const lastTs = trace.t.recordingStarted;\n  const outcomeStr = trace.outcome || \"unknown\";\n  if (lastTs) {\n    lines.push(`  Outcome: ${outcomeStr} (${((lastTs - base) / 1000).toFixed(2)}s total)`);\n  } else {\n    lines.push(`  Outcome: ${outcomeStr}`);\n  }\n\n  if (trace.stuck) {\n    lines.push(`  Stuck: ${trace.stuck.state} for ${trace.stuck.durationMs}ms`);\n  }\n\n  return lines.join(\"\\n\");\n};\n"
  },
  {
    "path": "src/schema.json",
    "content": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"skipSetup\": {\n      \"type\": \"boolean\",\n      \"description\": \"If true, prevents setup.html from opening on install.\"\n    }\n  }\n}"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": false,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"noEmit\": false,\n    \"checkJs\": false,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"isolatedModules\": true\n  },\n  \"include\": [\n    \"src\"\n  ],\n  \"exclude\": [\n    \"build\",\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "utils/autoReloadClients/backgroundClient.js",
    "content": "const querystring = require(\"querystring\");\n\nconst logger = (msg) => {\n  console.log(`[Background] ${msg}`);\n};\n\nlogger(\"Initializing auto-reload client\");\n\n// Parse port from query string\nconst port = querystring.parse(__resourceQuery.slice(1)).port;\n\n// Connect to SSE endpoint\nconst es = new EventSource(`http://localhost:${port}/__server_sent_events__`);\n\n// Handle connection events\nes.addEventListener(\n  \"open\",\n  () => {\n    logger(\"Connected to dev server\");\n  },\n  false\n);\n\nes.addEventListener(\n  \"error\",\n  (event) => {\n    if (event.target.readyState === 0) {\n      console.error(\n        \"[Background] Dev server connection failed - is it running?\"\n      );\n    } else {\n      console.error(\"[Background] SSE error:\", event);\n    }\n  },\n  false\n);\n\n// Handle background script update events\nes.addEventListener(\n  \"background-updated\",\n  () => {\n    logger(\"Background script updated, reloading extension...\");\n    chrome.runtime.reload();\n  },\n  false\n);\n\n// Handle content script update events\nes.addEventListener(\n  \"content-scripts-updated\",\n  () => {\n    logger(\"Content scripts updated, notifying tabs...\");\n\n    // Query all tabs to send reload messages\n    chrome.tabs.query({}, (tabs) => {\n      const reloadPromises = tabs.map((tab) => {\n        return new Promise((resolve) => {\n          try {\n            chrome.tabs.sendMessage(\n              tab.id,\n              { from: \"backgroundClient\", action: \"reload-yourself\" },\n              (response) => {\n                // Handle any runtime errors from sendMessage\n                if (chrome.runtime.lastError) {\n                  // Tab might not have content script running, ignore error\n                  resolve(false);\n                  return;\n                }\n\n                if (response && response.from === \"contentScriptClient\") {\n                  logger(`Tab ${tab.id} acknowledged reload request`);\n                  resolve(true);\n                } else {\n                  resolve(false);\n                }\n              }\n            );\n          } catch (err) {\n            logger(`Error sending message to tab ${tab.id}: ${err.message}`);\n            resolve(false);\n          }\n        });\n      });\n\n      // Wait for all tabs to respond or timeout\n      Promise.all(reloadPromises).then((results) => {\n        const successCount = results.filter(Boolean).length;\n        logger(`${successCount}/${tabs.length} tabs acknowledged reload`);\n\n        // Only reload extension if we got at least one successful tab response\n        if (successCount > 0) {\n          logger(\"Reloading extension to apply content script changes\");\n          es.close();\n          chrome.runtime.reload();\n        }\n      });\n    });\n  },\n  false\n);\n"
  },
  {
    "path": "utils/autoReloadClients/contentScriptClient.js",
    "content": "const logger = (msg) => {\n  console.log(`[Content Script] ${msg}`);\n};\n\nlogger(\"Initialized reload listener\");\n\n// Listen for reload messages from background script\nchrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {\n  const shouldReload =\n    request.from === \"backgroundClient\" && request.action === \"reload-yourself\";\n\n  if (shouldReload) {\n    logger(\"Received reload request from background\");\n    sendResponse({ from: \"contentScriptClient\", action: \"acknowledged\" });\n\n    // Short delay before reload to ensure response is sent\n    setTimeout(() => {\n      logger(\"Reloading page...\");\n      window.location.reload();\n    }, 100);\n  }\n});\n"
  },
  {
    "path": "utils/build.js",
    "content": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = \"production\";\nprocess.env.NODE_ENV = \"production\";\nprocess.env.ASSET_PATH = \"/\";\n\nvar webpack = require(\"webpack\"),\n  config = require(\"../webpack.config\");\n\n//delete config.chromeExtensionBoilerplate;\ndelete config.custom;\n\nconfig.mode = \"production\";\n\nwebpack(config, (err, stats) => {\n  if (err) {\n    console.error(\"Webpack compilation error:\", err);\n    throw err;\n  }\n\n  if (stats.hasErrors()) {\n    console.error(\"Webpack compilation failed with errors:\");\n    const info = stats.toJson();\n    console.error(info.errors);\n    process.exit(1);\n  }\n\n  if (stats.hasWarnings()) {\n    console.warn(\"Webpack compilation had warnings:\");\n    const info = stats.toJson();\n    console.warn(info.warnings);\n  }\n\n  console.log(\"Production build completed successfully!\");\n});\n"
  },
  {
    "path": "utils/env.js",
    "content": "// tiny wrapper with default env vars\nmodule.exports = {\n  NODE_ENV: process.env.NODE_ENV || \"development\",\n  PORT: process.env.PORT || 3001,\n};\n"
  },
  {
    "path": "utils/server.js",
    "content": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = \"development\";\nprocess.env.NODE_ENV = \"development\";\nprocess.env.ASSET_PATH = \"/\";\n\nconst WebpackDevServer = require(\"webpack-dev-server\");\nconst webpack = require(\"webpack\");\nconst config = require(\"../webpack.config\");\nconst env = require(\"./env\");\nconst path = require(\"path\");\nconst { debounce } = require(\"lodash\");\nconst SSEStream = require(\"ssestream\").default;\n\nconst customOptions = require(\"../custom.config\");\n\nfor (const entryName in config.entry) {\n  if (!customOptions.notHMR.includes(entryName)) {\n    config.entry[entryName] = [\n      \"webpack/hot/dev-server.js\",\n      `webpack-dev-server/client/index.js?hot=true&hostname=localhost&port=${env.PORT}`,\n    ].concat(config.entry[entryName]);\n  }\n}\n\nif (\n  customOptions.enableBackgroundAutoReload ||\n  customOptions.enableContentScriptsAutoReload\n) {\n  config.entry[\"background\"] = [\n    path.resolve(\n      __dirname,\n      `autoReloadClients/backgroundClient.js?port=${env.PORT}`\n    ),\n  ].concat(config.entry[\"background\"]);\n}\n\nif (customOptions.enableContentScriptsAutoReload) {\n  config.entry[\"contentScript\"] = [\n    path.resolve(__dirname, \"autoReloadClients/contentScriptClient.js\"),\n  ].concat(config.entry[\"contentScript\"]);\n}\n\nconfig.plugins = [new webpack.HotModuleReplacementPlugin()].concat(\n  config.plugins || []\n);\n\ndelete config.custom;\n\nconst compiler = webpack(config);\n\nconst server = new WebpackDevServer(\n  {\n    https: false,\n    hot: false, // We're handling HMR manually\n    client: false,\n    compress: false, // Important: Keep false for SSE to work\n    host: \"localhost\",\n    port: env.PORT,\n    static: {\n      directory: path.join(__dirname, \"../build\"),\n    },\n    devMiddleware: {\n      publicPath: `http://localhost:${env.PORT}/`,\n      writeToDisk: true,\n    },\n    headers: {\n      \"Access-Control-Allow-Origin\": \"*\",\n    },\n    allowedHosts: \"all\",\n    setupMiddlewares: (middlewares, devServer) => {\n      // Skip middleware setup if auto-reload is disabled\n      if (\n        !customOptions.enableBackgroundAutoReload &&\n        !customOptions.enableContentScriptsAutoReload\n      ) {\n        return middlewares;\n      }\n\n      if (!devServer) {\n        throw new Error(\"webpack-dev-server is not defined\");\n      }\n\n      // Add SSE middleware for extension reloading\n      middlewares.push({\n        path: \"/__server_sent_events__\",\n        middleware: (req, res) => {\n          const sseStream = new SSEStream(req);\n          sseStream.pipe(res);\n\n          sseStream.write(\"Dev server connected\");\n\n          let closed = false;\n\n          // Debounce compile events to prevent excessive reloads\n          const compileDoneHandler = debounce((stats) => {\n            if (closed || stats.hasErrors()) return;\n\n            const { modules } = stats.toJson({ all: false, modules: true });\n            const updatedJsModules = modules.filter(\n              (module) =>\n                module.type === \"module\" &&\n                module.moduleType === \"javascript/auto\"\n            );\n\n            // Check which parts of the extension have been updated\n            const isBackgroundUpdated = updatedJsModules.some((module) =>\n              module.nameForCondition.startsWith(\n                path.resolve(__dirname, \"../src/pages/Background\")\n              )\n            );\n\n            const isContentScriptsUpdated = updatedJsModules.some((module) =>\n              module.nameForCondition.startsWith(\n                path.resolve(__dirname, \"../src/pages/Content\")\n              )\n            );\n\n            // Determine what needs to be reloaded based on changes and config\n            const shouldBackgroundReload =\n              isBackgroundUpdated && customOptions.enableBackgroundAutoReload;\n\n            const shouldContentScriptsReload =\n              isContentScriptsUpdated &&\n              customOptions.enableContentScriptsAutoReload;\n\n            // Send appropriate events through SSE\n            if (shouldBackgroundReload) {\n              sseStream.writeMessage(\n                {\n                  event: \"background-updated\",\n                  data: {},\n                },\n                \"utf-8\"\n              );\n            }\n\n            if (shouldContentScriptsReload) {\n              sseStream.writeMessage(\n                {\n                  event: \"content-scripts-updated\",\n                  data: {},\n                },\n                \"utf-8\"\n              );\n            }\n          }, 1000);\n\n          // Register plugin with webpack compiler\n          compiler.hooks.done.tap(\"extension-auto-reload-plugin\", (stats) => {\n            if (!closed) {\n              compileDoneHandler(stats);\n            }\n          });\n\n          // Clean up when connection closes\n          res.on(\"close\", () => {\n            closed = true;\n            sseStream.unpipe(res);\n          });\n        },\n      });\n\n      return middlewares;\n    },\n  },\n  compiler\n);\n\nconst startServer = async () => {\n  try {\n    await server.start();\n    console.log(`Dev server running at http://localhost:${env.PORT}`);\n  } catch (error) {\n    console.error(\"Failed to start dev server:\", error);\n    process.exit(1);\n  }\n};\n\nstartServer();\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require(\"webpack\");\nconst path = require(\"path\");\nconst fileSystem = require(\"fs-extra\");\nconst env = require(\"./utils/env\");\nconst CopyWebpackPlugin = require(\"copy-webpack-plugin\");\nconst HtmlWebpackPlugin = require(\"html-webpack-plugin\");\nconst TerserPlugin = require(\"terser-webpack-plugin\");\n\nconst isDev = env.NODE_ENV === \"development\";\n\nconst ASSET_PATH = process.env.ASSET_PATH || \"/\";\n\nrequire(\"dotenv\").config();\n\n// Entry points for the different pages\nconst entryPoints = {\n  background: path.join(__dirname, \"src\", \"pages\", \"Background\", \"index.js\"),\n  contentScript: path.join(__dirname, \"src\", \"pages\", \"Content\", \"index.jsx\"),\n  recorder: path.join(__dirname, \"src\", \"pages\", \"Recorder\", \"index.jsx\"),\n  cloudrecorder: path.join(\n    __dirname,\n    \"src\",\n    \"pages\",\n    \"CloudRecorder\",\n    \"index.jsx\"\n  ),\n  recorderoffscreen: path.join(\n    __dirname,\n    \"src\",\n    \"pages\",\n    \"RecorderOffscreen\",\n    \"index.jsx\"\n  ),\n  audiooffscreen: path.join(\n    __dirname,\n    \"src\",\n    \"pages\",\n    \"AudioOffscreen\",\n    \"index.js\"\n  ),\n  camera: path.join(__dirname, \"src\", \"pages\", \"Camera\", \"index.jsx\"),\n  waveform: path.join(__dirname, \"src\", \"pages\", \"Waveform\", \"index.jsx\"),\n  sandbox: path.join(__dirname, \"src\", \"pages\", \"Sandbox\", \"index.jsx\"),\n  permissions: path.join(__dirname, \"src\", \"pages\", \"Permissions\", \"index.jsx\"),\n  setup: path.join(__dirname, \"src\", \"pages\", \"Setup\", \"index.jsx\"),\n  playground: path.join(__dirname, \"src\", \"pages\", \"Playground\", \"index.jsx\"),\n  editor: path.join(__dirname, \"src\", \"pages\", \"Editor\", \"index.jsx\"),\n  region: path.join(__dirname, \"src\", \"pages\", \"Region\", \"index.jsx\"),\n  download: path.join(__dirname, \"src\", \"pages\", \"Download\", \"index.jsx\"),\n  editorwebcodecs: path.join(\n    __dirname,\n    \"src\",\n    \"pages\",\n    \"EditorWebCodecs\",\n    \"index.jsx\"\n  ),\n  editorviewer: path.join(\n    __dirname,\n    \"src\",\n    \"pages\",\n    \"EditorViewer\",\n    \"index.jsx\"\n  ),\n  backup: path.join(__dirname, \"src\", \"pages\", \"Backup\", \"index.jsx\"),\n};\n\nconst htmlPlugins = Object.keys(entryPoints)\n  .map((entryName) => {\n    // Skip background script as it doesn't need an HTML file\n    if (entryName === \"background\" || entryName === \"contentScript\") {\n      return null;\n    }\n\n    // Map entry names to folder names (for multi-word entries)\n    const folderNameMap = {\n      cloudrecorder: \"CloudRecorder\",\n      recorderoffscreen: \"RecorderOffscreen\",\n      audiooffscreen: \"AudioOffscreen\",\n      editorwebcodecs: \"EditorWebCodecs\",\n      editorviewer: \"EditorViewer\",\n    };\n\n    const folderName =\n      folderNameMap[entryName] ||\n      entryName.charAt(0).toUpperCase() + entryName.slice(1);\n\n    const templatePath = path.join(\n      __dirname,\n      \"src\",\n      \"pages\",\n      folderName,\n      \"index.html\"\n    );\n\n    const options = {\n      template: templatePath,\n      filename: `${entryName}.html`,\n      chunks: [entryName],\n      cache: true,\n    };\n\n    // Add favicon only for backup page\n    if (entryName === \"backup\") {\n      options.favicon = path.join(\n        __dirname,\n        \"src\",\n        \"assets\",\n        \"backup-favicon.ico\"\n      );\n    } else {\n      options.favicon = path.join(__dirname, \"src\", \"assets\", \"favicon.png\");\n    }\n\n    return new HtmlWebpackPlugin(options);\n  })\n  .filter(Boolean); // Filter out null values\n\nconst fileExtensions = [\n  \"jpg\",\n  \"jpeg\",\n  \"png\",\n  \"gif\",\n  \"eot\",\n  \"otf\",\n  \"svg\",\n  \"ttf\",\n  \"woff\",\n  \"woff2\",\n];\n\nconst secretsPath = path.join(__dirname, `secrets.${env.NODE_ENV}.js`);\nconst alias = { \"react-dom\": \"@hot-loader/react-dom\" };\n\nif (fileSystem.existsSync(secretsPath)) {\n  alias[\"secrets\"] = secretsPath;\n}\n\nconst config = {\n  mode: process.env.NODE_ENV || \"production\",\n  performance: { hints: false },\n  entry: entryPoints,\n\n  // Persistent filesystem cache for fast rebuilds\n  cache: {\n    type: \"filesystem\",\n    buildDependencies: {\n      config: [__filename],\n    },\n  },\n\n  output: {\n    filename: \"[name].bundle.js\",\n    path: path.resolve(__dirname, \"build\"),\n    clean: !isDev, // Only wipe build dir in production; dev keeps it to avoid re-copying 40MB of assets\n    publicPath: ASSET_PATH,\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.(css|scss)$/,\n        use: [\n          { loader: \"style-loader\" },\n          { loader: \"css-loader\" },\n          {\n            loader: \"sass-loader\",\n            options: { sourceMap: true },\n          },\n        ],\n      },\n      {\n        test: new RegExp(`.(${fileExtensions.join(\"|\")})$`),\n        type: \"asset/resource\",\n        exclude: /node_modules/,\n      },\n      {\n        test: /\\.html$/,\n        loader: \"html-loader\",\n        exclude: /node_modules/,\n      },\n      {\n        test: /\\.(ts|tsx)$/,\n        loader: \"ts-loader\",\n        exclude: /node_modules/,\n        options: {\n          transpileOnly: isDev,\n        },\n      },\n      {\n        test: /\\.(js|jsx)$/,\n        use: isDev\n          ? [{ loader: \"babel-loader\" }]\n          : [{ loader: \"source-map-loader\" }, { loader: \"babel-loader\" }],\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  resolve: {\n    alias: {\n      react: path.resolve(\"./node_modules/react\"),\n      \"react-dom\": path.resolve(\"./node_modules/react-dom\"),\n      \"react/jsx-runtime\": path.resolve(\"./node_modules/react/jsx-runtime\"),\n    },\n    // Code extensions first — image/font extensions are only needed for explicit imports with extensions\n    extensions: [\".js\", \".jsx\", \".ts\", \".tsx\", \".css\"],\n  },\n  plugins: [\n    new webpack.ProgressPlugin(),\n    new webpack.DefinePlugin({\n      \"process.env.SCREENITY_APP_BASE\": JSON.stringify(\n        process.env.SCREENITY_APP_BASE\n      ),\n      \"process.env.SCREENITY_WEBSITE_BASE\": JSON.stringify(\n        process.env.SCREENITY_WEBSITE_BASE\n      ),\n      \"process.env.SCREENITY_API_BASE_URL\": JSON.stringify(\n        process.env.SCREENITY_API_BASE_URL\n      ),\n      \"process.env.SCREENITY_ENABLE_CLOUD_FEATURES\": JSON.stringify(\n        process.env.SCREENITY_ENABLE_CLOUD_FEATURES\n      ),\n      \"process.env.MAX_RECORDING_DURATION\": JSON.stringify(\n        process.env.MAX_RECORDING_DURATION || 3600 // Default to 1 hour\n      ),\n      \"process.env.RECORDING_WARNING_THRESHOLD\": JSON.stringify(\n        process.env.RECORDING_WARNING_THRESHOLD || 60 // Default to 1 minute\n      ),\n      \"process.env.SCREENITY_DEV_MODE\": JSON.stringify(\n        process.env.SCREENITY_DEV_MODE || \"\"\n      ),\n    }),\n\n    // Copy manifest and transform with package info\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: \"src/manifest.json\",\n          to: path.join(__dirname, \"build\"),\n          force: true,\n          transform: (content) => {\n            return Buffer.from(\n              JSON.stringify({\n                description: process.env.npm_package_description,\n                version: process.env.npm_package_version,\n                ...JSON.parse(content.toString()),\n              })\n            );\n          },\n        },\n        {\n          from: \"src/schema.json\",\n          to: path.join(__dirname, \"build/schema.json\"),\n          force: true,\n        },\n        {\n          from: \"src/assets/\",\n          to: path.join(__dirname, \"build/assets\"),\n          force: true,\n        },\n        {\n          from: \"src/_locales/\",\n          to: path.join(__dirname, \"build/_locales\"),\n          force: true,\n        },\n      ],\n    }),\n    ...htmlPlugins,\n  ],\n};\n\nif (isDev) {\n  config.devtool = \"cheap-module-source-map\";\n} else {\n  config.optimization = {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        extractComments: false,\n      }),\n    ],\n  };\n}\n\nmodule.exports = config;\n"
  }
]